@stalfh233/omc-fake-platform 0.4.74 → 0.4.76

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/field.js CHANGED
@@ -6,6 +6,12 @@ export const fieldRoute = (ctx, input) => {
6
6
  const body = (input.body ?? {});
7
7
  switch (input.path) {
8
8
  case "/api/api/app/bizproperty/list": {
9
+ // Live 2026-10-23 (issue §1.4): on some versions a CHILD-table schema code
10
+ // answers 601003 业务模型不存在 on this face while bizdatarule/list answers
11
+ // rows for it. That is a version-specific asymmetry, so the fake exposes it
12
+ // as an explicit lever (state.fieldListUnavailable) instead of guessing it.
13
+ if (ctx.state.fieldListUnavailable?.has(String(q.schemaCode ?? "")))
14
+ return businessRefusal(601003, "业务模型不存在");
9
15
  const rows = [...ctx.state.fields.entries()]
10
16
  .filter(([key]) => key.startsWith(`${q.schemaCode}#`))
11
17
  .map(([, field]) => ({
package/dist/form.js CHANGED
@@ -30,6 +30,7 @@ export const formRoute = (ctx, input) => {
30
30
  publishedAttributesJson: sheet.publishedAttributesJson ?? null,
31
31
  publishedActionsJson: sheet.publishedActionsJson ?? null,
32
32
  publishedHtmlJson: sheet.publishedHtmlJson ?? null,
33
+ options: sheet.options ?? null,
33
34
  binding: { fieldCodes: sheet.fieldCodes },
34
35
  };
35
36
  }
@@ -78,6 +79,7 @@ export const formRoute = (ctx, input) => {
78
79
  // customScript element when present). Default to "[]" on first
79
80
  // draft so onlinejs.compose can parse it as an array.
80
81
  draftHtmlJson: body.draftHtmlJson ?? existing.draftHtmlJson ?? "[]",
82
+ options: typeof body.options === "string" ? body.options : existing.options ?? null,
81
83
  });
82
84
  return null;
83
85
  }
@@ -91,6 +93,68 @@ export const formRoute = (ctx, input) => {
91
93
  sheet.publishedAttributesJson = sheet.draftAttributesJson ?? null;
92
94
  sheet.publishedActionsJson = body.publishedActionsJson ?? "[]";
93
95
  sheet.publishedHtmlJson = sheet.draftHtmlJson ?? "[]";
96
+ // The platform persists the whole BizFormModel on publish, options
97
+ // included; keep the authoring column so the readback can show it.
98
+ if (typeof body.options === "string")
99
+ sheet.options = body.options;
100
+ // ModelManagementFacadeImpl.publishBizForm:1022 — publish reads
101
+ // options.customButtonJson and re-adds the FORM-trigger button rows.
102
+ // A malformed value answers 50000 (fastjson "syntax error, position at
103
+ // 0, name customButtonJson") exactly like the platform.
104
+ const rawOptions = typeof body.options === "string" ? body.options : sheet.options;
105
+ if (typeof rawOptions === "string" && rawOptions !== "") {
106
+ let optionsRecord;
107
+ try {
108
+ const parsed = JSON.parse(rawOptions);
109
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
110
+ optionsRecord = parsed;
111
+ }
112
+ catch {
113
+ return businessRefusal(50000, "options 不是合法 JSON");
114
+ }
115
+ const rawButtons = optionsRecord?.customButtonJson;
116
+ if (rawButtons !== undefined && rawButtons !== null && rawButtons !== "") {
117
+ if (typeof rawButtons !== "string")
118
+ return businessRefusal(50000, "syntax error, position at 0, name customButtonJson");
119
+ let list;
120
+ try {
121
+ list = JSON.parse(rawButtons);
122
+ }
123
+ catch {
124
+ return businessRefusal(50000, "syntax error, position at 0, name customButtonJson");
125
+ }
126
+ if (!Array.isArray(list))
127
+ return businessRefusal(50000, "syntax error, position at 0, name customButtonJson");
128
+ for (const [key, button] of [...ctx.state.buttons]) {
129
+ if (button.schemaCode === sheet.sheetCode && button.triggerType === "FORM" && button.triggerCode === sheet.sheetCode)
130
+ ctx.state.buttons.delete(key);
131
+ }
132
+ list.forEach((entry, index) => {
133
+ const item = (entry ?? {});
134
+ const code = String(item.code ?? "");
135
+ if (code === "")
136
+ return;
137
+ ctx.state.buttons.set(`${sheet.sheetCode}#FORM#${sheet.sheetCode}#${code}`, {
138
+ id: `btn-${sheet.sheetCode}-${code}`,
139
+ schemaCode: sheet.sheetCode,
140
+ code,
141
+ name: String(item.name ?? code),
142
+ name_i18n: typeof item.name_i18n === "string" ? item.name_i18n : null,
143
+ triggerType: "FORM",
144
+ triggerCode: sheet.sheetCode,
145
+ useLocation: typeof item.useLocation === "string" ? item.useLocation : "FORM",
146
+ bindAction: typeof item.bindAction === "string" ? item.bindAction : undefined,
147
+ operateType: typeof item.operateType === "string" ? item.operateType : undefined,
148
+ targetCode: typeof item.targetCode === "string" ? item.targetCode : null,
149
+ targetObjCode: typeof item.targetObjCode === "string" ? item.targetObjCode : null,
150
+ showPermCode: typeof item.showPermCode === "string" ? item.showPermCode : null,
151
+ showPermType: typeof item.showPermType === "string" ? item.showPermType : null,
152
+ actionConfig: typeof item.actionConfig === "string" ? item.actionConfig : JSON.stringify(item.actionConfig ?? {}),
153
+ sortKey: typeof item.sortKey === "number" ? item.sortKey : index,
154
+ });
155
+ });
156
+ }
157
+ }
94
158
  }
95
159
  return null;
96
160
  }
@@ -67,9 +67,72 @@ function selfCheckProblems(state, model) {
67
67
  }
68
68
  return problems;
69
69
  }
70
+ const RUNTIME_EXECUTE_PREFIX = "/api/api/launcher/domain_service_runtime/";
71
+ /**
72
+ * Manual custom-trigger rule execution — the portal button's face (live
73
+ * 2026-10-23): POST /api/api/launcher/domain_service_runtime/<schema>/<rule>
74
+ * {schemaCode, ids, ruleCode, consolidation}. The response's `data` is the
75
+ * PRE-execution snapshot (the frontend sends reacquireDataFromDb:false), so the
76
+ * fake applies the rule's UPDATE nodes but returns the rows as they were.
77
+ */
78
+ function runtimeExecuteRule(ctx, schemaCode, ruleCode, ids) {
79
+ const rule = [...ctx.state.rules.values()].find((entry) => entry.schemaCode === schemaCode && entry.code === ruleCode);
80
+ if (!rule)
81
+ return businessRefusal(302024, "业务规则不存在");
82
+ let nodes = [];
83
+ try {
84
+ const parsed = typeof rule.node === "string" ? JSON.parse(rule.node) : rule.node;
85
+ if (Array.isArray(parsed))
86
+ nodes = parsed;
87
+ }
88
+ catch {
89
+ nodes = [];
90
+ }
91
+ const rows = ids
92
+ .map((id) => ctx.state.dataRows.get(id))
93
+ .filter((row) => row !== undefined);
94
+ const before = rows.map((row) => structuredClone(row));
95
+ for (const node of nodes) {
96
+ if (String(node.nodeType ?? "").toUpperCase() !== "UPDATE")
97
+ continue;
98
+ const actions = Array.isArray(node.dataActions) ? node.dataActions : [];
99
+ const groups = (node.filterCondition?.mainConditions ?? []);
100
+ const matches = (row) => {
101
+ if (groups.length === 0)
102
+ return true;
103
+ return groups.some((group) => (Array.isArray(group) ? group : []).every((raw) => {
104
+ const condition = (raw ?? {});
105
+ const field = String(condition.targetSchemaDataItem ?? "");
106
+ if (condition.ruleValueType !== "FIXED")
107
+ return true;
108
+ return String(row[field] ?? "") === String(condition.value ?? "");
109
+ }));
110
+ };
111
+ for (const row of rows) {
112
+ if (!matches(row))
113
+ continue;
114
+ for (const action of actions) {
115
+ if (action.ruleValueType !== "FIXED")
116
+ continue;
117
+ row[String(action.targetDataItem ?? "")] = action.value;
118
+ }
119
+ }
120
+ }
121
+ // The fake's route return value IS the envelope's `data` (errcode is added by
122
+ // the transport), so hand back the pre-execution rows exactly like the live
123
+ // `{"errcode":0,"errmsg":"Execution Completed","data":[…]}` unwraps to.
124
+ return before;
125
+ }
70
126
  export const ruleLifecycleRoute = (ctx, input) => {
71
127
  const q = input.query ?? {};
72
128
  const body = (input.body ?? {});
129
+ if (input.path.startsWith(RUNTIME_EXECUTE_PREFIX)) {
130
+ const [schemaCode, ruleCode] = input.path.slice(RUNTIME_EXECUTE_PREFIX.length).split("/").map((part) => decodeURIComponent(part));
131
+ const ids = Array.isArray(body.ids) ? body.ids.map(String) : [];
132
+ if (!schemaCode || !ruleCode)
133
+ return businessRefusal(10011, "业务模型不存在");
134
+ return runtimeExecuteRule(ctx, String(body.schemaCode ?? schemaCode), String(body.ruleCode ?? ruleCode), ids);
135
+ }
73
136
  switch (input.path) {
74
137
  case "/api/api/app/businessrule/node_run_map":
75
138
  return selfCheckProblems(ctx.state, body);
@@ -170,6 +170,60 @@ export const runtimeDataRoute = (ctx, input) => {
170
170
  },
171
171
  };
172
172
  }
173
+ case "/api/api/system/getCustomButtons": {
174
+ // SystemAdminController.getCustomButtons: SchemaCustomButtonModel with one
175
+ // TriggerInfo per default form; only triggerType=FORM buttons ride here.
176
+ const schemaCode = String(q.schemaCode ?? "");
177
+ const forms = [...ctx.state.sheets.values()].filter((sheet) => sheet.sheetCode === schemaCode);
178
+ return {
179
+ formCustomButtons: forms.map((sheet) => ({
180
+ triggerCode: sheet.sheetCode,
181
+ triggerName: sheet.sheetCode,
182
+ hasChildTable: false,
183
+ buttonModels: [...ctx.state.buttons.values()].filter((button) => button.triggerType === "FORM" && button.triggerCode === sheet.sheetCode && button.schemaCode === schemaCode),
184
+ })),
185
+ };
186
+ }
187
+ case "/api/api/runtime/form/executeCustomButton": {
188
+ // FormRuntimeController.executeCustomButton (live 2026-10-23): the portal
189
+ // posts the whole bizObject for a FORM button (not ids), and the payload
190
+ // carries the button row itself. Apply the bound rule's UPDATE nodes.
191
+ const model = (body.bizButtonModel ?? {});
192
+ const list = Array.isArray(body.bizObjectList) ? body.bizObjectList : [];
193
+ const schemaCode = String(model.schemaCode ?? "");
194
+ const ruleCode = String(model.targetCode ?? "");
195
+ const rule = [...ctx.state.rules.values()].find((entry) => entry.schemaCode === schemaCode && entry.code === ruleCode);
196
+ if (!rule)
197
+ return businessRefusal(302024, "业务规则不存在");
198
+ let nodes = [];
199
+ try {
200
+ const parsed = typeof rule.node === "string" ? JSON.parse(rule.node) : rule.node;
201
+ if (Array.isArray(parsed))
202
+ nodes = parsed;
203
+ }
204
+ catch {
205
+ nodes = [];
206
+ }
207
+ for (const entry of list) {
208
+ const data = (entry?.data ?? {});
209
+ const id = String(data.id ?? entry?.id ?? "");
210
+ const row = id !== "" ? ctx.state.dataRows.get(id) : undefined;
211
+ for (const node of nodes) {
212
+ if (String(node.nodeType ?? "").toUpperCase() !== "UPDATE")
213
+ continue;
214
+ const actions = Array.isArray(node.dataActions) ? node.dataActions : [];
215
+ for (const action of actions) {
216
+ if (action.ruleValueType !== "FIXED")
217
+ continue;
218
+ if (row)
219
+ row[String(action.targetDataItem ?? "")] = action.value;
220
+ else
221
+ data[String(action.targetDataItem ?? "")] = action.value;
222
+ }
223
+ }
224
+ }
225
+ return { errcode: 0, errmsg: "Execution Completed" };
226
+ }
173
227
  case "/api/api/runtime/form/delete": {
174
228
  // Ticket 22 source evidence (FormRuntimeController:1233): @RequestParam
175
229
  // schemaCode + objectId; validateNotEmpty(schemaCode) -> PortalException
package/dist/state.d.ts CHANGED
@@ -70,8 +70,31 @@ export interface FakeSheet {
70
70
  publishedAttributesJson?: string | null;
71
71
  publishedActionsJson?: string | null;
72
72
  publishedHtmlJson?: string | null;
73
+ /** `options` (BizForm.options): form-level JSON carrying customButtonJson,
74
+ * presetButtonManage, sheetPresetButtonManage. Live column, string value. */
75
+ options?: string | null;
73
76
  fieldCodes: string[];
74
77
  }
78
+ /** h_biz_button row (BizButtonModel). triggerType=QUERY for list buttons,
79
+ * FORM for form toolbar buttons (published from bizsheet options). */
80
+ export interface FakeButton {
81
+ id: string;
82
+ schemaCode: string;
83
+ code: string;
84
+ name: string;
85
+ name_i18n?: string | null;
86
+ triggerType: "QUERY" | "FORM";
87
+ triggerCode: string;
88
+ useLocation?: string;
89
+ bindAction?: string;
90
+ operateType?: string;
91
+ targetCode?: string | null;
92
+ targetObjCode?: string | null;
93
+ showPermCode?: string | null;
94
+ showPermType?: string | null;
95
+ actionConfig?: string;
96
+ sortKey?: number;
97
+ }
75
98
  export interface FakeRule {
76
99
  id: string;
77
100
  schemaCode: string;
@@ -265,6 +288,9 @@ export interface FakeState {
265
288
  }>;
266
289
  sheets: Map<string, FakeSheet>;
267
290
  rules: Map<string, FakeRule>;
291
+ buttons: Map<string, FakeButton>;
292
+ /** Schema codes for which bizproperty/list answers 601003 (see field.ts). */
293
+ fieldListUnavailable: Set<string>;
268
294
  /**
269
295
  * Ticket 109 platform fidelity: every businessrule create/update mints a NEW
270
296
  * record id — superseded rows stay readable via businessrule/get?id=
package/dist/state.js CHANGED
@@ -33,6 +33,8 @@ export function createFakeState(options = {}) {
33
33
  dictionaryClasses: new Map(),
34
34
  sheets: new Map(),
35
35
  rules: new Map(),
36
+ buttons: new Map(),
37
+ fieldListUnavailable: new Set(),
36
38
  ruleHistory: [],
37
39
  ruleSeq: 0,
38
40
  dataRules: new Map(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stalfh233/omc-fake-platform",
3
- "version": "0.4.74",
3
+ "version": "0.4.76",
4
4
  "description": "OMC 的内存假 CloudPivot 平台(wire 接缝的第二个适配器)+ HTTP 假服务器——@stalfh233/omc-cli 的测试与 bench 基准基座",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -18,7 +18,7 @@
18
18
  "build": "tsc --project tsconfig.build.json"
19
19
  },
20
20
  "dependencies": {
21
- "omc-core": "0.4.74"
21
+ "omc-core": "0.4.76"
22
22
  },
23
23
  "engines": {
24
24
  "node": ">=22.13"