UNPKG

@wishcbg/bobcode

Version:

基於店家同步功能的需求,讓店家能夠在不同的店家之間同步各項功能設定,並且能夠檢視各項功能的異動情況。 規劃出一套店家設定即程式碼的機制,讓店家能夠以 config file (.sac) 來定義各項功能設定,並且能夠還原店家各項功能設定。 暫且稱這個機制為「Settings as Code」「店家設定即程式碼」。

211 lines (210 loc) 8.47 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.generateChangeSet = generateChangeSet; const jsonpath_plus_1 = require("jsonpath-plus"); const deep_diff_1 = __importDefault(require("deep-diff")); const index_js_1 = require("../resources/index.js"); const { diff } = deep_diff_1.default; function generateChangeSet(origin, target) { const changes = []; const tMap = new Map(); (target.resources || []).forEach(r => tMap.set(`${r.resourceType}:${r.logicId}`, r)); const relationResourcesMap = new Map(); origin.resources.forEach(o => { const key = `${o.resourceType}:${o.logicId}`; const t = tMap.get(key); const { resourceSettingSchema, relationResources, } = index_js_1.allResourceSchemas[o.resourceType]; // 先處理關聯資源 relationResourcesMap.set(o.resourceType, relationResources); if (!t) { changes.push({ resourceType: o.resourceType, operation: 'create', logicId: o.logicId, description: o.description, diffs: [], payload: o.setting }); } else { const diffs = generateDiffs(t.setting, o.setting, resourceSettingSchema); if (diffs.length) { changes.push({ resourceType: o.resourceType, operation: 'update', logicId: o.logicId, description: o.description, diffs, payload: o.setting }); } tMap.delete(key); } }); // 剩下的 target 代表 delete for (const orphan of tMap.values()) { changes.push({ resourceType: orphan.resourceType, operation: 'delete', logicId: orphan.logicId, description: orphan.description, diffs: [], payload: null }); } // 處理關聯資源 const relationProcessedChanges = handleChangeByRelation(changes, relationResourcesMap); return { version: '1.0', scopePrefix: origin.logicIdPrefix || target.logicIdPrefix, changes: relationProcessedChanges, }; } function generateDiffs(oldSetting, newSetting, schema) { const result = []; const getDesc = (key) => { if (schema) { const shape = schema.shape; const def = shape[key]?._def?.description; return typeof def === 'string' ? def : key; } return key; }; // null → object;object → null;雙方都有 if (!oldSetting && newSetting && typeof newSetting === 'object') { Object.keys(newSetting).forEach(key => result.push({ key, desc: getDesc(key), old: null, new: newSetting[key] })); return result; } if (!newSetting && oldSetting && typeof oldSetting === 'object') { Object.keys(oldSetting).forEach(key => result.push({ key, desc: getDesc(key), old: oldSetting[key], new: null })); return result; } const diffs = diff(oldSetting, newSetting) || []; const seen = new Set(); for (const d of diffs) { if (d.path?.[0] && !seen.has(String(d.path[0]))) { const key = String(d.path[0]); seen.add(key); result.push({ key, desc: getDesc(key), old: oldSetting?.[key] ?? null, new: newSetting?.[key] ?? null }); } } return result; } // 根據關聯資源處理變更 function handleChangeByRelation(changes, relationResourcesMap) { const updatedChanges = [...changes]; const needRemoveRelation = new Map(); // 檢查關聯並標記需要移除的項目 for (const [currentResourceType, relations] of relationResourcesMap) { for (const rel of relations) { const targetResourceType = rel.resourceType; const targetRelations = relationResourcesMap.get(targetResourceType); if (!targetRelations) continue; const matchingRelationIndices = targetRelations .map((r, index) => (r.resourceType === currentResourceType ? index : -1)) .filter((index) => index !== -1); for (const index of matchingRelationIndices.sort((a, b) => b - a)) { const matchingRelation = targetRelations[index]; // 從 targetRelations 中移除 matchingRelation targetRelations.splice(index, 1); // 將移除的關聯添加到 needRemoveRelation const existingRelations = needRemoveRelation.get(targetResourceType) || []; needRemoveRelation.set(targetResourceType, [ ...existingRelations, matchingRelation, ]); } } } // 第二步:進行拓撲排序並檢測循環依賴 let sortedResourceTypes; try { sortedResourceTypes = sortResourceTypesWithCycleDetection(relationResourcesMap); } catch (error) { throw new Error(`Failed to sort resources: ${error.message}`); } // 第三步:根據 needRemoveRelation更新 changes for (const [resourceType, removeRelations] of needRemoveRelation) { const matchingChanges = updatedChanges.filter((c) => c.resourceType === resourceType); if (matchingChanges.length > 0) { for (const change of matchingChanges) { const updatedPayload = { ...change.payload }; for (const relation of removeRelations) { const fieldPath = relation.settingSchemaField; const relastionType = relation.relationType; (0, jsonpath_plus_1.JSONPath)({ path: fieldPath, json: updatedPayload, callback: (value, payloadType, { parent, }) => { const lastPath = fieldPath.split('.').pop(); if (lastPath !== undefined) { if (relastionType === 'single') { parent[lastPath] = null; } else if (relastionType === 'multi') { parent[lastPath] = []; } } } }); } change.payload = updatedPayload; } } } // 第四步:根據 sortedMap 更新 changes updatedChanges.sort((a, b) => { // Define operation order: create > update > delete const operationOrder = { create: 1, update: 2, delete: 3 }; const orderA = operationOrder[a.operation] || 4; const orderB = operationOrder[b.operation] || 4; if (orderA !== orderB) { return orderA - orderB; } const indexA = sortedResourceTypes.indexOf(a.resourceType); const indexB = sortedResourceTypes.indexOf(b.resourceType); return indexA - indexB; }); return updatedChanges; } // 使用拓撲排序並檢測循環依賴 function sortResourceTypesWithCycleDetection(relationResourcesMap) { const sorted = []; const visited = new Set(); const recursionStack = new Set(); function dfs(resourceType) { if (recursionStack.has(resourceType)) { throw new Error(`Circular dependency detected involving ${resourceType}`); } if (visited.has(resourceType)) { return; } recursionStack.add(resourceType); const relations = relationResourcesMap.get(resourceType) || []; for (const rel of relations) { const targetResourceType = rel.resourceType; if (relationResourcesMap.has(targetResourceType)) { dfs(targetResourceType); } } recursionStack.delete(resourceType); visited.add(resourceType); sorted.push(resourceType); } for (const resourceType of relationResourcesMap.keys()) { if (!visited.has(resourceType)) { dfs(resourceType); } } return sorted; }