kintone-migrator 0.1.0
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/LICENSE +21 -0
- package/README.md +120 -0
- package/dist/index.js +1656 -0
- package/package.json +65 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1656 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { cli, define } from "gunshi";
|
|
4
|
+
import * as p from "@clack/prompts";
|
|
5
|
+
import pc from "picocolors";
|
|
6
|
+
import { KintoneRestAPIClient, KintoneRestAPIError } from "@kintone/rest-api-client";
|
|
7
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
8
|
+
import { dirname } from "node:path";
|
|
9
|
+
import { parse, stringify } from "yaml";
|
|
10
|
+
import * as v from "valibot";
|
|
11
|
+
|
|
12
|
+
//#region src/lib/error.ts
|
|
13
|
+
var AnyError = class extends Error {
|
|
14
|
+
constructor(message, cause) {
|
|
15
|
+
super(message, { cause });
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/core/application/error.ts
|
|
21
|
+
var ApplicationError = class extends AnyError {
|
|
22
|
+
name = "ApplicationError";
|
|
23
|
+
constructor(code, message, cause) {
|
|
24
|
+
super(message, cause);
|
|
25
|
+
this.code = code;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const ConflictErrorCode = { Conflict: "CONFLICT" };
|
|
29
|
+
var ConflictError = class extends ApplicationError {
|
|
30
|
+
name = "ConflictError";
|
|
31
|
+
constructor(code, message, cause) {
|
|
32
|
+
super(code, message, cause);
|
|
33
|
+
this.code = code;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const ValidationErrorCode = { InvalidInput: "INVALID_INPUT" };
|
|
37
|
+
var ValidationError = class extends ApplicationError {
|
|
38
|
+
name = "ValidationError";
|
|
39
|
+
constructor(code, message, cause) {
|
|
40
|
+
super(code, message, cause);
|
|
41
|
+
this.code = code;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
function isValidationError(error) {
|
|
45
|
+
return error instanceof ValidationError;
|
|
46
|
+
}
|
|
47
|
+
const SystemErrorCode = {
|
|
48
|
+
InternalServerError: "INTERNAL_SERVER_ERROR",
|
|
49
|
+
DatabaseError: "DATABASE_ERROR",
|
|
50
|
+
NetworkError: "NETWORK_ERROR",
|
|
51
|
+
StorageError: "STORAGE_ERROR",
|
|
52
|
+
DocumentGenerationError: "DOCUMENT_GENERATION_ERROR",
|
|
53
|
+
ExternalApiError: "EXTERNAL_API_ERROR"
|
|
54
|
+
};
|
|
55
|
+
var SystemError = class extends ApplicationError {
|
|
56
|
+
name = "SystemError";
|
|
57
|
+
constructor(code, message, cause) {
|
|
58
|
+
super(code, message, cause);
|
|
59
|
+
this.code = code;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
function isSystemError(error) {
|
|
63
|
+
return error instanceof SystemError;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/core/domain/formSchema/errorCode.ts
|
|
68
|
+
const FormSchemaErrorCode = {
|
|
69
|
+
EmptyFieldCode: "EMPTY_FIELD_CODE",
|
|
70
|
+
EmptySchemaText: "EMPTY_SCHEMA_TEXT",
|
|
71
|
+
InvalidSchemaJson: "INVALID_SCHEMA_JSON",
|
|
72
|
+
InvalidSchemaStructure: "INVALID_SCHEMA_STRUCTURE",
|
|
73
|
+
DuplicateFieldCode: "DUPLICATE_FIELD_CODE",
|
|
74
|
+
InvalidFieldType: "INVALID_FIELD_TYPE",
|
|
75
|
+
InvalidLayoutStructure: "INVALID_LAYOUT_STRUCTURE",
|
|
76
|
+
InvalidDecorationElement: "INVALID_DECORATION_ELEMENT"
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
//#endregion
|
|
80
|
+
//#region src/core/domain/error.ts
|
|
81
|
+
const BusinessRuleErrorCode = { ...FormSchemaErrorCode };
|
|
82
|
+
/**
|
|
83
|
+
* Domain Layer - Business Rule Error
|
|
84
|
+
*
|
|
85
|
+
* Represents a violation of business rules in the domain layer.
|
|
86
|
+
* This error is thrown when domain logic determines that an operation cannot proceed.
|
|
87
|
+
*/
|
|
88
|
+
var BusinessRuleError = class extends AnyError {
|
|
89
|
+
name = "BusinessRuleError";
|
|
90
|
+
constructor(code, message, cause) {
|
|
91
|
+
super(message, cause);
|
|
92
|
+
this.code = code;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
function isBusinessRuleError(error) {
|
|
96
|
+
return error instanceof BusinessRuleError;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/core/adapters/kintone/appDeployer.ts
|
|
101
|
+
const DEFAULT_POLL_INTERVAL_MS = 1e3;
|
|
102
|
+
const DEFAULT_MAX_RETRIES = 180;
|
|
103
|
+
var KintoneAppDeployer = class {
|
|
104
|
+
pollIntervalMs;
|
|
105
|
+
maxRetries;
|
|
106
|
+
constructor(client, appId, config) {
|
|
107
|
+
this.client = client;
|
|
108
|
+
this.appId = appId;
|
|
109
|
+
this.pollIntervalMs = config?.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
110
|
+
this.maxRetries = config?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
111
|
+
}
|
|
112
|
+
async deploy() {
|
|
113
|
+
try {
|
|
114
|
+
await this.client.app.deployApp({ apps: [{ app: this.appId }] });
|
|
115
|
+
await this.waitForDeployment();
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if (isBusinessRuleError(error)) throw error;
|
|
118
|
+
if (error instanceof SystemError) throw error;
|
|
119
|
+
throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to deploy app", error);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async waitForDeployment() {
|
|
123
|
+
for (let i = 0; i < this.maxRetries; i++) {
|
|
124
|
+
await this.sleep(this.pollIntervalMs);
|
|
125
|
+
const { apps } = await this.client.app.getDeployStatus({ apps: [this.appId] });
|
|
126
|
+
const status = apps[0]?.status;
|
|
127
|
+
switch (status) {
|
|
128
|
+
case "SUCCESS": return;
|
|
129
|
+
case "FAIL": throw new SystemError(SystemErrorCode.ExternalApiError, "App deployment failed");
|
|
130
|
+
case "CANCEL": throw new SystemError(SystemErrorCode.ExternalApiError, "App deployment was cancelled");
|
|
131
|
+
case "PROCESSING": continue;
|
|
132
|
+
case void 0: throw new SystemError(SystemErrorCode.ExternalApiError, "Deploy status unavailable");
|
|
133
|
+
default: throw new SystemError(SystemErrorCode.ExternalApiError, `Unexpected deploy status: ${status}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
throw new SystemError(SystemErrorCode.ExternalApiError, "App deployment timed out");
|
|
137
|
+
}
|
|
138
|
+
sleep(ms) {
|
|
139
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
//#endregion
|
|
144
|
+
//#region src/core/domain/formSchema/valueObject.ts
|
|
145
|
+
const FieldCode = { create: (code) => {
|
|
146
|
+
if (code.length === 0) throw new BusinessRuleError(FormSchemaErrorCode.EmptyFieldCode, "Field code cannot be empty");
|
|
147
|
+
return code;
|
|
148
|
+
} };
|
|
149
|
+
|
|
150
|
+
//#endregion
|
|
151
|
+
//#region src/core/adapters/kintone/formConfigurator.ts
|
|
152
|
+
const KNOWN_FIELD_TYPES = new Set([
|
|
153
|
+
"SINGLE_LINE_TEXT",
|
|
154
|
+
"MULTI_LINE_TEXT",
|
|
155
|
+
"RICH_TEXT",
|
|
156
|
+
"NUMBER",
|
|
157
|
+
"CALC",
|
|
158
|
+
"CHECK_BOX",
|
|
159
|
+
"RADIO_BUTTON",
|
|
160
|
+
"MULTI_SELECT",
|
|
161
|
+
"DROP_DOWN",
|
|
162
|
+
"DATE",
|
|
163
|
+
"TIME",
|
|
164
|
+
"DATETIME",
|
|
165
|
+
"LINK",
|
|
166
|
+
"USER_SELECT",
|
|
167
|
+
"ORGANIZATION_SELECT",
|
|
168
|
+
"GROUP_SELECT",
|
|
169
|
+
"FILE",
|
|
170
|
+
"GROUP",
|
|
171
|
+
"SUBTABLE",
|
|
172
|
+
"REFERENCE_TABLE"
|
|
173
|
+
]);
|
|
174
|
+
const SYSTEM_FIELD_TYPES$1 = new Set([
|
|
175
|
+
"RECORD_NUMBER",
|
|
176
|
+
"CREATOR",
|
|
177
|
+
"CREATED_TIME",
|
|
178
|
+
"MODIFIER",
|
|
179
|
+
"UPDATED_TIME",
|
|
180
|
+
"CATEGORY",
|
|
181
|
+
"STATUS",
|
|
182
|
+
"STATUS_ASSIGNEE"
|
|
183
|
+
]);
|
|
184
|
+
const DECORATION_TYPES$1 = new Set([
|
|
185
|
+
"LABEL",
|
|
186
|
+
"SPACER",
|
|
187
|
+
"HR"
|
|
188
|
+
]);
|
|
189
|
+
const KINTONE_REVISION_CONFLICT_CODE = "GAIA_CO02";
|
|
190
|
+
function isRevisionConflict(error) {
|
|
191
|
+
return error instanceof KintoneRestAPIError && error.code === KINTONE_REVISION_CONFLICT_CODE;
|
|
192
|
+
}
|
|
193
|
+
function fromKintoneProperty(prop) {
|
|
194
|
+
const { type, code, label, noLabel, ...rest } = prop;
|
|
195
|
+
const base = {
|
|
196
|
+
code: FieldCode.create(code),
|
|
197
|
+
label,
|
|
198
|
+
...noLabel !== void 0 ? { noLabel } : {}
|
|
199
|
+
};
|
|
200
|
+
if (type === "SUBTABLE" && "fields" in rest) {
|
|
201
|
+
const kintoneSubFields = rest.fields;
|
|
202
|
+
const subFields = /* @__PURE__ */ new Map();
|
|
203
|
+
for (const [subCode, subProp] of Object.entries(kintoneSubFields)) subFields.set(FieldCode.create(subCode), fromKintoneProperty(subProp));
|
|
204
|
+
return {
|
|
205
|
+
...base,
|
|
206
|
+
type: "SUBTABLE",
|
|
207
|
+
properties: { fields: subFields }
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
if (type === "REFERENCE_TABLE" && "referenceTable" in rest) {
|
|
211
|
+
const rt = rest.referenceTable;
|
|
212
|
+
const condition = rt.condition;
|
|
213
|
+
const displayFields = rt.displayFields.map((f) => FieldCode.create(f));
|
|
214
|
+
return {
|
|
215
|
+
...base,
|
|
216
|
+
type: "REFERENCE_TABLE",
|
|
217
|
+
properties: { referenceTable: {
|
|
218
|
+
relatedApp: rt.relatedApp,
|
|
219
|
+
condition: {
|
|
220
|
+
field: FieldCode.create(condition.field),
|
|
221
|
+
relatedField: FieldCode.create(condition.relatedField)
|
|
222
|
+
},
|
|
223
|
+
...rt.filterCond !== void 0 ? { filterCond: rt.filterCond } : {},
|
|
224
|
+
displayFields,
|
|
225
|
+
...rt.sort !== void 0 ? { sort: rt.sort } : {},
|
|
226
|
+
...rt.size !== void 0 ? { size: rt.size } : {}
|
|
227
|
+
} }
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
if (!KNOWN_FIELD_TYPES.has(type)) throw new SystemError(SystemErrorCode.ExternalApiError, `Unknown field type: ${type}`);
|
|
231
|
+
return {
|
|
232
|
+
...base,
|
|
233
|
+
type,
|
|
234
|
+
properties: rest
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function toKintoneProperty(field) {
|
|
238
|
+
const base = {
|
|
239
|
+
type: field.type,
|
|
240
|
+
code: field.code,
|
|
241
|
+
label: field.label,
|
|
242
|
+
...field.noLabel !== void 0 ? { noLabel: field.noLabel } : {}
|
|
243
|
+
};
|
|
244
|
+
if (field.type === "SUBTABLE") {
|
|
245
|
+
const subFields = {};
|
|
246
|
+
for (const [code, subField] of field.properties.fields) subFields[code] = toKintoneProperty(subField);
|
|
247
|
+
return {
|
|
248
|
+
...base,
|
|
249
|
+
fields: subFields
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
if (field.type === "REFERENCE_TABLE") {
|
|
253
|
+
const ref = field.properties.referenceTable;
|
|
254
|
+
return {
|
|
255
|
+
...base,
|
|
256
|
+
referenceTable: {
|
|
257
|
+
relatedApp: ref.relatedApp,
|
|
258
|
+
condition: {
|
|
259
|
+
field: ref.condition.field,
|
|
260
|
+
relatedField: ref.condition.relatedField
|
|
261
|
+
},
|
|
262
|
+
...ref.filterCond !== void 0 ? { filterCond: ref.filterCond } : {},
|
|
263
|
+
displayFields: ref.displayFields.map((f) => f),
|
|
264
|
+
...ref.sort !== void 0 ? { sort: ref.sort } : {},
|
|
265
|
+
...ref.size !== void 0 ? { size: ref.size } : {}
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
...base,
|
|
271
|
+
...field.properties
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function parseElementSize(raw) {
|
|
275
|
+
if (!raw) return void 0;
|
|
276
|
+
return {
|
|
277
|
+
...raw.width !== void 0 ? { width: raw.width } : {},
|
|
278
|
+
...raw.height !== void 0 ? { height: raw.height } : {},
|
|
279
|
+
...raw.innerHeight !== void 0 ? { innerHeight: raw.innerHeight } : {}
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
function fromKintoneLayoutElement(raw) {
|
|
283
|
+
const type = raw.type;
|
|
284
|
+
if (DECORATION_TYPES$1.has(type)) {
|
|
285
|
+
const size$1 = parseElementSize(raw.size) ?? {};
|
|
286
|
+
const elementId = String(raw.elementId ?? "");
|
|
287
|
+
switch (type) {
|
|
288
|
+
case "LABEL": return {
|
|
289
|
+
type: "LABEL",
|
|
290
|
+
label: String(raw.label ?? ""),
|
|
291
|
+
elementId,
|
|
292
|
+
size: size$1
|
|
293
|
+
};
|
|
294
|
+
case "SPACER": return {
|
|
295
|
+
type: "SPACER",
|
|
296
|
+
elementId,
|
|
297
|
+
size: size$1
|
|
298
|
+
};
|
|
299
|
+
case "HR": return {
|
|
300
|
+
type: "HR",
|
|
301
|
+
elementId,
|
|
302
|
+
size: size$1
|
|
303
|
+
};
|
|
304
|
+
default: throw new SystemError(SystemErrorCode.ExternalApiError, `Unknown decoration type: ${type}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (SYSTEM_FIELD_TYPES$1.has(type)) return {
|
|
308
|
+
code: String(raw.code ?? ""),
|
|
309
|
+
type,
|
|
310
|
+
...raw.size !== void 0 ? { size: parseElementSize(raw.size) } : {}
|
|
311
|
+
};
|
|
312
|
+
if (!KNOWN_FIELD_TYPES.has(type)) throw new SystemError(SystemErrorCode.ExternalApiError, `Unknown field type: ${type}`);
|
|
313
|
+
const fieldType = type;
|
|
314
|
+
const code = FieldCode.create(String(raw.code ?? ""));
|
|
315
|
+
const size = parseElementSize(raw.size);
|
|
316
|
+
return {
|
|
317
|
+
field: {
|
|
318
|
+
type: fieldType,
|
|
319
|
+
code,
|
|
320
|
+
label: "",
|
|
321
|
+
properties: {}
|
|
322
|
+
},
|
|
323
|
+
...size !== void 0 ? { size } : {}
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
function fromKintoneLayoutRow(raw) {
|
|
327
|
+
return {
|
|
328
|
+
type: "ROW",
|
|
329
|
+
fields: (raw.fields ?? []).map(fromKintoneLayoutElement)
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
function fromKintoneLayoutItem(raw) {
|
|
333
|
+
switch (raw.type) {
|
|
334
|
+
case "ROW": return fromKintoneLayoutRow(raw);
|
|
335
|
+
case "GROUP": return {
|
|
336
|
+
type: "GROUP",
|
|
337
|
+
code: FieldCode.create(String(raw.code ?? "")),
|
|
338
|
+
label: String(raw.label ?? ""),
|
|
339
|
+
...raw.openGroup !== void 0 ? { openGroup: raw.openGroup } : {},
|
|
340
|
+
layout: (raw.layout ?? []).map(fromKintoneLayoutRow)
|
|
341
|
+
};
|
|
342
|
+
case "SUBTABLE": return {
|
|
343
|
+
type: "SUBTABLE",
|
|
344
|
+
code: FieldCode.create(String(raw.code ?? "")),
|
|
345
|
+
label: String(raw.label ?? ""),
|
|
346
|
+
fields: (raw.fields ?? []).map(fromKintoneLayoutElement)
|
|
347
|
+
};
|
|
348
|
+
default: throw new SystemError(SystemErrorCode.ExternalApiError, `Unknown layout item type: ${raw.type}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
function toKintoneLayoutElement(element) {
|
|
352
|
+
if ("field" in element) {
|
|
353
|
+
const lf = element;
|
|
354
|
+
const result = {
|
|
355
|
+
type: lf.field.type,
|
|
356
|
+
code: lf.field.code
|
|
357
|
+
};
|
|
358
|
+
if (lf.size !== void 0) result.size = lf.size;
|
|
359
|
+
return result;
|
|
360
|
+
}
|
|
361
|
+
if ("elementId" in element && !("code" in element)) {
|
|
362
|
+
const dec = element;
|
|
363
|
+
const result = {
|
|
364
|
+
type: dec.type,
|
|
365
|
+
elementId: dec.elementId,
|
|
366
|
+
size: dec.size
|
|
367
|
+
};
|
|
368
|
+
if (dec.type === "LABEL") result.label = dec.label;
|
|
369
|
+
return result;
|
|
370
|
+
}
|
|
371
|
+
const sys = element;
|
|
372
|
+
return {
|
|
373
|
+
type: sys.type,
|
|
374
|
+
code: sys.code,
|
|
375
|
+
...sys.size !== void 0 ? { size: sys.size } : {}
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
function toKintoneLayoutRow(row) {
|
|
379
|
+
return {
|
|
380
|
+
type: "ROW",
|
|
381
|
+
fields: row.fields.map(toKintoneLayoutElement)
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
function toKintoneLayoutItem(item) {
|
|
385
|
+
switch (item.type) {
|
|
386
|
+
case "ROW": return toKintoneLayoutRow(item);
|
|
387
|
+
case "GROUP": {
|
|
388
|
+
const group = item;
|
|
389
|
+
return {
|
|
390
|
+
type: "GROUP",
|
|
391
|
+
code: group.code,
|
|
392
|
+
...group.openGroup !== void 0 ? { openGroup: group.openGroup } : {},
|
|
393
|
+
layout: group.layout.map(toKintoneLayoutRow)
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
case "SUBTABLE": {
|
|
397
|
+
const subtable = item;
|
|
398
|
+
return {
|
|
399
|
+
type: "SUBTABLE",
|
|
400
|
+
code: subtable.code,
|
|
401
|
+
fields: subtable.fields.map(toKintoneLayoutElement)
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
default: throw new SystemError(SystemErrorCode.ExternalApiError, `Unknown layout item type: ${item.type}`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
var KintoneFormConfigurator = class {
|
|
408
|
+
latestRevision = void 0;
|
|
409
|
+
constructor(client, appId) {
|
|
410
|
+
this.client = client;
|
|
411
|
+
this.appId = appId;
|
|
412
|
+
}
|
|
413
|
+
trackRevision(revision) {
|
|
414
|
+
if (this.latestRevision === void 0 || Number(revision) > Number(this.latestRevision)) this.latestRevision = revision;
|
|
415
|
+
}
|
|
416
|
+
async getFields() {
|
|
417
|
+
try {
|
|
418
|
+
const { properties, revision } = await this.client.app.getFormFields({
|
|
419
|
+
app: this.appId,
|
|
420
|
+
preview: true
|
|
421
|
+
});
|
|
422
|
+
this.trackRevision(revision);
|
|
423
|
+
const fields = /* @__PURE__ */ new Map();
|
|
424
|
+
for (const [code, prop] of Object.entries(properties)) {
|
|
425
|
+
const kintoneProp = prop;
|
|
426
|
+
if (SYSTEM_FIELD_TYPES$1.has(kintoneProp.type)) continue;
|
|
427
|
+
const fieldDef = fromKintoneProperty(kintoneProp);
|
|
428
|
+
fields.set(FieldCode.create(code), fieldDef);
|
|
429
|
+
if (fieldDef.type === "SUBTABLE") for (const [subCode, subDef] of fieldDef.properties.fields) fields.set(subCode, subDef);
|
|
430
|
+
}
|
|
431
|
+
return fields;
|
|
432
|
+
} catch (error) {
|
|
433
|
+
if (isBusinessRuleError(error)) throw error;
|
|
434
|
+
if (error instanceof SystemError) throw error;
|
|
435
|
+
throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to get form fields", error);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
async addFields(fields) {
|
|
439
|
+
try {
|
|
440
|
+
const properties = {};
|
|
441
|
+
for (const field of fields) properties[field.code] = toKintoneProperty(field);
|
|
442
|
+
const response = await this.client.app.addFormFields({
|
|
443
|
+
app: this.appId,
|
|
444
|
+
properties,
|
|
445
|
+
...this.latestRevision !== void 0 ? { revision: this.latestRevision } : {}
|
|
446
|
+
});
|
|
447
|
+
if (response.revision) this.trackRevision(response.revision);
|
|
448
|
+
} catch (error) {
|
|
449
|
+
if (isBusinessRuleError(error)) throw error;
|
|
450
|
+
if (error instanceof SystemError) throw error;
|
|
451
|
+
if (isRevisionConflict(error)) throw new ConflictError(ConflictErrorCode.Conflict, "Form configuration was modified by another process. Please retry the operation.", error);
|
|
452
|
+
throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to add form fields", error);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
async updateFields(fields) {
|
|
456
|
+
try {
|
|
457
|
+
const properties = {};
|
|
458
|
+
for (const field of fields) properties[field.code] = toKintoneProperty(field);
|
|
459
|
+
const response = await this.client.app.updateFormFields({
|
|
460
|
+
app: this.appId,
|
|
461
|
+
properties,
|
|
462
|
+
...this.latestRevision !== void 0 ? { revision: this.latestRevision } : {}
|
|
463
|
+
});
|
|
464
|
+
if (response.revision) this.trackRevision(response.revision);
|
|
465
|
+
} catch (error) {
|
|
466
|
+
if (isBusinessRuleError(error)) throw error;
|
|
467
|
+
if (error instanceof SystemError) throw error;
|
|
468
|
+
if (isRevisionConflict(error)) throw new ConflictError(ConflictErrorCode.Conflict, "Form configuration was modified by another process. Please retry the operation.", error);
|
|
469
|
+
throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to update form fields", error);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
async deleteFields(fieldCodes) {
|
|
473
|
+
try {
|
|
474
|
+
const response = await this.client.app.deleteFormFields({
|
|
475
|
+
app: this.appId,
|
|
476
|
+
fields: fieldCodes.map((code) => code),
|
|
477
|
+
...this.latestRevision !== void 0 ? { revision: this.latestRevision } : {}
|
|
478
|
+
});
|
|
479
|
+
if (response.revision) this.trackRevision(response.revision);
|
|
480
|
+
} catch (error) {
|
|
481
|
+
if (isBusinessRuleError(error)) throw error;
|
|
482
|
+
if (error instanceof SystemError) throw error;
|
|
483
|
+
if (isRevisionConflict(error)) throw new ConflictError(ConflictErrorCode.Conflict, "Form configuration was modified by another process. Please retry the operation.", error);
|
|
484
|
+
throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to delete form fields", error);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
async getLayout() {
|
|
488
|
+
try {
|
|
489
|
+
const response = await this.client.app.getFormLayout({
|
|
490
|
+
app: this.appId,
|
|
491
|
+
preview: true
|
|
492
|
+
});
|
|
493
|
+
this.trackRevision(response.revision);
|
|
494
|
+
return response.layout.map(fromKintoneLayoutItem);
|
|
495
|
+
} catch (error) {
|
|
496
|
+
if (isBusinessRuleError(error)) throw error;
|
|
497
|
+
if (error instanceof SystemError) throw error;
|
|
498
|
+
throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to get form layout", error);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
async updateLayout(layout) {
|
|
502
|
+
try {
|
|
503
|
+
const kintoneLayout = layout.map(toKintoneLayoutItem);
|
|
504
|
+
const response = await this.client.app.updateFormLayout({
|
|
505
|
+
app: this.appId,
|
|
506
|
+
layout: kintoneLayout,
|
|
507
|
+
revision: this.latestRevision !== void 0 ? Number(this.latestRevision) : -1
|
|
508
|
+
});
|
|
509
|
+
if (response.revision) this.trackRevision(response.revision);
|
|
510
|
+
} catch (error) {
|
|
511
|
+
if (isBusinessRuleError(error)) throw error;
|
|
512
|
+
if (error instanceof SystemError) throw error;
|
|
513
|
+
if (isRevisionConflict(error)) throw new ConflictError(ConflictErrorCode.Conflict, "Form configuration was modified by another process. Please retry the operation.", error);
|
|
514
|
+
throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to update form layout", error);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
//#endregion
|
|
520
|
+
//#region src/core/adapters/local/schemaStorage.ts
|
|
521
|
+
var LocalFileSchemaStorage = class {
|
|
522
|
+
constructor(filePath) {
|
|
523
|
+
this.filePath = filePath;
|
|
524
|
+
}
|
|
525
|
+
async get() {
|
|
526
|
+
try {
|
|
527
|
+
return await readFile(this.filePath, "utf-8");
|
|
528
|
+
} catch (error) {
|
|
529
|
+
if (isNodeError(error) && error.code === "ENOENT") return "";
|
|
530
|
+
throw new SystemError(SystemErrorCode.StorageError, `Failed to read schema file: ${this.filePath}`, error);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
async update(content) {
|
|
534
|
+
try {
|
|
535
|
+
await mkdir(dirname(this.filePath), { recursive: true });
|
|
536
|
+
await writeFile(this.filePath, content, "utf-8");
|
|
537
|
+
} catch (error) {
|
|
538
|
+
throw new SystemError(SystemErrorCode.StorageError, `Failed to write schema file: ${this.filePath}`, error);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
function isNodeError(error) {
|
|
543
|
+
return error instanceof Error && "code" in error;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
//#endregion
|
|
547
|
+
//#region src/core/application/container/cli.ts
|
|
548
|
+
function createCliContainer(config) {
|
|
549
|
+
const client = new KintoneRestAPIClient({
|
|
550
|
+
baseUrl: config.baseUrl,
|
|
551
|
+
auth: {
|
|
552
|
+
username: config.username,
|
|
553
|
+
password: config.password
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
return {
|
|
557
|
+
formConfigurator: new KintoneFormConfigurator(client, config.appId),
|
|
558
|
+
schemaStorage: new LocalFileSchemaStorage(config.schemaFilePath),
|
|
559
|
+
appDeployer: new KintoneAppDeployer(client, config.appId)
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
//#endregion
|
|
564
|
+
//#region src/core/domain/formSchema/entity.ts
|
|
565
|
+
function enrichLayoutElement(element, fields) {
|
|
566
|
+
if (!("field" in element)) return element;
|
|
567
|
+
const lf = element;
|
|
568
|
+
const fullField = fields.get(lf.field.code);
|
|
569
|
+
if (fullField === void 0) return element;
|
|
570
|
+
return {
|
|
571
|
+
field: fullField,
|
|
572
|
+
...lf.size !== void 0 ? { size: lf.size } : {}
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
function enrichLayoutRow(row, fields) {
|
|
576
|
+
return {
|
|
577
|
+
type: "ROW",
|
|
578
|
+
fields: row.fields.map((e) => enrichLayoutElement(e, fields))
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
function enrichLayoutItem(item, fields) {
|
|
582
|
+
switch (item.type) {
|
|
583
|
+
case "ROW": return enrichLayoutRow(item, fields);
|
|
584
|
+
case "GROUP": {
|
|
585
|
+
const groupDef = fields.get(item.code);
|
|
586
|
+
return {
|
|
587
|
+
...item,
|
|
588
|
+
...groupDef !== void 0 ? { label: groupDef.label } : {},
|
|
589
|
+
...groupDef !== void 0 && groupDef.noLabel !== void 0 ? { noLabel: groupDef.noLabel } : {},
|
|
590
|
+
...groupDef !== void 0 && groupDef.type === "GROUP" && groupDef.properties.openGroup !== void 0 ? { openGroup: groupDef.properties.openGroup } : {},
|
|
591
|
+
layout: item.layout.map((r) => enrichLayoutRow(r, fields))
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
case "SUBTABLE": {
|
|
595
|
+
const subtableDef = fields.get(item.code);
|
|
596
|
+
const subFieldsMap = subtableDef !== void 0 && subtableDef.type === "SUBTABLE" ? subtableDef.properties.fields : /* @__PURE__ */ new Map();
|
|
597
|
+
return {
|
|
598
|
+
...item,
|
|
599
|
+
...subtableDef !== void 0 ? { label: subtableDef.label } : {},
|
|
600
|
+
...subtableDef !== void 0 && subtableDef.noLabel !== void 0 ? { noLabel: subtableDef.noLabel } : {},
|
|
601
|
+
fields: item.fields.map((e) => enrichLayoutElement(e, subFieldsMap))
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
function enrichLayoutWithFields(layout, fields) {
|
|
607
|
+
return layout.map((item) => enrichLayoutItem(item, fields));
|
|
608
|
+
}
|
|
609
|
+
function collectSubtableInnerFieldCodes(fields) {
|
|
610
|
+
const innerCodes = /* @__PURE__ */ new Set();
|
|
611
|
+
for (const def of fields.values()) if (def.type === "SUBTABLE") for (const subCode of def.properties.fields.keys()) innerCodes.add(subCode);
|
|
612
|
+
return innerCodes;
|
|
613
|
+
}
|
|
614
|
+
const FormDiff = { create: (entries) => {
|
|
615
|
+
const summary = {
|
|
616
|
+
added: entries.filter((e) => e.type === "added").length,
|
|
617
|
+
modified: entries.filter((e) => e.type === "modified").length,
|
|
618
|
+
deleted: entries.filter((e) => e.type === "deleted").length,
|
|
619
|
+
total: entries.length
|
|
620
|
+
};
|
|
621
|
+
const sortOrder = {
|
|
622
|
+
added: 0,
|
|
623
|
+
modified: 1,
|
|
624
|
+
deleted: 2
|
|
625
|
+
};
|
|
626
|
+
return {
|
|
627
|
+
entries: [...entries].sort((a, b) => sortOrder[a.type] - sortOrder[b.type]),
|
|
628
|
+
summary,
|
|
629
|
+
isEmpty: entries.length === 0
|
|
630
|
+
};
|
|
631
|
+
} };
|
|
632
|
+
|
|
633
|
+
//#endregion
|
|
634
|
+
//#region src/core/domain/formSchema/services/schemaSerializer.ts
|
|
635
|
+
function isLayoutField(element) {
|
|
636
|
+
return "field" in element;
|
|
637
|
+
}
|
|
638
|
+
function isDecorationElement(element) {
|
|
639
|
+
return "elementId" in element;
|
|
640
|
+
}
|
|
641
|
+
function isSystemFieldLayout(element) {
|
|
642
|
+
return !("field" in element) && !("elementId" in element);
|
|
643
|
+
}
|
|
644
|
+
function serializeSize(size) {
|
|
645
|
+
const result = {};
|
|
646
|
+
if (size.width !== void 0) result.width = size.width;
|
|
647
|
+
if (size.height !== void 0) result.height = size.height;
|
|
648
|
+
if (size.innerHeight !== void 0) result.innerHeight = size.innerHeight;
|
|
649
|
+
return result;
|
|
650
|
+
}
|
|
651
|
+
function serializeFlatField(field, size) {
|
|
652
|
+
const result = {
|
|
653
|
+
code: field.code,
|
|
654
|
+
type: field.type,
|
|
655
|
+
label: field.label
|
|
656
|
+
};
|
|
657
|
+
if (field.noLabel !== void 0) result.noLabel = field.noLabel;
|
|
658
|
+
if (size !== void 0) result.size = serializeSize(size);
|
|
659
|
+
if (field.type === "REFERENCE_TABLE") {
|
|
660
|
+
const ref = field.properties.referenceTable;
|
|
661
|
+
result.referenceTable = {
|
|
662
|
+
relatedApp: ref.relatedApp,
|
|
663
|
+
condition: {
|
|
664
|
+
field: ref.condition.field,
|
|
665
|
+
relatedField: ref.condition.relatedField
|
|
666
|
+
},
|
|
667
|
+
...ref.filterCond !== void 0 ? { filterCond: ref.filterCond } : {},
|
|
668
|
+
displayFields: ref.displayFields.map((f) => f),
|
|
669
|
+
...ref.sort !== void 0 ? { sort: ref.sort } : {},
|
|
670
|
+
...ref.size !== void 0 ? { size: ref.size } : {}
|
|
671
|
+
};
|
|
672
|
+
return result;
|
|
673
|
+
}
|
|
674
|
+
if (field.type !== "SUBTABLE" && field.type !== "GROUP") {
|
|
675
|
+
const properties = field.properties;
|
|
676
|
+
for (const [key, value] of Object.entries(properties)) result[key] = value;
|
|
677
|
+
}
|
|
678
|
+
return result;
|
|
679
|
+
}
|
|
680
|
+
function serializeLayoutElement(element) {
|
|
681
|
+
if (isLayoutField(element)) return serializeFlatField(element.field, element.size);
|
|
682
|
+
if (isDecorationElement(element)) {
|
|
683
|
+
const dec = element;
|
|
684
|
+
const result = { type: dec.type };
|
|
685
|
+
if (dec.type === "LABEL") result.label = dec.label;
|
|
686
|
+
result.elementId = dec.elementId;
|
|
687
|
+
if (dec.size !== void 0) result.size = serializeSize(dec.size);
|
|
688
|
+
return result;
|
|
689
|
+
}
|
|
690
|
+
if (isSystemFieldLayout(element)) {
|
|
691
|
+
const sys = element;
|
|
692
|
+
const result = {
|
|
693
|
+
code: sys.code,
|
|
694
|
+
type: sys.type
|
|
695
|
+
};
|
|
696
|
+
if (sys.size !== void 0) result.size = serializeSize(sys.size);
|
|
697
|
+
return result;
|
|
698
|
+
}
|
|
699
|
+
return element;
|
|
700
|
+
}
|
|
701
|
+
function serializeLayoutRow(row) {
|
|
702
|
+
return {
|
|
703
|
+
type: "ROW",
|
|
704
|
+
fields: row.fields.map(serializeLayoutElement)
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
function serializeLayoutItem(item) {
|
|
708
|
+
switch (item.type) {
|
|
709
|
+
case "ROW": return serializeLayoutRow(item);
|
|
710
|
+
case "GROUP": return {
|
|
711
|
+
type: "GROUP",
|
|
712
|
+
code: item.code,
|
|
713
|
+
label: item.label,
|
|
714
|
+
...item.noLabel !== void 0 ? { noLabel: item.noLabel } : {},
|
|
715
|
+
...item.openGroup !== void 0 ? { openGroup: item.openGroup } : {},
|
|
716
|
+
layout: item.layout.map(serializeLayoutRow)
|
|
717
|
+
};
|
|
718
|
+
case "SUBTABLE": return {
|
|
719
|
+
type: "SUBTABLE",
|
|
720
|
+
code: item.code,
|
|
721
|
+
label: item.label,
|
|
722
|
+
...item.noLabel !== void 0 ? { noLabel: item.noLabel } : {},
|
|
723
|
+
fields: item.fields.map(serializeLayoutElement)
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
const SchemaSerializer = { serialize: (layout) => {
|
|
728
|
+
return stringify({ layout: layout.map(serializeLayoutItem) }, {
|
|
729
|
+
lineWidth: 0,
|
|
730
|
+
defaultKeyType: "PLAIN",
|
|
731
|
+
defaultStringType: "PLAIN"
|
|
732
|
+
});
|
|
733
|
+
} };
|
|
734
|
+
|
|
735
|
+
//#endregion
|
|
736
|
+
//#region src/core/application/formSchema/captureSchema.ts
|
|
737
|
+
async function captureSchema({ container }) {
|
|
738
|
+
const [currentFields, currentLayout] = await Promise.all([container.formConfigurator.getFields(), container.formConfigurator.getLayout()]);
|
|
739
|
+
const enrichedLayout = enrichLayoutWithFields(currentLayout, currentFields);
|
|
740
|
+
return {
|
|
741
|
+
schemaText: SchemaSerializer.serialize(enrichedLayout),
|
|
742
|
+
hasExistingSchema: (await container.schemaStorage.get()).length > 0
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
//#endregion
|
|
747
|
+
//#region src/core/application/formSchema/saveSchema.ts
|
|
748
|
+
async function saveSchema({ container, input }) {
|
|
749
|
+
await container.schemaStorage.update(input.schemaText);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
//#endregion
|
|
753
|
+
//#region src/cli/config.ts
|
|
754
|
+
const CliConfigSchema = v.object({
|
|
755
|
+
KINTONE_DOMAIN: v.pipe(v.string(), v.nonEmpty("KINTONE_DOMAIN is required")),
|
|
756
|
+
KINTONE_USERNAME: v.pipe(v.string(), v.nonEmpty("KINTONE_USERNAME is required")),
|
|
757
|
+
KINTONE_PASSWORD: v.pipe(v.string(), v.nonEmpty("KINTONE_PASSWORD is required")),
|
|
758
|
+
KINTONE_APP_ID: v.pipe(v.string(), v.nonEmpty("KINTONE_APP_ID is required")),
|
|
759
|
+
SCHEMA_FILE_PATH: v.optional(v.string(), "schema.yaml")
|
|
760
|
+
});
|
|
761
|
+
const kintoneArgs = {
|
|
762
|
+
domain: {
|
|
763
|
+
type: "string",
|
|
764
|
+
short: "d",
|
|
765
|
+
description: "kintone domain (overrides KINTONE_DOMAIN env var)"
|
|
766
|
+
},
|
|
767
|
+
username: {
|
|
768
|
+
type: "string",
|
|
769
|
+
short: "u",
|
|
770
|
+
description: "kintone username (overrides KINTONE_USERNAME env var)"
|
|
771
|
+
},
|
|
772
|
+
password: {
|
|
773
|
+
type: "string",
|
|
774
|
+
short: "p",
|
|
775
|
+
description: "kintone password (overrides KINTONE_PASSWORD env var)"
|
|
776
|
+
},
|
|
777
|
+
"app-id": {
|
|
778
|
+
type: "string",
|
|
779
|
+
short: "a",
|
|
780
|
+
description: "kintone app ID (overrides KINTONE_APP_ID env var)"
|
|
781
|
+
},
|
|
782
|
+
"schema-file": {
|
|
783
|
+
type: "string",
|
|
784
|
+
short: "f",
|
|
785
|
+
description: "Schema file path (default: schema.yaml)"
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
function resolveConfig(cliValues) {
|
|
789
|
+
const result = v.safeParse(CliConfigSchema, {
|
|
790
|
+
KINTONE_DOMAIN: cliValues.domain ?? process.env.KINTONE_DOMAIN ?? "",
|
|
791
|
+
KINTONE_USERNAME: cliValues.username ?? process.env.KINTONE_USERNAME ?? "",
|
|
792
|
+
KINTONE_PASSWORD: cliValues.password ?? process.env.KINTONE_PASSWORD ?? "",
|
|
793
|
+
KINTONE_APP_ID: cliValues["app-id"] ?? process.env.KINTONE_APP_ID ?? "",
|
|
794
|
+
SCHEMA_FILE_PATH: cliValues["schema-file"] ?? process.env.SCHEMA_FILE_PATH
|
|
795
|
+
});
|
|
796
|
+
if (!result.success) {
|
|
797
|
+
const missing = result.issues.map((issue) => issue.message).join("\n ");
|
|
798
|
+
throw new Error(`Missing required configuration:\n ${missing}`);
|
|
799
|
+
}
|
|
800
|
+
return {
|
|
801
|
+
baseUrl: `https://${result.output.KINTONE_DOMAIN}`,
|
|
802
|
+
username: result.output.KINTONE_USERNAME,
|
|
803
|
+
password: result.output.KINTONE_PASSWORD,
|
|
804
|
+
appId: result.output.KINTONE_APP_ID,
|
|
805
|
+
schemaFilePath: result.output.SCHEMA_FILE_PATH
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
//#endregion
|
|
810
|
+
//#region src/cli/handleError.ts
|
|
811
|
+
function handleCliError(error) {
|
|
812
|
+
if (isBusinessRuleError(error)) {
|
|
813
|
+
p.log.error(`[BusinessRuleError] ${error.code}: ${error.message}`);
|
|
814
|
+
logErrorDetails(error);
|
|
815
|
+
p.outro("Failed.");
|
|
816
|
+
process.exit(1);
|
|
817
|
+
}
|
|
818
|
+
if (isValidationError(error)) {
|
|
819
|
+
p.log.error(`[ValidationError] ${error.message}`);
|
|
820
|
+
logErrorDetails(error);
|
|
821
|
+
p.outro("Failed.");
|
|
822
|
+
process.exit(1);
|
|
823
|
+
}
|
|
824
|
+
if (isSystemError(error)) {
|
|
825
|
+
p.log.error(`[SystemError] ${error.code}: ${error.message}`);
|
|
826
|
+
logErrorDetails(error);
|
|
827
|
+
p.outro("Failed.");
|
|
828
|
+
process.exit(1);
|
|
829
|
+
}
|
|
830
|
+
if (isApplicationError(error)) {
|
|
831
|
+
p.log.error(`[${error.name}] ${error.code}: ${error.message}`);
|
|
832
|
+
logErrorDetails(error);
|
|
833
|
+
p.outro("Failed.");
|
|
834
|
+
process.exit(1);
|
|
835
|
+
}
|
|
836
|
+
if (error instanceof Error) {
|
|
837
|
+
p.log.error(`[Error] ${error.message}`);
|
|
838
|
+
logErrorDetails(error);
|
|
839
|
+
p.outro("Failed.");
|
|
840
|
+
process.exit(1);
|
|
841
|
+
}
|
|
842
|
+
p.log.error(`[Error] 予期しないエラーが発生しました: ${String(error)}`);
|
|
843
|
+
p.outro("Failed.");
|
|
844
|
+
process.exit(1);
|
|
845
|
+
}
|
|
846
|
+
function logErrorDetails(error) {
|
|
847
|
+
if (error.cause) {
|
|
848
|
+
p.log.warn(`Cause: ${String(error.cause)}`);
|
|
849
|
+
const cause = error.cause;
|
|
850
|
+
if (cause.errors) p.log.warn(`Details: ${JSON.stringify(cause.errors, null, 2)}`);
|
|
851
|
+
}
|
|
852
|
+
if (error.stack) p.log.warn(`Stack: ${error.stack}`);
|
|
853
|
+
}
|
|
854
|
+
function isApplicationError(error) {
|
|
855
|
+
return error instanceof Error && "code" in error && error.name !== "BusinessRuleError";
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
//#endregion
|
|
859
|
+
//#region src/cli/commands/capture.ts
|
|
860
|
+
var capture_default = define({
|
|
861
|
+
name: "capture",
|
|
862
|
+
description: "Capture current kintone form schema to file",
|
|
863
|
+
args: { ...kintoneArgs },
|
|
864
|
+
run: async (ctx) => {
|
|
865
|
+
try {
|
|
866
|
+
const config = resolveConfig(ctx.values);
|
|
867
|
+
const container = createCliContainer(config);
|
|
868
|
+
const s = p.spinner();
|
|
869
|
+
s.start("Capturing current form schema...");
|
|
870
|
+
const result = await captureSchema({ container });
|
|
871
|
+
s.stop("Form schema captured.");
|
|
872
|
+
await saveSchema({
|
|
873
|
+
container,
|
|
874
|
+
input: { schemaText: result.schemaText }
|
|
875
|
+
});
|
|
876
|
+
p.log.success(`Schema saved to: ${pc.cyan(config.schemaFilePath)}`);
|
|
877
|
+
if (result.hasExistingSchema) p.log.warn("Existing schema was overwritten.");
|
|
878
|
+
} catch (error) {
|
|
879
|
+
handleCliError(error);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
//#endregion
|
|
885
|
+
//#region src/core/domain/formSchema/services/diffDetector.ts
|
|
886
|
+
function isArrayEqual(a, b) {
|
|
887
|
+
if (!Array.isArray(a) || !Array.isArray(b)) return false;
|
|
888
|
+
if (a.length !== b.length) return false;
|
|
889
|
+
for (let i = 0; i < a.length; i++) if (!isValueEqual(a[i], b[i])) return false;
|
|
890
|
+
return true;
|
|
891
|
+
}
|
|
892
|
+
function isRecordEqual(a, b) {
|
|
893
|
+
const keysA = Object.keys(a);
|
|
894
|
+
const keysB = Object.keys(b);
|
|
895
|
+
if (keysA.length !== keysB.length) return false;
|
|
896
|
+
for (const key of keysA) {
|
|
897
|
+
if (!Object.hasOwn(b, key)) return false;
|
|
898
|
+
if (!isValueEqual(a[key], b[key])) return false;
|
|
899
|
+
}
|
|
900
|
+
return true;
|
|
901
|
+
}
|
|
902
|
+
function isValueEqual(a, b) {
|
|
903
|
+
if (a === b) return true;
|
|
904
|
+
if (a === null || b === null) return a === b;
|
|
905
|
+
if (typeof a !== typeof b) return false;
|
|
906
|
+
if (Array.isArray(a)) return isArrayEqual(a, b);
|
|
907
|
+
if (typeof a === "object") return isRecordEqual(a, b);
|
|
908
|
+
return false;
|
|
909
|
+
}
|
|
910
|
+
function isMapEqual(a, b) {
|
|
911
|
+
if (a.size !== b.size) return false;
|
|
912
|
+
for (const [key, valA] of a) {
|
|
913
|
+
const valB = b.get(key);
|
|
914
|
+
if (valB === void 0) return false;
|
|
915
|
+
if (!isFieldEqual(valA, valB)) return false;
|
|
916
|
+
}
|
|
917
|
+
return true;
|
|
918
|
+
}
|
|
919
|
+
function isPropertiesEqual(a, b) {
|
|
920
|
+
if (a.type === "SUBTABLE" && b.type === "SUBTABLE") return isMapEqual(a.properties.fields, b.properties.fields);
|
|
921
|
+
if (a.type === "REFERENCE_TABLE" && b.type === "REFERENCE_TABLE") {
|
|
922
|
+
const refA = a.properties.referenceTable;
|
|
923
|
+
const refB = b.properties.referenceTable;
|
|
924
|
+
return isValueEqual({
|
|
925
|
+
...refA,
|
|
926
|
+
displayFields: [...refA.displayFields]
|
|
927
|
+
}, {
|
|
928
|
+
...refB,
|
|
929
|
+
displayFields: [...refB.displayFields]
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
return isValueEqual(a.properties, b.properties);
|
|
933
|
+
}
|
|
934
|
+
function isFieldEqual(a, b) {
|
|
935
|
+
if (a.type !== b.type) return false;
|
|
936
|
+
if (a.label !== b.label) return false;
|
|
937
|
+
if (a.code !== b.code) return false;
|
|
938
|
+
if (Boolean(a.noLabel) !== Boolean(b.noLabel)) return false;
|
|
939
|
+
return isPropertiesEqual(a, b);
|
|
940
|
+
}
|
|
941
|
+
function hasPropertiesChanged(before, after) {
|
|
942
|
+
if (before.type === "SUBTABLE" && after.type === "SUBTABLE") return !isMapEqual(before.properties.fields, after.properties.fields);
|
|
943
|
+
if (before.type === "REFERENCE_TABLE" && after.type === "REFERENCE_TABLE") {
|
|
944
|
+
const refB = before.properties.referenceTable;
|
|
945
|
+
const refA = after.properties.referenceTable;
|
|
946
|
+
return !isValueEqual({
|
|
947
|
+
...refB,
|
|
948
|
+
displayFields: [...refB.displayFields]
|
|
949
|
+
}, {
|
|
950
|
+
...refA,
|
|
951
|
+
displayFields: [...refA.displayFields]
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
return !isValueEqual(before.properties, after.properties);
|
|
955
|
+
}
|
|
956
|
+
function describeChanges(before, after) {
|
|
957
|
+
const changes = [];
|
|
958
|
+
if (before.type !== after.type) changes.push(`type: ${before.type} -> ${after.type}`);
|
|
959
|
+
if (before.label !== after.label) changes.push(`label: ${before.label} -> ${after.label}`);
|
|
960
|
+
if (Boolean(before.noLabel) !== Boolean(after.noLabel)) changes.push(`noLabel: ${before.noLabel ?? false} -> ${after.noLabel ?? false}`);
|
|
961
|
+
if (hasPropertiesChanged(before, after)) changes.push("properties changed");
|
|
962
|
+
return changes.length > 0 ? changes.join(", ") : "no visible changes";
|
|
963
|
+
}
|
|
964
|
+
function isLayoutEqual(a, b) {
|
|
965
|
+
return isValueEqual(a, b);
|
|
966
|
+
}
|
|
967
|
+
const DiffDetector = {
|
|
968
|
+
detectLayoutChanges: (schemaLayout, currentLayout) => {
|
|
969
|
+
return !isLayoutEqual(schemaLayout, currentLayout);
|
|
970
|
+
},
|
|
971
|
+
detect: (schema, current) => {
|
|
972
|
+
const entries = [];
|
|
973
|
+
for (const [fieldCode, schemaDef] of schema.fields) {
|
|
974
|
+
const currentDef = current.get(fieldCode);
|
|
975
|
+
if (currentDef === void 0) entries.push({
|
|
976
|
+
type: "added",
|
|
977
|
+
fieldCode,
|
|
978
|
+
fieldLabel: schemaDef.label,
|
|
979
|
+
details: "new field",
|
|
980
|
+
after: schemaDef
|
|
981
|
+
});
|
|
982
|
+
else if (!isFieldEqual(schemaDef, currentDef)) entries.push({
|
|
983
|
+
type: "modified",
|
|
984
|
+
fieldCode,
|
|
985
|
+
fieldLabel: schemaDef.label,
|
|
986
|
+
details: describeChanges(currentDef, schemaDef),
|
|
987
|
+
before: currentDef,
|
|
988
|
+
after: schemaDef
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
for (const [fieldCode, currentDef] of current) if (!schema.fields.has(fieldCode)) entries.push({
|
|
992
|
+
type: "deleted",
|
|
993
|
+
fieldCode,
|
|
994
|
+
fieldLabel: currentDef.label,
|
|
995
|
+
details: "deleted",
|
|
996
|
+
before: currentDef
|
|
997
|
+
});
|
|
998
|
+
return FormDiff.create(entries);
|
|
999
|
+
}
|
|
1000
|
+
};
|
|
1001
|
+
|
|
1002
|
+
//#endregion
|
|
1003
|
+
//#region src/core/domain/formSchema/services/schemaParser.ts
|
|
1004
|
+
const VALID_UNIT_POSITIONS = new Set(["BEFORE", "AFTER"]);
|
|
1005
|
+
const VALID_CALC_FORMATS = new Set([
|
|
1006
|
+
"NUMBER",
|
|
1007
|
+
"NUMBER_DIGIT",
|
|
1008
|
+
"DATE",
|
|
1009
|
+
"TIME",
|
|
1010
|
+
"DATETIME",
|
|
1011
|
+
"HOUR_MINUTE",
|
|
1012
|
+
"DAY_HOUR_MINUTE"
|
|
1013
|
+
]);
|
|
1014
|
+
const VALID_SELECTION_ALIGNS = new Set(["HORIZONTAL", "VERTICAL"]);
|
|
1015
|
+
const VALID_LINK_PROTOCOLS = new Set([
|
|
1016
|
+
"WEB",
|
|
1017
|
+
"CALL",
|
|
1018
|
+
"MAIL"
|
|
1019
|
+
]);
|
|
1020
|
+
function validateEnumProperty(fieldCode, propName, value, validValues) {
|
|
1021
|
+
if (value !== void 0 && !validValues.has(value)) throw new BusinessRuleError(FormSchemaErrorCode.InvalidSchemaStructure, `Invalid ${propName} "${String(value)}" for field "${fieldCode}". Expected one of: ${[...validValues].join(", ")}`);
|
|
1022
|
+
}
|
|
1023
|
+
function validateFieldProperties(code, fieldType, properties) {
|
|
1024
|
+
switch (fieldType) {
|
|
1025
|
+
case "NUMBER":
|
|
1026
|
+
validateEnumProperty(code, "unitPosition", properties.unitPosition, VALID_UNIT_POSITIONS);
|
|
1027
|
+
break;
|
|
1028
|
+
case "CALC":
|
|
1029
|
+
validateEnumProperty(code, "format", properties.format, VALID_CALC_FORMATS);
|
|
1030
|
+
validateEnumProperty(code, "unitPosition", properties.unitPosition, VALID_UNIT_POSITIONS);
|
|
1031
|
+
break;
|
|
1032
|
+
case "CHECK_BOX":
|
|
1033
|
+
case "RADIO_BUTTON":
|
|
1034
|
+
case "MULTI_SELECT":
|
|
1035
|
+
case "DROP_DOWN":
|
|
1036
|
+
validateEnumProperty(code, "align", properties.align, VALID_SELECTION_ALIGNS);
|
|
1037
|
+
break;
|
|
1038
|
+
case "LINK":
|
|
1039
|
+
validateEnumProperty(code, "protocol", properties.protocol, VALID_LINK_PROTOCOLS);
|
|
1040
|
+
break;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
const VALID_FIELD_TYPES = new Set([
|
|
1044
|
+
"SINGLE_LINE_TEXT",
|
|
1045
|
+
"MULTI_LINE_TEXT",
|
|
1046
|
+
"RICH_TEXT",
|
|
1047
|
+
"NUMBER",
|
|
1048
|
+
"CALC",
|
|
1049
|
+
"CHECK_BOX",
|
|
1050
|
+
"RADIO_BUTTON",
|
|
1051
|
+
"MULTI_SELECT",
|
|
1052
|
+
"DROP_DOWN",
|
|
1053
|
+
"DATE",
|
|
1054
|
+
"TIME",
|
|
1055
|
+
"DATETIME",
|
|
1056
|
+
"LINK",
|
|
1057
|
+
"USER_SELECT",
|
|
1058
|
+
"ORGANIZATION_SELECT",
|
|
1059
|
+
"GROUP_SELECT",
|
|
1060
|
+
"FILE",
|
|
1061
|
+
"GROUP",
|
|
1062
|
+
"SUBTABLE",
|
|
1063
|
+
"REFERENCE_TABLE"
|
|
1064
|
+
]);
|
|
1065
|
+
const DECORATION_TYPES = new Set([
|
|
1066
|
+
"LABEL",
|
|
1067
|
+
"SPACER",
|
|
1068
|
+
"HR"
|
|
1069
|
+
]);
|
|
1070
|
+
const SYSTEM_FIELD_TYPES = new Set([
|
|
1071
|
+
"RECORD_NUMBER",
|
|
1072
|
+
"CREATOR",
|
|
1073
|
+
"CREATED_TIME",
|
|
1074
|
+
"MODIFIER",
|
|
1075
|
+
"UPDATED_TIME",
|
|
1076
|
+
"CATEGORY",
|
|
1077
|
+
"STATUS",
|
|
1078
|
+
"STATUS_ASSIGNEE"
|
|
1079
|
+
]);
|
|
1080
|
+
const BASE_ATTRIBUTES = new Set([
|
|
1081
|
+
"code",
|
|
1082
|
+
"type",
|
|
1083
|
+
"label",
|
|
1084
|
+
"noLabel"
|
|
1085
|
+
]);
|
|
1086
|
+
const LAYOUT_ATTRIBUTES = new Set(["size"]);
|
|
1087
|
+
const DECORATION_ATTRIBUTES = new Set(["elementId"]);
|
|
1088
|
+
const GROUP_ATTRIBUTES = new Set(["openGroup", "layout"]);
|
|
1089
|
+
const SUBTABLE_ATTRIBUTES = new Set(["fields"]);
|
|
1090
|
+
function parseSize(raw) {
|
|
1091
|
+
if (raw === void 0 || raw === null) return void 0;
|
|
1092
|
+
if (typeof raw !== "object") return void 0;
|
|
1093
|
+
const obj = raw;
|
|
1094
|
+
return {
|
|
1095
|
+
...obj.width !== void 0 ? { width: String(obj.width) } : {},
|
|
1096
|
+
...obj.height !== void 0 ? { height: String(obj.height) } : {},
|
|
1097
|
+
...obj.innerHeight !== void 0 ? { innerHeight: String(obj.innerHeight) } : {}
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
function normalizePropertyValue(value) {
|
|
1101
|
+
if (typeof value === "number") return String(value);
|
|
1102
|
+
return value;
|
|
1103
|
+
}
|
|
1104
|
+
function extractProperties(raw) {
|
|
1105
|
+
const properties = {};
|
|
1106
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
1107
|
+
if (BASE_ATTRIBUTES.has(key)) continue;
|
|
1108
|
+
if (LAYOUT_ATTRIBUTES.has(key)) continue;
|
|
1109
|
+
if (DECORATION_ATTRIBUTES.has(key)) continue;
|
|
1110
|
+
if (GROUP_ATTRIBUTES.has(key)) continue;
|
|
1111
|
+
if (SUBTABLE_ATTRIBUTES.has(key)) continue;
|
|
1112
|
+
properties[key] = normalizePropertyValue(value);
|
|
1113
|
+
}
|
|
1114
|
+
return properties;
|
|
1115
|
+
}
|
|
1116
|
+
function buildFieldDefinition(base, fieldType, properties) {
|
|
1117
|
+
switch (fieldType) {
|
|
1118
|
+
case "SINGLE_LINE_TEXT": return {
|
|
1119
|
+
...base,
|
|
1120
|
+
type: fieldType,
|
|
1121
|
+
properties
|
|
1122
|
+
};
|
|
1123
|
+
case "MULTI_LINE_TEXT": return {
|
|
1124
|
+
...base,
|
|
1125
|
+
type: fieldType,
|
|
1126
|
+
properties
|
|
1127
|
+
};
|
|
1128
|
+
case "RICH_TEXT": return {
|
|
1129
|
+
...base,
|
|
1130
|
+
type: fieldType,
|
|
1131
|
+
properties
|
|
1132
|
+
};
|
|
1133
|
+
case "NUMBER": return {
|
|
1134
|
+
...base,
|
|
1135
|
+
type: fieldType,
|
|
1136
|
+
properties
|
|
1137
|
+
};
|
|
1138
|
+
case "CALC": return {
|
|
1139
|
+
...base,
|
|
1140
|
+
type: fieldType,
|
|
1141
|
+
properties
|
|
1142
|
+
};
|
|
1143
|
+
case "CHECK_BOX":
|
|
1144
|
+
case "RADIO_BUTTON":
|
|
1145
|
+
case "MULTI_SELECT":
|
|
1146
|
+
case "DROP_DOWN": return {
|
|
1147
|
+
...base,
|
|
1148
|
+
type: fieldType,
|
|
1149
|
+
properties
|
|
1150
|
+
};
|
|
1151
|
+
case "DATE": return {
|
|
1152
|
+
...base,
|
|
1153
|
+
type: fieldType,
|
|
1154
|
+
properties
|
|
1155
|
+
};
|
|
1156
|
+
case "TIME": return {
|
|
1157
|
+
...base,
|
|
1158
|
+
type: fieldType,
|
|
1159
|
+
properties
|
|
1160
|
+
};
|
|
1161
|
+
case "DATETIME": return {
|
|
1162
|
+
...base,
|
|
1163
|
+
type: fieldType,
|
|
1164
|
+
properties
|
|
1165
|
+
};
|
|
1166
|
+
case "LINK": return {
|
|
1167
|
+
...base,
|
|
1168
|
+
type: fieldType,
|
|
1169
|
+
properties
|
|
1170
|
+
};
|
|
1171
|
+
case "USER_SELECT":
|
|
1172
|
+
case "ORGANIZATION_SELECT":
|
|
1173
|
+
case "GROUP_SELECT": return {
|
|
1174
|
+
...base,
|
|
1175
|
+
type: fieldType,
|
|
1176
|
+
properties
|
|
1177
|
+
};
|
|
1178
|
+
case "FILE": return {
|
|
1179
|
+
...base,
|
|
1180
|
+
type: fieldType,
|
|
1181
|
+
properties
|
|
1182
|
+
};
|
|
1183
|
+
case "GROUP": return {
|
|
1184
|
+
...base,
|
|
1185
|
+
type: fieldType,
|
|
1186
|
+
properties
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
function parseFieldDefinitionFromFlat(raw) {
|
|
1191
|
+
const code = String(raw.code);
|
|
1192
|
+
const type = String(raw.type);
|
|
1193
|
+
if (!VALID_FIELD_TYPES.has(type)) throw new BusinessRuleError(FormSchemaErrorCode.InvalidFieldType, `Invalid field type "${type}" for field "${code}"`);
|
|
1194
|
+
const fieldCode = FieldCode.create(code);
|
|
1195
|
+
const fieldType = type;
|
|
1196
|
+
const base = {
|
|
1197
|
+
code: fieldCode,
|
|
1198
|
+
label: String(raw.label ?? ""),
|
|
1199
|
+
...raw.noLabel !== void 0 ? { noLabel: raw.noLabel } : {}
|
|
1200
|
+
};
|
|
1201
|
+
if (fieldType === "SUBTABLE") {
|
|
1202
|
+
const rawFields = raw.fields ?? [];
|
|
1203
|
+
const subFields = /* @__PURE__ */ new Map();
|
|
1204
|
+
for (const subRaw of rawFields) {
|
|
1205
|
+
const subDef = parseFieldDefinitionFromFlat(subRaw);
|
|
1206
|
+
subFields.set(subDef.code, subDef);
|
|
1207
|
+
}
|
|
1208
|
+
return {
|
|
1209
|
+
...base,
|
|
1210
|
+
type: "SUBTABLE",
|
|
1211
|
+
properties: { fields: subFields }
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
if (fieldType === "REFERENCE_TABLE") {
|
|
1215
|
+
if (raw.referenceTable === void 0 || raw.referenceTable === null || typeof raw.referenceTable !== "object") throw new BusinessRuleError(FormSchemaErrorCode.InvalidSchemaStructure, `Field "${code}" of type REFERENCE_TABLE must have a "referenceTable" property`);
|
|
1216
|
+
const refTable = raw.referenceTable;
|
|
1217
|
+
if (refTable.relatedApp === void 0 || refTable.relatedApp === null || typeof refTable.relatedApp !== "object") throw new BusinessRuleError(FormSchemaErrorCode.InvalidSchemaStructure, `Field "${code}" of type REFERENCE_TABLE must have "referenceTable.relatedApp"`);
|
|
1218
|
+
if (refTable.condition === void 0 || refTable.condition === null || typeof refTable.condition !== "object") throw new BusinessRuleError(FormSchemaErrorCode.InvalidSchemaStructure, `Field "${code}" of type REFERENCE_TABLE must have "referenceTable.condition"`);
|
|
1219
|
+
if (!Array.isArray(refTable.displayFields)) throw new BusinessRuleError(FormSchemaErrorCode.InvalidSchemaStructure, `Field "${code}" of type REFERENCE_TABLE must have "referenceTable.displayFields" array`);
|
|
1220
|
+
const condition = refTable.condition;
|
|
1221
|
+
const displayFields = refTable.displayFields.map((f) => FieldCode.create(f));
|
|
1222
|
+
return {
|
|
1223
|
+
...base,
|
|
1224
|
+
type: "REFERENCE_TABLE",
|
|
1225
|
+
properties: { referenceTable: {
|
|
1226
|
+
relatedApp: refTable.relatedApp,
|
|
1227
|
+
condition: {
|
|
1228
|
+
field: FieldCode.create(condition.field),
|
|
1229
|
+
relatedField: FieldCode.create(condition.relatedField)
|
|
1230
|
+
},
|
|
1231
|
+
...refTable.filterCond !== void 0 ? { filterCond: refTable.filterCond } : {},
|
|
1232
|
+
displayFields,
|
|
1233
|
+
...refTable.sort !== void 0 ? { sort: refTable.sort } : {},
|
|
1234
|
+
...refTable.size !== void 0 ? { size: String(refTable.size) } : {}
|
|
1235
|
+
} }
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
const properties = extractProperties(raw);
|
|
1239
|
+
validateFieldProperties(code, fieldType, properties);
|
|
1240
|
+
return buildFieldDefinition(base, fieldType, properties);
|
|
1241
|
+
}
|
|
1242
|
+
function parseDecorationElement(raw) {
|
|
1243
|
+
const type = String(raw.type);
|
|
1244
|
+
const elementId = String(raw.elementId ?? "");
|
|
1245
|
+
const size = parseSize(raw.size) ?? {};
|
|
1246
|
+
switch (type) {
|
|
1247
|
+
case "LABEL": return {
|
|
1248
|
+
type: "LABEL",
|
|
1249
|
+
label: String(raw.label ?? ""),
|
|
1250
|
+
elementId,
|
|
1251
|
+
size
|
|
1252
|
+
};
|
|
1253
|
+
case "SPACER": return {
|
|
1254
|
+
type: "SPACER",
|
|
1255
|
+
elementId,
|
|
1256
|
+
size
|
|
1257
|
+
};
|
|
1258
|
+
case "HR": return {
|
|
1259
|
+
type: "HR",
|
|
1260
|
+
elementId,
|
|
1261
|
+
size
|
|
1262
|
+
};
|
|
1263
|
+
default: throw new BusinessRuleError(FormSchemaErrorCode.InvalidDecorationElement, `Unknown decoration element type: "${type}"`);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
function parseLayoutElement(raw) {
|
|
1267
|
+
const type = String(raw.type);
|
|
1268
|
+
if (DECORATION_TYPES.has(type)) return parseDecorationElement(raw);
|
|
1269
|
+
if (SYSTEM_FIELD_TYPES.has(type)) return {
|
|
1270
|
+
code: String(raw.code),
|
|
1271
|
+
type,
|
|
1272
|
+
...raw.size !== void 0 ? { size: parseSize(raw.size) } : {}
|
|
1273
|
+
};
|
|
1274
|
+
const field = parseFieldDefinitionFromFlat(raw);
|
|
1275
|
+
const size = parseSize(raw.size);
|
|
1276
|
+
return {
|
|
1277
|
+
field,
|
|
1278
|
+
...size !== void 0 ? { size } : {}
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
function parseLayoutRow(raw) {
|
|
1282
|
+
if (raw.type !== "ROW") throw new BusinessRuleError(FormSchemaErrorCode.InvalidLayoutStructure, `Expected layout row type "ROW", got "${String(raw.type)}"`);
|
|
1283
|
+
return {
|
|
1284
|
+
type: "ROW",
|
|
1285
|
+
fields: (raw.fields ?? []).map(parseLayoutElement)
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
function parseLayoutItem(raw, fieldMap) {
|
|
1289
|
+
const type = String(raw.type);
|
|
1290
|
+
switch (type) {
|
|
1291
|
+
case "ROW": {
|
|
1292
|
+
const row = parseLayoutRow(raw);
|
|
1293
|
+
collectFieldsFromElements(row.fields, fieldMap);
|
|
1294
|
+
return row;
|
|
1295
|
+
}
|
|
1296
|
+
case "GROUP": {
|
|
1297
|
+
const code = FieldCode.create(String(raw.code));
|
|
1298
|
+
const label = String(raw.label ?? "");
|
|
1299
|
+
const noLabel = raw.noLabel !== void 0 ? raw.noLabel : void 0;
|
|
1300
|
+
const openGroup = raw.openGroup !== void 0 ? raw.openGroup : void 0;
|
|
1301
|
+
const layout = (raw.layout ?? []).map((r) => {
|
|
1302
|
+
const row = parseLayoutRow(r);
|
|
1303
|
+
collectFieldsFromElements(row.fields, fieldMap);
|
|
1304
|
+
return row;
|
|
1305
|
+
});
|
|
1306
|
+
addFieldToMap(fieldMap, code, {
|
|
1307
|
+
code,
|
|
1308
|
+
label,
|
|
1309
|
+
...noLabel !== void 0 ? { noLabel } : {},
|
|
1310
|
+
type: "GROUP",
|
|
1311
|
+
properties: { ...openGroup !== void 0 ? { openGroup } : {} }
|
|
1312
|
+
});
|
|
1313
|
+
return {
|
|
1314
|
+
type: "GROUP",
|
|
1315
|
+
code,
|
|
1316
|
+
label,
|
|
1317
|
+
...noLabel !== void 0 ? { noLabel } : {},
|
|
1318
|
+
...openGroup !== void 0 ? { openGroup } : {},
|
|
1319
|
+
layout
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
case "SUBTABLE": {
|
|
1323
|
+
const code = FieldCode.create(String(raw.code));
|
|
1324
|
+
const label = String(raw.label ?? "");
|
|
1325
|
+
const noLabel = raw.noLabel !== void 0 ? raw.noLabel : void 0;
|
|
1326
|
+
const elements = (raw.fields ?? []).map(parseLayoutElement);
|
|
1327
|
+
const subFields = /* @__PURE__ */ new Map();
|
|
1328
|
+
collectFieldsFromElements(elements, subFields);
|
|
1329
|
+
for (const [subCode, subDef] of subFields) addFieldToMap(fieldMap, subCode, subDef);
|
|
1330
|
+
addFieldToMap(fieldMap, code, {
|
|
1331
|
+
code,
|
|
1332
|
+
label,
|
|
1333
|
+
...noLabel !== void 0 ? { noLabel } : {},
|
|
1334
|
+
type: "SUBTABLE",
|
|
1335
|
+
properties: { fields: subFields }
|
|
1336
|
+
});
|
|
1337
|
+
return {
|
|
1338
|
+
type: "SUBTABLE",
|
|
1339
|
+
code,
|
|
1340
|
+
label,
|
|
1341
|
+
...noLabel !== void 0 ? { noLabel } : {},
|
|
1342
|
+
fields: elements
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
default: throw new BusinessRuleError(FormSchemaErrorCode.InvalidLayoutStructure, `Unknown layout item type: "${type}"`);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
function addFieldToMap(fieldMap, code, definition) {
|
|
1349
|
+
if (fieldMap.has(code)) throw new BusinessRuleError(FormSchemaErrorCode.DuplicateFieldCode, `Duplicate field code: "${code}"`);
|
|
1350
|
+
fieldMap.set(code, definition);
|
|
1351
|
+
}
|
|
1352
|
+
function collectFieldsFromElements(elements, fieldMap) {
|
|
1353
|
+
for (const element of elements) if ("field" in element) addFieldToMap(fieldMap, element.field.code, element.field);
|
|
1354
|
+
}
|
|
1355
|
+
const SchemaParser = { parse: (rawText) => {
|
|
1356
|
+
if (rawText.trim().length === 0) throw new BusinessRuleError(FormSchemaErrorCode.EmptySchemaText, "Schema text cannot be empty");
|
|
1357
|
+
let parsed;
|
|
1358
|
+
try {
|
|
1359
|
+
parsed = parse(rawText);
|
|
1360
|
+
} catch {
|
|
1361
|
+
throw new BusinessRuleError(FormSchemaErrorCode.InvalidSchemaJson, "Schema text is not valid YAML/JSON");
|
|
1362
|
+
}
|
|
1363
|
+
if (typeof parsed !== "object" || parsed === null) throw new BusinessRuleError(FormSchemaErrorCode.InvalidSchemaStructure, "Schema must be an object");
|
|
1364
|
+
const obj = parsed;
|
|
1365
|
+
if ("fields" in obj && !("layout" in obj)) throw new BusinessRuleError(FormSchemaErrorCode.InvalidSchemaStructure, "\"fields\" キーが検出されました。スキーマフォーマットが変更されています。「現在の設定を取り込む」で新しいフォーマットのスキーマを生成してください。");
|
|
1366
|
+
if (!("layout" in obj) || !Array.isArray(obj.layout)) throw new BusinessRuleError(FormSchemaErrorCode.InvalidLayoutStructure, "Schema must have a \"layout\" array");
|
|
1367
|
+
const rawLayout = obj.layout;
|
|
1368
|
+
const fieldMap = /* @__PURE__ */ new Map();
|
|
1369
|
+
const layout = [];
|
|
1370
|
+
for (const rawItem of rawLayout) layout.push(parseLayoutItem(rawItem, fieldMap));
|
|
1371
|
+
return {
|
|
1372
|
+
fields: fieldMap,
|
|
1373
|
+
layout
|
|
1374
|
+
};
|
|
1375
|
+
} };
|
|
1376
|
+
|
|
1377
|
+
//#endregion
|
|
1378
|
+
//#region src/core/application/formSchema/parseSchema.ts
|
|
1379
|
+
function parseSchemaText(rawText) {
|
|
1380
|
+
try {
|
|
1381
|
+
return SchemaParser.parse(rawText);
|
|
1382
|
+
} catch (error) {
|
|
1383
|
+
if (isBusinessRuleError(error)) throw new ValidationError(ValidationErrorCode.InvalidInput, error.message, error);
|
|
1384
|
+
throw error;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
//#endregion
|
|
1389
|
+
//#region src/core/application/formSchema/detectDiff.ts
|
|
1390
|
+
function toFieldDto(field) {
|
|
1391
|
+
return {
|
|
1392
|
+
code: field.code,
|
|
1393
|
+
type: field.type,
|
|
1394
|
+
label: field.label,
|
|
1395
|
+
properties: field.properties
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
async function detectDiff({ container }) {
|
|
1399
|
+
const schema = parseSchemaText(await container.schemaStorage.get());
|
|
1400
|
+
const [currentFields, currentLayout] = await Promise.all([container.formConfigurator.getFields(), container.formConfigurator.getLayout()]);
|
|
1401
|
+
const diff = DiffDetector.detect(schema, currentFields);
|
|
1402
|
+
const enrichedCurrentLayout = enrichLayoutWithFields(currentLayout, currentFields);
|
|
1403
|
+
const hasLayoutChanges = DiffDetector.detectLayoutChanges(schema.layout, enrichedCurrentLayout);
|
|
1404
|
+
return {
|
|
1405
|
+
entries: diff.entries.map((entry) => ({
|
|
1406
|
+
type: entry.type,
|
|
1407
|
+
fieldCode: entry.fieldCode,
|
|
1408
|
+
fieldLabel: entry.fieldLabel,
|
|
1409
|
+
details: entry.details,
|
|
1410
|
+
...entry.before ? { before: toFieldDto(entry.before) } : {},
|
|
1411
|
+
...entry.after ? { after: toFieldDto(entry.after) } : {}
|
|
1412
|
+
})),
|
|
1413
|
+
schemaFields: Array.from(schema.fields.entries()).map(([code, def]) => ({
|
|
1414
|
+
fieldCode: code,
|
|
1415
|
+
fieldLabel: def.label,
|
|
1416
|
+
fieldType: def.type
|
|
1417
|
+
})),
|
|
1418
|
+
summary: diff.summary,
|
|
1419
|
+
isEmpty: diff.isEmpty && !hasLayoutChanges,
|
|
1420
|
+
hasLayoutChanges
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
//#endregion
|
|
1425
|
+
//#region src/core/application/formSchema/deployApp.ts
|
|
1426
|
+
async function deployApp({ container }) {
|
|
1427
|
+
await container.appDeployer.deploy();
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
//#endregion
|
|
1431
|
+
//#region src/cli/output.ts
|
|
1432
|
+
function printDiffResult(result) {
|
|
1433
|
+
const { summary } = result;
|
|
1434
|
+
if (result.isEmpty) {
|
|
1435
|
+
p.log.info("No changes detected.");
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
const summaryParts = [
|
|
1439
|
+
summary.added > 0 ? pc.green(`+${summary.added} added`) : null,
|
|
1440
|
+
summary.modified > 0 ? pc.yellow(`~${summary.modified} modified`) : null,
|
|
1441
|
+
summary.deleted > 0 ? pc.red(`-${summary.deleted} deleted`) : null
|
|
1442
|
+
].filter(Boolean).join(pc.dim(" | "));
|
|
1443
|
+
p.log.info(`Changes: ${summaryParts}`);
|
|
1444
|
+
if (result.hasLayoutChanges) p.log.info("Layout changes detected.");
|
|
1445
|
+
const lines = result.entries.map((entry) => {
|
|
1446
|
+
const colorize = entry.type === "added" ? pc.green : entry.type === "deleted" ? pc.red : pc.yellow;
|
|
1447
|
+
return `${colorize(entry.type === "added" ? "+" : entry.type === "deleted" ? "-" : "~")} ${pc.dim("[")}${colorize(entry.fieldCode)}${pc.dim("]")} ${entry.fieldLabel}${pc.dim(":")} ${entry.details}`;
|
|
1448
|
+
});
|
|
1449
|
+
p.note(lines.join("\n"), "Diff Details");
|
|
1450
|
+
}
|
|
1451
|
+
async function promptDeploy(container) {
|
|
1452
|
+
const shouldDeploy = await p.confirm({ message: "運用環境に反映しますか?" });
|
|
1453
|
+
if (p.isCancel(shouldDeploy) || !shouldDeploy) {
|
|
1454
|
+
p.log.warn("テスト環境に反映済みですが、運用環境には反映されていません。");
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
const ds = p.spinner();
|
|
1458
|
+
ds.start("運用環境に反映しています...");
|
|
1459
|
+
await deployApp({ container });
|
|
1460
|
+
ds.stop("運用環境への反映が完了しました。");
|
|
1461
|
+
p.log.success("運用環境への反映が完了しました。");
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
//#endregion
|
|
1465
|
+
//#region src/cli/commands/diff.ts
|
|
1466
|
+
var diff_default = define({
|
|
1467
|
+
name: "diff",
|
|
1468
|
+
description: "Detect differences between schema file and current kintone form",
|
|
1469
|
+
args: { ...kintoneArgs },
|
|
1470
|
+
run: async (ctx) => {
|
|
1471
|
+
try {
|
|
1472
|
+
const container = createCliContainer(resolveConfig(ctx.values));
|
|
1473
|
+
const s = p.spinner();
|
|
1474
|
+
s.start("Fetching form schema...");
|
|
1475
|
+
const result = await detectDiff({ container });
|
|
1476
|
+
s.stop("Form schema fetched.");
|
|
1477
|
+
printDiffResult(result);
|
|
1478
|
+
} catch (error) {
|
|
1479
|
+
handleCliError(error);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
});
|
|
1483
|
+
|
|
1484
|
+
//#endregion
|
|
1485
|
+
//#region src/cli/commands/dump.ts
|
|
1486
|
+
var dump_default = define({
|
|
1487
|
+
name: "dump",
|
|
1488
|
+
description: "Dump current kintone form fields and layout as JSON",
|
|
1489
|
+
args: { ...kintoneArgs },
|
|
1490
|
+
run: async (ctx) => {
|
|
1491
|
+
try {
|
|
1492
|
+
const config = resolveConfig(ctx.values);
|
|
1493
|
+
const client = new KintoneRestAPIClient({
|
|
1494
|
+
baseUrl: config.baseUrl,
|
|
1495
|
+
auth: {
|
|
1496
|
+
username: config.username,
|
|
1497
|
+
password: config.password
|
|
1498
|
+
}
|
|
1499
|
+
});
|
|
1500
|
+
const s = p.spinner();
|
|
1501
|
+
s.start("Fetching form fields and layout...");
|
|
1502
|
+
const [fields, layout] = await Promise.all([client.app.getFormFields({ app: config.appId }), client.app.getFormLayout({ app: config.appId })]);
|
|
1503
|
+
s.stop("Form data fetched.");
|
|
1504
|
+
await Promise.all([writeFile("fields.json", JSON.stringify(fields, null, 2)), writeFile("layout.json", JSON.stringify(layout, null, 2))]);
|
|
1505
|
+
p.log.success(`Saved ${pc.cyan("fields.json")} and ${pc.cyan("layout.json")}`);
|
|
1506
|
+
} catch (error) {
|
|
1507
|
+
handleCliError(error);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
});
|
|
1511
|
+
|
|
1512
|
+
//#endregion
|
|
1513
|
+
//#region src/core/application/formSchema/executeMigration.ts
|
|
1514
|
+
async function executeMigration({ container }) {
|
|
1515
|
+
const schema = parseSchemaText(await container.schemaStorage.get());
|
|
1516
|
+
const [currentFields, currentLayout] = await Promise.all([container.formConfigurator.getFields(), container.formConfigurator.getLayout()]);
|
|
1517
|
+
const diff = DiffDetector.detect(schema, currentFields);
|
|
1518
|
+
const enrichedCurrentLayout = enrichLayoutWithFields(currentLayout, currentFields);
|
|
1519
|
+
const hasLayoutChanges = DiffDetector.detectLayoutChanges(schema.layout, enrichedCurrentLayout);
|
|
1520
|
+
if (diff.isEmpty && !hasLayoutChanges) return;
|
|
1521
|
+
const subtableInnerCodes = collectSubtableInnerFieldCodes(schema.fields);
|
|
1522
|
+
const added = diff.entries.filter((e) => e.type === "added");
|
|
1523
|
+
const modified = diff.entries.filter((e) => e.type === "modified");
|
|
1524
|
+
const deleted = diff.entries.filter((e) => e.type === "deleted");
|
|
1525
|
+
if (added.length > 0) {
|
|
1526
|
+
const fields = added.filter((e) => e.after !== void 0).filter((e) => !subtableInnerCodes.has(e.fieldCode)).map((e) => e.after);
|
|
1527
|
+
if (fields.length > 0) await container.formConfigurator.addFields(fields);
|
|
1528
|
+
}
|
|
1529
|
+
if (modified.length > 0) {
|
|
1530
|
+
const fields = modified.filter((e) => e.after !== void 0).filter((e) => !subtableInnerCodes.has(e.fieldCode)).map((e) => e.after);
|
|
1531
|
+
if (fields.length > 0) await container.formConfigurator.updateFields(fields);
|
|
1532
|
+
}
|
|
1533
|
+
if (deleted.length > 0) {
|
|
1534
|
+
const currentSubtableInnerCodes = collectSubtableInnerFieldCodes(currentFields);
|
|
1535
|
+
const fieldCodes = deleted.filter((e) => !currentSubtableInnerCodes.has(e.fieldCode)).map((e) => e.fieldCode);
|
|
1536
|
+
if (fieldCodes.length > 0) await container.formConfigurator.deleteFields(fieldCodes);
|
|
1537
|
+
}
|
|
1538
|
+
if (hasLayoutChanges) await container.formConfigurator.updateLayout(schema.layout);
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
//#endregion
|
|
1542
|
+
//#region src/cli/commands/migrate.ts
|
|
1543
|
+
var migrate_default = define({
|
|
1544
|
+
name: "migrate",
|
|
1545
|
+
description: "Apply schema changes to kintone form",
|
|
1546
|
+
args: { ...kintoneArgs },
|
|
1547
|
+
run: async (ctx) => {
|
|
1548
|
+
try {
|
|
1549
|
+
const container = createCliContainer(resolveConfig(ctx.values));
|
|
1550
|
+
const s = p.spinner();
|
|
1551
|
+
s.start("Detecting changes...");
|
|
1552
|
+
const result = await detectDiff({ container });
|
|
1553
|
+
s.stop("Changes detected.");
|
|
1554
|
+
if (result.isEmpty) {
|
|
1555
|
+
p.log.success("No changes detected. Form is up to date.");
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
printDiffResult(result);
|
|
1559
|
+
const shouldContinue = await p.confirm({ message: "Apply these changes?" });
|
|
1560
|
+
if (p.isCancel(shouldContinue) || !shouldContinue) {
|
|
1561
|
+
p.cancel("Migration cancelled.");
|
|
1562
|
+
process.exit(0);
|
|
1563
|
+
}
|
|
1564
|
+
const ms = p.spinner();
|
|
1565
|
+
ms.start("Applying migration...");
|
|
1566
|
+
await executeMigration({ container });
|
|
1567
|
+
ms.stop("Migration applied.");
|
|
1568
|
+
p.log.success("Migration completed successfully.");
|
|
1569
|
+
await promptDeploy(container);
|
|
1570
|
+
} catch (error) {
|
|
1571
|
+
handleCliError(error);
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
});
|
|
1575
|
+
|
|
1576
|
+
//#endregion
|
|
1577
|
+
//#region src/core/application/formSchema/forceOverrideForm.ts
|
|
1578
|
+
async function forceOverrideForm({ container }) {
|
|
1579
|
+
const schema = parseSchemaText(await container.schemaStorage.get());
|
|
1580
|
+
const currentFields = await container.formConfigurator.getFields();
|
|
1581
|
+
const subtableInnerCodes = collectSubtableInnerFieldCodes(schema.fields);
|
|
1582
|
+
const toAdd = [];
|
|
1583
|
+
const toUpdate = [];
|
|
1584
|
+
const toDelete = [];
|
|
1585
|
+
for (const [fieldCode, schemaDef] of schema.fields) {
|
|
1586
|
+
if (subtableInnerCodes.has(fieldCode)) continue;
|
|
1587
|
+
if (currentFields.has(fieldCode)) toUpdate.push(schemaDef);
|
|
1588
|
+
else toAdd.push(schemaDef);
|
|
1589
|
+
}
|
|
1590
|
+
const currentSubtableInnerCodes = collectSubtableInnerFieldCodes(currentFields);
|
|
1591
|
+
for (const fieldCode of currentFields.keys()) {
|
|
1592
|
+
if (currentSubtableInnerCodes.has(fieldCode)) continue;
|
|
1593
|
+
if (!schema.fields.has(fieldCode)) toDelete.push(fieldCode);
|
|
1594
|
+
}
|
|
1595
|
+
if (toAdd.length > 0) await container.formConfigurator.addFields(toAdd);
|
|
1596
|
+
if (toUpdate.length > 0) await container.formConfigurator.updateFields(toUpdate);
|
|
1597
|
+
if (toDelete.length > 0) await container.formConfigurator.deleteFields(toDelete);
|
|
1598
|
+
await container.formConfigurator.updateLayout(schema.layout);
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
//#endregion
|
|
1602
|
+
//#region src/cli/commands/override.ts
|
|
1603
|
+
var override_default = define({
|
|
1604
|
+
name: "override",
|
|
1605
|
+
description: "Force override kintone form with declared schema",
|
|
1606
|
+
args: { ...kintoneArgs },
|
|
1607
|
+
run: async (ctx) => {
|
|
1608
|
+
try {
|
|
1609
|
+
const container = createCliContainer(resolveConfig(ctx.values));
|
|
1610
|
+
p.log.warn(`${pc.bold(pc.red("WARNING:"))} This will replace the entire form with the declared schema.`);
|
|
1611
|
+
p.log.warn("Fields not defined in the schema will be deleted.");
|
|
1612
|
+
const shouldContinue = await p.confirm({ message: "Are you sure you want to force override?" });
|
|
1613
|
+
if (p.isCancel(shouldContinue) || !shouldContinue) {
|
|
1614
|
+
p.cancel("Force override cancelled.");
|
|
1615
|
+
process.exit(0);
|
|
1616
|
+
}
|
|
1617
|
+
const s = p.spinner();
|
|
1618
|
+
s.start("Force overriding form...");
|
|
1619
|
+
await forceOverrideForm({ container });
|
|
1620
|
+
s.stop("Force override applied.");
|
|
1621
|
+
p.log.success("Force override completed successfully.");
|
|
1622
|
+
await promptDeploy(container);
|
|
1623
|
+
} catch (error) {
|
|
1624
|
+
handleCliError(error);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
});
|
|
1628
|
+
|
|
1629
|
+
//#endregion
|
|
1630
|
+
//#region src/cli/index.ts
|
|
1631
|
+
function loadVersion() {
|
|
1632
|
+
try {
|
|
1633
|
+
return createRequire(import.meta.url)("../package.json").version;
|
|
1634
|
+
} catch {
|
|
1635
|
+
return "0.0.0-dev";
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
const main = define({
|
|
1639
|
+
name: "kintone-migrator",
|
|
1640
|
+
description: "kintone form schema migration tool",
|
|
1641
|
+
run: () => {}
|
|
1642
|
+
});
|
|
1643
|
+
await cli(process.argv.slice(2), main, {
|
|
1644
|
+
name: "kintone-migrator",
|
|
1645
|
+
version: loadVersion(),
|
|
1646
|
+
subCommands: {
|
|
1647
|
+
diff: diff_default,
|
|
1648
|
+
migrate: migrate_default,
|
|
1649
|
+
override: override_default,
|
|
1650
|
+
capture: capture_default,
|
|
1651
|
+
dump: dump_default
|
|
1652
|
+
}
|
|
1653
|
+
});
|
|
1654
|
+
|
|
1655
|
+
//#endregion
|
|
1656
|
+
export { };
|