openxiangda 1.0.7 → 1.0.8
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/package.json
CHANGED
|
@@ -1,6 +1,119 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { getApiBaseUrl } from "./load-config.mjs";
|
|
3
|
+
import { getApiBaseUrl, rootDir } from "./load-config.mjs";
|
|
4
|
+
|
|
5
|
+
const PROJECT_STATE_FILE = ".openxiangda/state.json";
|
|
6
|
+
|
|
7
|
+
function readJson(filePath, fallback) {
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
10
|
+
} catch {
|
|
11
|
+
return fallback;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getWorkspaceRoot(config) {
|
|
16
|
+
return config.workspaceRoot || rootDir;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getProjectStatePath(config) {
|
|
20
|
+
return path.join(getWorkspaceRoot(config), PROJECT_STATE_FILE);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function loadProjectState(config) {
|
|
24
|
+
return readJson(getProjectStatePath(config), {
|
|
25
|
+
version: 1,
|
|
26
|
+
profiles: {},
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function saveProjectState(config, state) {
|
|
31
|
+
const statePath = getProjectStatePath(config);
|
|
32
|
+
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
|
33
|
+
const normalized = {
|
|
34
|
+
version: 1,
|
|
35
|
+
...state,
|
|
36
|
+
profiles: state.profiles || {},
|
|
37
|
+
};
|
|
38
|
+
const tempPath = `${statePath}.${process.pid}.tmp`;
|
|
39
|
+
fs.writeFileSync(tempPath, `${JSON.stringify(normalized, null, 2)}\n`, "utf-8");
|
|
40
|
+
fs.renameSync(tempPath, statePath);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeBaseUrl(value) {
|
|
44
|
+
return String(value || "").replace(/\/+$/, "");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function ensureResourceBuckets(bound) {
|
|
48
|
+
bound.resources = bound.resources || {};
|
|
49
|
+
bound.resources.forms = bound.resources.forms || {};
|
|
50
|
+
bound.resources.pages = bound.resources.pages || {};
|
|
51
|
+
bound.resources.workflows = bound.resources.workflows || {};
|
|
52
|
+
bound.resources.automations = bound.resources.automations || {};
|
|
53
|
+
bound.resources.menus = bound.resources.menus || {};
|
|
54
|
+
bound.resources.roles = bound.resources.roles || {};
|
|
55
|
+
bound.resources.pagePermissionGroups = bound.resources.pagePermissionGroups || {};
|
|
56
|
+
bound.resources.formPermissionGroups = bound.resources.formPermissionGroups || {};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function resolveProjectProfileName(config, state) {
|
|
60
|
+
if (config.openXiangdaProfile) return config.openXiangdaProfile;
|
|
61
|
+
|
|
62
|
+
const profiles = Object.entries(state.profiles || {});
|
|
63
|
+
const platformUrl = normalizeBaseUrl(config.platformUrl);
|
|
64
|
+
const matched = profiles.find(([, bound]) => {
|
|
65
|
+
if (bound?.appType !== config.appType) return false;
|
|
66
|
+
const boundBaseUrl = normalizeBaseUrl(bound?.baseUrl);
|
|
67
|
+
return !platformUrl || !boundBaseUrl || platformUrl === boundBaseUrl;
|
|
68
|
+
});
|
|
69
|
+
return matched?.[0] || "";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function readWorkspaceFormBinding(config, formName) {
|
|
73
|
+
if (!isOpenXiangdaMode(config)) return null;
|
|
74
|
+
const state = loadProjectState(config);
|
|
75
|
+
const profileName = resolveProjectProfileName(config, state);
|
|
76
|
+
if (!profileName) return null;
|
|
77
|
+
const bound = state.profiles?.[profileName];
|
|
78
|
+
const form = bound?.resources?.forms?.[formName];
|
|
79
|
+
if (!form?.formUuid) return null;
|
|
80
|
+
return {
|
|
81
|
+
profileName,
|
|
82
|
+
bound,
|
|
83
|
+
form,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function saveWorkspaceFormBinding(config, formName, formUuid, extra = {}) {
|
|
88
|
+
if (!isOpenXiangdaMode(config) || !formUuid) return false;
|
|
89
|
+
|
|
90
|
+
const state = loadProjectState(config);
|
|
91
|
+
const profileName =
|
|
92
|
+
resolveProjectProfileName(config, state) ||
|
|
93
|
+
config.openXiangdaProfile ||
|
|
94
|
+
config.appType ||
|
|
95
|
+
"default";
|
|
96
|
+
|
|
97
|
+
state.profiles = state.profiles || {};
|
|
98
|
+
state.profiles[profileName] = {
|
|
99
|
+
...(state.profiles[profileName] || {}),
|
|
100
|
+
baseUrl: config.platformUrl,
|
|
101
|
+
appType: config.appType,
|
|
102
|
+
updatedAt: new Date().toISOString(),
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const bound = state.profiles[profileName];
|
|
106
|
+
ensureResourceBuckets(bound);
|
|
107
|
+
bound.resources.forms[formName] = {
|
|
108
|
+
...(bound.resources.forms[formName] || {}),
|
|
109
|
+
...extra,
|
|
110
|
+
formUuid,
|
|
111
|
+
updatedAt: new Date().toISOString(),
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
saveProjectState(config, state);
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
4
117
|
|
|
5
118
|
function readExportedStringConstant(filePath, exportName) {
|
|
6
119
|
if (!filePath || !fs.existsSync(filePath)) return "";
|
|
@@ -520,42 +633,50 @@ export async function ensureSchemaFormUuid({
|
|
|
520
633
|
}) {
|
|
521
634
|
const meta = readSchemaMeta(schemaPath);
|
|
522
635
|
const targetAppType = resolveTargetAppType(config, meta);
|
|
636
|
+
const workspaceBinding = readWorkspaceFormBinding(config, formName);
|
|
637
|
+
const effectiveMeta = workspaceBinding?.form?.formUuid
|
|
638
|
+
? {
|
|
639
|
+
...meta,
|
|
640
|
+
formUuid: workspaceBinding.form.formUuid,
|
|
641
|
+
appType: targetAppType,
|
|
642
|
+
}
|
|
643
|
+
: meta;
|
|
523
644
|
const staleOpenXiangdaBinding = isStaleOpenXiangdaBinding(
|
|
524
645
|
config,
|
|
525
|
-
|
|
646
|
+
effectiveMeta,
|
|
526
647
|
targetAppType,
|
|
527
648
|
);
|
|
528
649
|
|
|
529
|
-
if (
|
|
650
|
+
if (effectiveMeta.formUuid && !staleOpenXiangdaBinding) {
|
|
530
651
|
let created = false;
|
|
531
652
|
if (!dryRun) {
|
|
532
653
|
const token = accessToken || (await getOpenApiAccessToken(config));
|
|
533
654
|
const existingForm = await getOpenApiForm(config, token, {
|
|
534
655
|
appType: targetAppType,
|
|
535
|
-
formUuid:
|
|
656
|
+
formUuid: effectiveMeta.formUuid,
|
|
536
657
|
});
|
|
537
658
|
|
|
538
659
|
if (!existingForm) {
|
|
539
660
|
await createOpenApiForm(config, token, {
|
|
540
|
-
name:
|
|
661
|
+
name: effectiveMeta.title || formName,
|
|
541
662
|
appType: targetAppType,
|
|
542
|
-
formType:
|
|
543
|
-
relateUuid:
|
|
544
|
-
formUuid:
|
|
663
|
+
formType: effectiveMeta.formType || "receipt",
|
|
664
|
+
relateUuid: effectiveMeta.relateUuid,
|
|
665
|
+
formUuid: effectiveMeta.formUuid,
|
|
545
666
|
builderVersion: config.defaults?.formBuilderVersion || "2.0",
|
|
546
667
|
});
|
|
547
668
|
created = true;
|
|
548
669
|
console.log(
|
|
549
|
-
` ✓ ${formName}:
|
|
670
|
+
` ✓ ${formName}: 已按已有 formUuid 创建表单 ${effectiveMeta.formUuid}`,
|
|
550
671
|
);
|
|
551
672
|
}
|
|
552
673
|
|
|
553
674
|
if ((created || ensureMenu) && !isOpenXiangdaMode(config)) {
|
|
554
675
|
const menuResult = await ensureOpenApiMenuForForm(config, token, {
|
|
555
676
|
appType: targetAppType,
|
|
556
|
-
formUuid:
|
|
557
|
-
name:
|
|
558
|
-
formType:
|
|
677
|
+
formUuid: effectiveMeta.formUuid,
|
|
678
|
+
name: effectiveMeta.title || formName,
|
|
679
|
+
formType: effectiveMeta.formType || "receipt",
|
|
559
680
|
parentId: config.menu?.parentId,
|
|
560
681
|
icon: config.menu?.icon,
|
|
561
682
|
});
|
|
@@ -566,17 +687,21 @@ export async function ensureSchemaFormUuid({
|
|
|
566
687
|
);
|
|
567
688
|
}
|
|
568
689
|
}
|
|
690
|
+
|
|
691
|
+
saveWorkspaceFormBinding(config, formName, effectiveMeta.formUuid, {
|
|
692
|
+
title: effectiveMeta.title || formName,
|
|
693
|
+
});
|
|
569
694
|
}
|
|
570
695
|
|
|
571
696
|
return {
|
|
572
|
-
...
|
|
697
|
+
...effectiveMeta,
|
|
573
698
|
appType: targetAppType,
|
|
574
699
|
created,
|
|
575
700
|
dryRunCreated: false,
|
|
576
701
|
};
|
|
577
702
|
}
|
|
578
703
|
|
|
579
|
-
if (!meta.hasFormUuidProp) {
|
|
704
|
+
if (!meta.hasFormUuidProp && !isOpenXiangdaMode(config)) {
|
|
580
705
|
throw new Error(
|
|
581
706
|
`${formName}: schema.ts 中未找到 formUuid 字段,无法自动创建并回写`,
|
|
582
707
|
);
|
|
@@ -608,7 +733,13 @@ export async function ensureSchemaFormUuid({
|
|
|
608
733
|
|
|
609
734
|
const token = accessToken || (await getOpenApiAccessToken(config));
|
|
610
735
|
const createdForm = await createOpenApiForm(config, token, createOptions);
|
|
611
|
-
|
|
736
|
+
if (isOpenXiangdaMode(config)) {
|
|
737
|
+
saveWorkspaceFormBinding(config, formName, createdForm.formUuid, {
|
|
738
|
+
title: createOptions.name,
|
|
739
|
+
});
|
|
740
|
+
} else {
|
|
741
|
+
syncCreatedFormMeta(schemaPath, meta, targetAppType, createdForm.formUuid);
|
|
742
|
+
}
|
|
612
743
|
console.log(` ✓ ${formName}: 已创建表单 ${createdForm.formUuid}`);
|
|
613
744
|
|
|
614
745
|
let menuResult = { created: false, menu: null };
|
|
@@ -170,7 +170,77 @@ describe("form api provisioning helpers", () => {
|
|
|
170
170
|
);
|
|
171
171
|
});
|
|
172
172
|
|
|
173
|
-
it("uses
|
|
173
|
+
it("uses OpenXiangda workspace state bindings before schema literals", async () => {
|
|
174
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "form-api-"));
|
|
175
|
+
tempDirs.push(dir);
|
|
176
|
+
const schemaPath = path.join(dir, "schema.ts");
|
|
177
|
+
fs.mkdirSync(path.join(dir, ".openxiangda"), { recursive: true });
|
|
178
|
+
fs.writeFileSync(
|
|
179
|
+
path.join(dir, ".openxiangda", "state.json"),
|
|
180
|
+
`${JSON.stringify(
|
|
181
|
+
{
|
|
182
|
+
version: 1,
|
|
183
|
+
profiles: {
|
|
184
|
+
dev: {
|
|
185
|
+
baseUrl: "https://platform.example.com/service",
|
|
186
|
+
appType: "APP_TEST",
|
|
187
|
+
resources: {
|
|
188
|
+
forms: {
|
|
189
|
+
customer: {
|
|
190
|
+
formUuid: "FORM_STATE",
|
|
191
|
+
title: "客户表单",
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
null,
|
|
199
|
+
2,
|
|
200
|
+
)}\n`,
|
|
201
|
+
"utf-8",
|
|
202
|
+
);
|
|
203
|
+
fs.writeFileSync(
|
|
204
|
+
schemaPath,
|
|
205
|
+
[
|
|
206
|
+
'const FORM_UUID = "";',
|
|
207
|
+
'const APP_TYPE = process.env.OPENXIANGDA_APP_TYPE || process.env.APP_TYPE || "";',
|
|
208
|
+
'export default { formMeta: { formUuid: FORM_UUID, appType: APP_TYPE, title: "客户表单", formType: "receipt" } };',
|
|
209
|
+
"",
|
|
210
|
+
].join("\n"),
|
|
211
|
+
"utf-8",
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const fetchMock = vi.fn(async (url: string) => {
|
|
215
|
+
if (String(url).endsWith("/forms")) {
|
|
216
|
+
return Response.json({
|
|
217
|
+
code: 200,
|
|
218
|
+
data: [{ formUuid: "FORM_STATE", appType: "APP_TEST" }],
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
throw new Error(`unexpected request: ${url}`);
|
|
222
|
+
});
|
|
223
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
224
|
+
|
|
225
|
+
const result = await ensureSchemaFormUuid({
|
|
226
|
+
config: {
|
|
227
|
+
...config,
|
|
228
|
+
workspaceRoot: dir,
|
|
229
|
+
openXiangdaAccessToken: "token",
|
|
230
|
+
openXiangdaProfile: "dev",
|
|
231
|
+
},
|
|
232
|
+
schemaPath,
|
|
233
|
+
formName: "customer",
|
|
234
|
+
accessToken: "token",
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
expect(result.formUuid).toBe("FORM_STATE");
|
|
238
|
+
expect(result.appType).toBe("APP_TEST");
|
|
239
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
240
|
+
expect(fs.readFileSync(schemaPath, "utf-8")).toContain('const FORM_UUID = "";');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("uses the bound OpenXiangda appType and stores stale imported form ids in workspace state", async () => {
|
|
174
244
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "form-api-"));
|
|
175
245
|
tempDirs.push(dir);
|
|
176
246
|
const schemaPath = path.join(dir, "schema.ts");
|
|
@@ -223,7 +293,9 @@ describe("form api provisioning helpers", () => {
|
|
|
223
293
|
const result = await ensureSchemaFormUuid({
|
|
224
294
|
config: {
|
|
225
295
|
...config,
|
|
296
|
+
workspaceRoot: dir,
|
|
226
297
|
openXiangdaAccessToken: "token",
|
|
298
|
+
openXiangdaProfile: "dev",
|
|
227
299
|
},
|
|
228
300
|
schemaPath,
|
|
229
301
|
formName: "customer",
|
|
@@ -235,10 +307,60 @@ describe("form api provisioning helpers", () => {
|
|
|
235
307
|
expect(result.appType).toBe("APP_TEST");
|
|
236
308
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
237
309
|
expect(fs.readFileSync(metaPath, "utf-8")).toContain(
|
|
238
|
-
'CUSTOMER_INFO_FORM_UUID = "
|
|
310
|
+
'CUSTOMER_INFO_FORM_UUID = "FORM_OLD"',
|
|
239
311
|
);
|
|
240
312
|
expect(fs.readFileSync(metaPath, "utf-8")).toContain(
|
|
241
|
-
'CUSTOMER_INFO_APP_TYPE = "
|
|
313
|
+
'CUSTOMER_INFO_APP_TYPE = "APP_OLD"',
|
|
314
|
+
);
|
|
315
|
+
expect(
|
|
316
|
+
JSON.parse(
|
|
317
|
+
fs.readFileSync(path.join(dir, ".openxiangda", "state.json"), "utf-8"),
|
|
318
|
+
).profiles.dev.resources.forms.customer.formUuid,
|
|
319
|
+
).toBe("FORM_NEW");
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("creates OpenXiangda forms without requiring schema formUuid writeback", async () => {
|
|
323
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "form-api-"));
|
|
324
|
+
tempDirs.push(dir);
|
|
325
|
+
const schemaPath = path.join(dir, "schema.ts");
|
|
326
|
+
fs.writeFileSync(
|
|
327
|
+
schemaPath,
|
|
328
|
+
`export default { formMeta: { appType: "", title: "客户表单", formType: "receipt" } };\n`,
|
|
329
|
+
"utf-8",
|
|
242
330
|
);
|
|
331
|
+
|
|
332
|
+
const fetchMock = vi.fn(async (url: string, init?: RequestInit) => {
|
|
333
|
+
expect(String(url)).toBe(
|
|
334
|
+
"https://platform.example.com/service/openxiangda-api/v1/apps/APP_TEST/forms",
|
|
335
|
+
);
|
|
336
|
+
const body = JSON.parse(String(init?.body || "{}"));
|
|
337
|
+
expect(body.formUuid).toBeUndefined();
|
|
338
|
+
return Response.json({
|
|
339
|
+
code: 200,
|
|
340
|
+
data: { form: { formUuid: "FORM_CREATED" } },
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
344
|
+
|
|
345
|
+
const result = await ensureSchemaFormUuid({
|
|
346
|
+
config: {
|
|
347
|
+
...config,
|
|
348
|
+
workspaceRoot: dir,
|
|
349
|
+
openXiangdaAccessToken: "token",
|
|
350
|
+
openXiangdaProfile: "dev",
|
|
351
|
+
},
|
|
352
|
+
schemaPath,
|
|
353
|
+
formName: "customer",
|
|
354
|
+
accessToken: "token",
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
expect(result.created).toBe(true);
|
|
358
|
+
expect(result.formUuid).toBe("FORM_CREATED");
|
|
359
|
+
expect(fs.readFileSync(schemaPath, "utf-8")).not.toContain("FORM_CREATED");
|
|
360
|
+
expect(
|
|
361
|
+
JSON.parse(
|
|
362
|
+
fs.readFileSync(path.join(dir, ".openxiangda", "state.json"), "utf-8"),
|
|
363
|
+
).profiles.dev.resources.forms.customer.formUuid,
|
|
364
|
+
).toBe("FORM_CREATED");
|
|
243
365
|
});
|
|
244
366
|
});
|