@wishcbg/bobcode
Version:
基於店家同步功能的需求,讓店家能夠在不同的店家之間同步各項功能設定,並且能夠檢視各項功能的異動情況。 規劃出一套店家設定即程式碼的機制,讓店家能夠以 config file (.sac) 來定義各項功能設定,並且能夠還原店家各項功能設定。 暫且稱這個機制為「Settings as Code」「店家設定即程式碼」。
211 lines (210 loc) • 8.47 kB
JavaScript
"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;
}