@carthooks/arcubase-cli 0.1.2 → 0.1.3

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.
Files changed (95) hide show
  1. package/bundle/arcubase-admin.mjs +907 -10
  2. package/bundle/arcubase.mjs +907 -10
  3. package/dist/runtime/command_registry.d.ts +8 -0
  4. package/dist/runtime/command_registry.d.ts.map +1 -1
  5. package/dist/runtime/command_registry.js +35 -0
  6. package/dist/runtime/entity_save_schema.d.ts +15 -0
  7. package/dist/runtime/entity_save_schema.d.ts.map +1 -0
  8. package/dist/runtime/entity_save_schema.js +641 -0
  9. package/dist/runtime/errors.d.ts +10 -0
  10. package/dist/runtime/errors.d.ts.map +1 -1
  11. package/dist/runtime/execute.d.ts.map +1 -1
  12. package/dist/runtime/execute.js +89 -10
  13. package/dist/runtime/zod_registry.d.ts +6 -0
  14. package/dist/runtime/zod_registry.d.ts.map +1 -1
  15. package/dist/runtime/zod_registry.js +133 -0
  16. package/dist/tests/command_registry.test.js +5 -0
  17. package/dist/tests/entity_save_schema.test.d.ts +2 -0
  18. package/dist/tests/entity_save_schema.test.d.ts.map +1 -0
  19. package/dist/tests/entity_save_schema.test.js +221 -0
  20. package/dist/tests/execute_validation.test.js +450 -0
  21. package/dist/tests/help.test.js +33 -0
  22. package/package.json +1 -1
  23. package/sdk-dist/docs/runtime-reference/README.md +177 -0
  24. package/sdk-dist/docs/runtime-reference/entity-schema/README.md +120 -0
  25. package/sdk-dist/docs/runtime-reference/entity-schema/address.md +34 -0
  26. package/sdk-dist/docs/runtime-reference/entity-schema/boolean.md +37 -0
  27. package/sdk-dist/docs/runtime-reference/entity-schema/button.md +47 -0
  28. package/sdk-dist/docs/runtime-reference/entity-schema/checkbox.md +61 -0
  29. package/sdk-dist/docs/runtime-reference/entity-schema/datasets.md +35 -0
  30. package/sdk-dist/docs/runtime-reference/entity-schema/datetime.md +40 -0
  31. package/sdk-dist/docs/runtime-reference/entity-schema/department.md +33 -0
  32. package/sdk-dist/docs/runtime-reference/entity-schema/departments.md +34 -0
  33. package/sdk-dist/docs/runtime-reference/entity-schema/divider.md +39 -0
  34. package/sdk-dist/docs/runtime-reference/entity-schema/file.md +44 -0
  35. package/sdk-dist/docs/runtime-reference/entity-schema/formula.md +44 -0
  36. package/sdk-dist/docs/runtime-reference/entity-schema/image.md +43 -0
  37. package/sdk-dist/docs/runtime-reference/entity-schema/linkto.md +67 -0
  38. package/sdk-dist/docs/runtime-reference/entity-schema/location.md +32 -0
  39. package/sdk-dist/docs/runtime-reference/entity-schema/lookup.md +41 -0
  40. package/sdk-dist/docs/runtime-reference/entity-schema/member.md +37 -0
  41. package/sdk-dist/docs/runtime-reference/entity-schema/members.md +31 -0
  42. package/sdk-dist/docs/runtime-reference/entity-schema/number.md +70 -0
  43. package/sdk-dist/docs/runtime-reference/entity-schema/query.md +50 -0
  44. package/sdk-dist/docs/runtime-reference/entity-schema/radio.md +53 -0
  45. package/sdk-dist/docs/runtime-reference/entity-schema/relation.md +55 -0
  46. package/sdk-dist/docs/runtime-reference/entity-schema/relationfield.md +38 -0
  47. package/sdk-dist/docs/runtime-reference/entity-schema/rollup.md +54 -0
  48. package/sdk-dist/docs/runtime-reference/entity-schema/select.md +63 -0
  49. package/sdk-dist/docs/runtime-reference/entity-schema/selects.md +51 -0
  50. package/sdk-dist/docs/runtime-reference/entity-schema/serialnumber.md +48 -0
  51. package/sdk-dist/docs/runtime-reference/entity-schema/signature.md +31 -0
  52. package/sdk-dist/docs/runtime-reference/entity-schema/status.md +70 -0
  53. package/sdk-dist/docs/runtime-reference/entity-schema/subform.md +74 -0
  54. package/sdk-dist/docs/runtime-reference/entity-schema/tags.md +19 -0
  55. package/sdk-dist/docs/runtime-reference/entity-schema/text.md +76 -0
  56. package/sdk-dist/docs/runtime-reference/entity-schema/textarea.md +47 -0
  57. package/sdk-dist/docs/runtime-reference/entity-schema/workflow_status.md +17 -0
  58. package/sdk-dist/docs/runtime-reference/entity-schema.md +197 -0
  59. package/sdk-dist/docs/runtime-reference/examples/README.md +51 -0
  60. package/sdk-dist/docs/runtime-reference/examples/crm-01/README.md +20 -0
  61. package/sdk-dist/docs/runtime-reference/examples/mes-01/README.md +21 -0
  62. package/sdk-dist/docs/runtime-reference/examples/oms-01/README.md +164 -0
  63. package/sdk-dist/docs/runtime-reference/examples/oms-01/app-overview.md +79 -0
  64. package/sdk-dist/docs/runtime-reference/examples/oms-01/create-app.json +3 -0
  65. package/sdk-dist/docs/runtime-reference/examples/oms-01/goods-receipt.create-entity.json +3 -0
  66. package/sdk-dist/docs/runtime-reference/examples/oms-01/goods-receipt.schema.json +159 -0
  67. package/sdk-dist/docs/runtime-reference/examples/oms-01/item-master.create-entity.json +3 -0
  68. package/sdk-dist/docs/runtime-reference/examples/oms-01/item-master.schema.json +179 -0
  69. package/sdk-dist/docs/runtime-reference/examples/oms-01/picking-list.create-entity.json +3 -0
  70. package/sdk-dist/docs/runtime-reference/examples/oms-01/picking-list.schema.json +168 -0
  71. package/sdk-dist/docs/runtime-reference/examples/oms-01/purchase-order.create-entity.json +3 -0
  72. package/sdk-dist/docs/runtime-reference/examples/oms-01/purchase-order.schema.json +417 -0
  73. package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.bulk-update.json +22 -0
  74. package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.create-entity.json +3 -0
  75. package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.query.json +10 -0
  76. package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.row.insert.json +28 -0
  77. package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.schema.json +398 -0
  78. package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.selection.condition.json +13 -0
  79. package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.selection.ids.json +7 -0
  80. package/sdk-dist/docs/runtime-reference/examples/oms-01/stock-issue.create-entity.json +3 -0
  81. package/sdk-dist/docs/runtime-reference/examples/oms-01/stock-issue.schema.json +137 -0
  82. package/sdk-dist/docs/runtime-reference/examples/wms-01/README.md +21 -0
  83. package/sdk-dist/docs/runtime-reference/row-crud.md +186 -0
  84. package/sdk-dist/docs/runtime-reference/search-and-bulk-actions.md +494 -0
  85. package/sdk-dist/docs/runtime-reference/table-lifecycle.md +123 -0
  86. package/sdk-dist/docs/runtime-reference/workflow/README.md +19 -0
  87. package/src/runtime/command_registry.ts +47 -0
  88. package/src/runtime/entity_save_schema.ts +708 -0
  89. package/src/runtime/errors.ts +11 -0
  90. package/src/runtime/execute.ts +123 -10
  91. package/src/runtime/zod_registry.ts +154 -0
  92. package/src/tests/command_registry.test.ts +6 -0
  93. package/src/tests/entity_save_schema.test.ts +236 -0
  94. package/src/tests/execute_validation.test.ts +598 -0
  95. package/src/tests/help.test.ts +39 -0
@@ -0,0 +1,708 @@
1
+ import { z } from 'zod'
2
+ import { EntityOptionsSchema, EntitySerialUpdaterVOSchema } from '../generated/zod_registry.generated.js'
3
+
4
+ const simpleFieldTypes = [
5
+ 'text',
6
+ 'textarea',
7
+ 'number',
8
+ 'datetime',
9
+ 'boolean',
10
+ 'button',
11
+ 'address',
12
+ 'datasets',
13
+ 'department',
14
+ 'departments',
15
+ 'divider',
16
+ 'file',
17
+ 'image',
18
+ 'location',
19
+ 'member',
20
+ 'members',
21
+ 'signature',
22
+ 'tags',
23
+ 'workflow_status',
24
+ 'mobile',
25
+ ] as const
26
+
27
+ const enumFieldTypes = ['select', 'selects', 'radio', 'checkbox', 'status'] as const
28
+ const relationFieldTypes = ['linkto', 'relation', 'query'] as const
29
+ const strictFieldTypes = new Set<string>([
30
+ ...simpleFieldTypes,
31
+ ...enumFieldTypes,
32
+ ...relationFieldTypes,
33
+ 'formula',
34
+ 'lookup',
35
+ 'relationfield',
36
+ 'rollup',
37
+ 'serialnumber',
38
+ 'subform',
39
+ ])
40
+
41
+ const relationTypeSchema = z.union([z.literal('entity'), z.literal('aggregation')])
42
+ const linkModeSchema = z.union([z.literal('manual'), z.literal('auto')])
43
+ const linkStructureSchema = z.union([z.literal('single'), z.literal('multiple')])
44
+
45
+ const enumItemSchema = z.object({
46
+ key: z.union([z.number(), z.string()]).optional(),
47
+ value: z.string().min(1),
48
+ }).passthrough()
49
+
50
+ const statusItemSchema = z.object({
51
+ key: z.union([z.number(), z.string()]).optional(),
52
+ code: z.union([z.number(), z.string()]),
53
+ value: z.string().min(1),
54
+ }).passthrough()
55
+
56
+ const sortConfigSchema = z.object({
57
+ column: z.string().min(1),
58
+ order: z.union([z.literal('asc'), z.literal('desc')]),
59
+ }).strict()
60
+
61
+ const relationOptionsSchema = z.object({
62
+ entity_id: z.number().int().positive(),
63
+ app_id: z.number().int().positive(),
64
+ type: relationTypeSchema,
65
+ show_fields: z.array(z.number().int().positive()).optional(),
66
+ condition: z.record(z.string(), z.any()).optional(),
67
+ sorts: z.array(sortConfigSchema).optional(),
68
+ queryFields: z.array(z.number().int().positive()).optional(),
69
+ subform_id: z.number().int().positive().optional(),
70
+ }).strict()
71
+
72
+ const lookupRelationSchema = z.object({
73
+ local_linkto_field_id: z.number().int().min(1001),
74
+ target_field_id: z.number().int().min(1001),
75
+ target_field_type: z.string().min(1),
76
+ }).strict()
77
+
78
+ const relationFieldRelationSchema = z.object({
79
+ app_id: z.number().int().positive(),
80
+ entity_id: z.number().int().positive(),
81
+ type: relationTypeSchema.optional(),
82
+ show_fields: z.array(z.number().int().positive()).optional(),
83
+ condition: z.record(z.string(), z.any()).optional(),
84
+ sorts: z.array(sortConfigSchema).optional(),
85
+ queryFields: z.array(z.number().int().positive()).optional(),
86
+ subform_id: z.number().int().positive().optional(),
87
+ }).strict()
88
+
89
+ const formulaSchema = z.object({
90
+ code: z.string().min(1),
91
+ jsonlogic: z.record(z.string(), z.any()),
92
+ }).strict()
93
+
94
+ const serialPartBuildinStringSchema = z.object({
95
+ mode: z.literal('buildin'),
96
+ type: z.literal('string'),
97
+ options: z.object({
98
+ value: z.string(),
99
+ }).strict(),
100
+ }).strict()
101
+
102
+ const serialPartBuildinDateSchema = z.object({
103
+ mode: z.literal('buildin'),
104
+ type: z.literal('date'),
105
+ options: z.object({
106
+ format: z.string().optional(),
107
+ perdefined: z.boolean().optional(),
108
+ perdefinedFormat: z.string().optional(),
109
+ }).strict().refine((value) => Boolean(value.format || value.perdefinedFormat), {
110
+ message: 'date serial part requires format or perdefinedFormat',
111
+ }),
112
+ }).strict()
113
+
114
+ const serialPartBuildinSerialSchema = z.object({
115
+ mode: z.literal('buildin'),
116
+ type: z.literal('serial'),
117
+ options: z.object({
118
+ length: z.number().int().min(1),
119
+ lenfixed: z.boolean().optional(),
120
+ random: z.boolean().optional(),
121
+ reset: z.string().optional(),
122
+ }).strict(),
123
+ }).strict()
124
+
125
+ const serialPartFieldSchema = z.object({
126
+ mode: z.literal('field'),
127
+ field_id: z.number().int().min(1001),
128
+ }).passthrough()
129
+
130
+ const serialPartSchema = z.union([
131
+ serialPartBuildinStringSchema,
132
+ serialPartBuildinDateSchema,
133
+ serialPartBuildinSerialSchema,
134
+ serialPartFieldSchema,
135
+ ])
136
+
137
+ const fieldCommonShape = {
138
+ id: z.number().int().min(1001),
139
+ label: z.string().min(1),
140
+ required: z.boolean(),
141
+ unique: z.boolean(),
142
+ editable: z.boolean(),
143
+ visible: z.boolean(),
144
+ show_label: z.boolean(),
145
+ scannable: z.boolean(),
146
+ default_value_mode: z.number(),
147
+ description: z.string(),
148
+ value: z.any(),
149
+ number_decimal: z.number().int().min(0),
150
+ depends: z.array(z.number().int().min(1001)),
151
+ key: z.string(),
152
+ code_index: z.boolean(),
153
+ transfers: z.array(z.any()).nullable(),
154
+ }
155
+
156
+ function isRecord(value: unknown): value is Record<string, unknown> {
157
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
158
+ }
159
+
160
+ function cloneJSON<T>(value: T): T {
161
+ return JSON.parse(JSON.stringify(value)) as T
162
+ }
163
+
164
+ function makeSimpleFieldSchema(type: string) {
165
+ return z.object({
166
+ ...fieldCommonShape,
167
+ type: z.literal(type),
168
+ options: z.record(z.string(), z.any()).nullable(),
169
+ children: z.null(),
170
+ }).strict()
171
+ }
172
+
173
+ function makeEnumFieldSchema(type: typeof enumFieldTypes[number]) {
174
+ const itemSchema = type === 'status' ? statusItemSchema : enumItemSchema
175
+ return z.object({
176
+ ...fieldCommonShape,
177
+ type: z.literal(type),
178
+ options: z.object({
179
+ options: z.object({
180
+ items: z.array(itemSchema).min(1),
181
+ }).passthrough(),
182
+ }).passthrough(),
183
+ children: z.null(),
184
+ }).strict()
185
+ }
186
+
187
+ const simpleFieldSchemas = simpleFieldTypes.map((type) => makeSimpleFieldSchema(type))
188
+ const enumFieldSchemas = enumFieldTypes.map((type) => makeEnumFieldSchema(type))
189
+
190
+ const formulaFieldSchema = z.object({
191
+ ...fieldCommonShape,
192
+ type: z.literal('formula'),
193
+ options: z.object({
194
+ formula: formulaSchema,
195
+ }).passthrough(),
196
+ children: z.null(),
197
+ }).strict()
198
+
199
+ const relationFieldSchemas = relationFieldTypes.map((type) =>
200
+ z.object({
201
+ ...fieldCommonShape,
202
+ type: z.literal(type),
203
+ options: z.object({
204
+ relation: relationOptionsSchema,
205
+ mode: linkModeSchema.optional(),
206
+ structure: linkStructureSchema.optional(),
207
+ multiple: z.boolean().optional(),
208
+ backfill: z.array(z.object({
209
+ id: z.number().int().min(1001),
210
+ local_id: z.number().int().min(1001),
211
+ }).strict()).optional(),
212
+ addValue: z.boolean().optional(),
213
+ placeholder: z.string().optional(),
214
+ }).passthrough(),
215
+ children: z.null(),
216
+ }).strict(),
217
+ )
218
+
219
+ const lookupFieldSchema = z.object({
220
+ ...fieldCommonShape,
221
+ type: z.literal('lookup'),
222
+ options: z.object({
223
+ relation: lookupRelationSchema,
224
+ }).passthrough(),
225
+ children: z.null(),
226
+ }).strict()
227
+
228
+ const relationFieldFieldSchema = z.object({
229
+ ...fieldCommonShape,
230
+ type: z.literal('relationfield'),
231
+ options: z.object({
232
+ relation: relationFieldRelationSchema,
233
+ }).passthrough(),
234
+ children: z.null(),
235
+ }).strict()
236
+
237
+ const rollupFieldSchema = z.object({
238
+ ...fieldCommonShape,
239
+ type: z.literal('rollup'),
240
+ options: z.object({
241
+ relation: lookupRelationSchema,
242
+ aggregation_formula: formulaSchema,
243
+ }).passthrough(),
244
+ children: z.null(),
245
+ }).strict()
246
+
247
+ const serialNumberFieldSchema = z.object({
248
+ ...fieldCommonShape,
249
+ type: z.literal('serialnumber'),
250
+ options: z.object({
251
+ parts: z.array(serialPartSchema).min(1),
252
+ }).passthrough(),
253
+ children: z.null(),
254
+ }).strict()
255
+
256
+ const genericFieldSchema = z.object({
257
+ ...fieldCommonShape,
258
+ type: z.string().min(1).refine((value) => !strictFieldTypes.has(value), {
259
+ message: 'known field type must use dedicated schema',
260
+ }),
261
+ options: z.record(z.string(), z.any()).nullable(),
262
+ children: z.null(),
263
+ }).strict()
264
+
265
+ const baseFieldSchemaVariants = [
266
+ ...simpleFieldSchemas,
267
+ ...enumFieldSchemas,
268
+ ...relationFieldSchemas,
269
+ formulaFieldSchema,
270
+ lookupFieldSchema,
271
+ relationFieldFieldSchema,
272
+ rollupFieldSchema,
273
+ serialNumberFieldSchema,
274
+ genericFieldSchema,
275
+ ] as unknown as [z.ZodTypeAny, z.ZodTypeAny, ...z.ZodTypeAny[]]
276
+
277
+ export const BaseFieldSchema: z.ZodTypeAny = z.union(baseFieldSchemaVariants)
278
+
279
+ export const SubformFieldSchema: z.ZodTypeAny = z.lazy(() =>
280
+ z.object({
281
+ ...fieldCommonShape,
282
+ type: z.literal('subform'),
283
+ options: z.record(z.string(), z.any()).nullable(),
284
+ children: z.array(BaseFieldSchema),
285
+ layout: z.array(z.array(z.number().int().min(1001))).optional(),
286
+ }).strict(),
287
+ )
288
+
289
+ export const FieldVOSchema: z.ZodTypeAny = z.lazy(() =>
290
+ z.union([
291
+ SubformFieldSchema,
292
+ BaseFieldSchema,
293
+ ]),
294
+ )
295
+
296
+ function normalizeEntitySaveInput(value: unknown): unknown {
297
+ if (!isRecord(value)) {
298
+ return value
299
+ }
300
+ const entity = cloneJSON(value)
301
+ if (entity.layout === null) {
302
+ entity.layout = []
303
+ }
304
+ if (entity.current_serial === null) {
305
+ delete entity.current_serial
306
+ }
307
+ if (isRecord(entity.options)) {
308
+ if (entity.options.Buttons === null) {
309
+ entity.options.Buttons = []
310
+ }
311
+ if (entity.options.CustomActions === null) {
312
+ entity.options.CustomActions = []
313
+ }
314
+ if (entity.options.FrontendEvents === null) {
315
+ entity.options.FrontendEvents = []
316
+ }
317
+ if (entity.options.misc === null) {
318
+ entity.options.misc = {}
319
+ }
320
+ if (isRecord(entity.options.TitleFormat)) {
321
+ if (entity.options.TitleFormat.Fields === null) {
322
+ entity.options.TitleFormat.Fields = []
323
+ }
324
+ if (entity.options.TitleFormat.Parts === null) {
325
+ entity.options.TitleFormat.Parts = []
326
+ }
327
+ }
328
+ }
329
+ return entity
330
+ }
331
+
332
+ function collectAllFieldIds(fields: ReadonlyArray<any>): number[] {
333
+ const out: number[] = []
334
+ for (const field of fields) {
335
+ out.push(field.id)
336
+ if (field.type === 'subform' && Array.isArray(field.children)) {
337
+ for (const child of field.children) {
338
+ out.push(child.id)
339
+ }
340
+ }
341
+ }
342
+ return out
343
+ }
344
+
345
+ function collectTopLevelFieldIds(fields: ReadonlyArray<any>): number[] {
346
+ return fields.map((field) => field.id)
347
+ }
348
+
349
+ function extractFormulaFieldIds(value: unknown): number[] {
350
+ if (!value || typeof value !== 'string') return []
351
+ const ids = [...value.matchAll(/:(\d+)\]\]/g)].map((match) => Number(match[1]))
352
+ return ids.filter((id) => Number.isInteger(id) && id > 0)
353
+ }
354
+
355
+ const EntitySaveReqVOBaseSchema = z.object({
356
+ app_id: z.number().optional(),
357
+ current_serial: EntitySerialUpdaterVOSchema.optional(),
358
+ field_id_seq: z.number().int(),
359
+ fields: z.array(FieldVOSchema),
360
+ id: z.number().optional(),
361
+ layout: z.array(z.array(z.number().int().min(1001))).optional(),
362
+ name: z.string().optional(),
363
+ options: EntityOptionsSchema.optional(),
364
+ prototypes: z.array(z.number()).optional(),
365
+ schema_version: z.number().optional(),
366
+ workflow_enabled: z.boolean().optional(),
367
+ }).strict().superRefine((entity, ctx) => {
368
+ const fields = entity.fields
369
+ const topLevelIds = new Set<number>(collectTopLevelFieldIds(fields))
370
+ const allIds = collectAllFieldIds(fields)
371
+ const allIdSet = new Set<number>()
372
+ for (const id of allIds) {
373
+ if (allIdSet.has(id)) {
374
+ ctx.addIssue({
375
+ code: z.ZodIssueCode.custom,
376
+ path: ['fields'],
377
+ message: `duplicate field id: ${id}`,
378
+ })
379
+ }
380
+ allIdSet.add(id)
381
+ }
382
+
383
+ const maxFieldId = allIds.length > 0 ? Math.max(...allIds) : 1000
384
+ if (entity.field_id_seq <= maxFieldId) {
385
+ ctx.addIssue({
386
+ code: z.ZodIssueCode.custom,
387
+ path: ['field_id_seq'],
388
+ message: `field_id_seq must be greater than max field id ${maxFieldId}`,
389
+ })
390
+ }
391
+
392
+ for (const [rowIndex, row] of (entity.layout ?? []).entries()) {
393
+ for (const [colIndex, fieldId] of row.entries()) {
394
+ if (!topLevelIds.has(fieldId)) {
395
+ ctx.addIssue({
396
+ code: z.ZodIssueCode.custom,
397
+ path: ['layout', rowIndex, colIndex],
398
+ message: `layout references unknown top-level field id ${fieldId}`,
399
+ })
400
+ }
401
+ }
402
+ }
403
+
404
+ for (const [fieldIndex, field] of fields.entries()) {
405
+ if (field.type !== 'subform' && field.children !== null) {
406
+ ctx.addIssue({
407
+ code: z.ZodIssueCode.custom,
408
+ path: ['fields', fieldIndex, 'children'],
409
+ message: 'only subform field can define children',
410
+ })
411
+ }
412
+ if (field.type === 'subform') {
413
+ const childIds = new Set<number>((field.children ?? []).map((child: any) => child.id))
414
+ for (const [rowIndex, row] of (field.layout ?? []).entries()) {
415
+ for (const [colIndex, fieldId] of row.entries()) {
416
+ if (!childIds.has(fieldId)) {
417
+ ctx.addIssue({
418
+ code: z.ZodIssueCode.custom,
419
+ path: ['fields', fieldIndex, 'layout', rowIndex, colIndex],
420
+ message: `subform layout references non-child field id ${fieldId}`,
421
+ })
422
+ }
423
+ }
424
+ }
425
+ }
426
+
427
+ if (field.type === 'lookup' || field.type === 'rollup') {
428
+ const localLinktoFieldId = field.options?.relation?.local_linkto_field_id
429
+ if (typeof localLinktoFieldId === 'number') {
430
+ const target = fields.find((item: any) => item.id === localLinktoFieldId)
431
+ if (!target || target.type !== 'linkto') {
432
+ ctx.addIssue({
433
+ code: z.ZodIssueCode.custom,
434
+ path: ['fields', fieldIndex, 'options', 'relation', 'local_linkto_field_id'],
435
+ message: 'local_linkto_field_id must reference a linkto field',
436
+ })
437
+ }
438
+ }
439
+ }
440
+
441
+ if (field.type === 'formula' || field.type === 'rollup') {
442
+ const formulaCode = field.type === 'formula'
443
+ ? field.options?.formula?.code
444
+ : field.options?.aggregation_formula?.code
445
+ for (const referencedFieldId of extractFormulaFieldIds(formulaCode)) {
446
+ if (!allIdSet.has(referencedFieldId)) {
447
+ ctx.addIssue({
448
+ code: z.ZodIssueCode.custom,
449
+ path: ['fields', fieldIndex, 'options'],
450
+ message: `formula references unknown field id ${referencedFieldId}`,
451
+ })
452
+ }
453
+ }
454
+ }
455
+ }
456
+ })
457
+
458
+ export const EntitySaveReqVOSchemaOverride: z.ZodTypeAny = z.preprocess(normalizeEntitySaveInput, EntitySaveReqVOBaseSchema)
459
+
460
+ const queryOrderSchema = z.object({
461
+ column: z.string().min(1),
462
+ order: z.string().min(1),
463
+ }).strict()
464
+
465
+ export const AppCreateByTenantsReqVOSchemaOverride: z.ZodTypeAny = z.object({
466
+ icon_color: z.string().optional(),
467
+ icon_name: z.string().optional(),
468
+ name: z.string().min(1).max(20),
469
+ }).strict()
470
+
471
+ export const EntityQueryReqVOSchemaOverride: z.ZodTypeAny = z.object({
472
+ limit: z.number().int().positive().optional(),
473
+ offset: z.number().int().min(0).optional(),
474
+ policy_id: z.string().optional(),
475
+ search: z.record(z.string(), z.any()).optional(),
476
+ sorts: z.array(queryOrderSchema).optional(),
477
+ view_mode: z.string().optional(),
478
+ viewMode: z.never().optional(),
479
+ withSubForm: z.boolean().optional(),
480
+ }).strict()
481
+
482
+ export const InsertReqVOSchemaOverride: z.ZodTypeAny = z.object({
483
+ policy_id: z.string().optional(),
484
+ fields: z.record(z.string(), z.any()),
485
+ }).strict().superRefine((body, ctx) => {
486
+ for (const key of Object.keys(body.fields ?? {})) {
487
+ if (!/^\d+$/.test(key)) {
488
+ ctx.addIssue({
489
+ code: z.ZodIssueCode.custom,
490
+ path: ['fields', key],
491
+ message: 'row fields must use numeric field ids as keys',
492
+ })
493
+ }
494
+ }
495
+ })
496
+
497
+ export const EntityUpdateReqVOSchemaOverride: z.ZodTypeAny = z.object({
498
+ changed_fields: z.array(z.number().int().positive()).min(1),
499
+ data: z.record(z.string(), z.any()),
500
+ policy_id: z.string().optional(),
501
+ tags: z.array(z.string()).optional(),
502
+ fields: z.never().optional(),
503
+ }).strict().superRefine((body, ctx) => {
504
+ const dataFieldIds = new Set<number>()
505
+ for (const key of Object.keys(body.data ?? {})) {
506
+ if (!/^\d+$/.test(key)) {
507
+ ctx.addIssue({
508
+ code: z.ZodIssueCode.custom,
509
+ path: ['data', key],
510
+ message: 'row data must use numeric field ids as keys',
511
+ })
512
+ continue
513
+ }
514
+ dataFieldIds.add(Number(key))
515
+ }
516
+ for (const [index, fieldId] of body.changed_fields.entries()) {
517
+ if (!dataFieldIds.has(fieldId)) {
518
+ ctx.addIssue({
519
+ code: z.ZodIssueCode.custom,
520
+ path: ['changed_fields', index],
521
+ message: `changed_fields references ${fieldId}, but data does not contain that field id`,
522
+ })
523
+ }
524
+ }
525
+ })
526
+
527
+ const entitySelectionSortSchema = z.object({
528
+ column: z.string().min(1),
529
+ order: z.string().min(1),
530
+ }).strict()
531
+
532
+ const entitySelectionIdsSchema = z.object({
533
+ type: z.literal('ids'),
534
+ ids: z.array(z.number().int().positive()).min(1),
535
+ length: z.number().int().positive().optional(),
536
+ sorts: z.array(entitySelectionSortSchema).optional(),
537
+ condition: z.record(z.string(), z.any()).optional(),
538
+ }).strict().superRefine((selection, ctx) => {
539
+ if (selection.length !== undefined && selection.length !== selection.ids.length) {
540
+ ctx.addIssue({
541
+ code: z.ZodIssueCode.custom,
542
+ path: ['length'],
543
+ message: 'length must match ids.length when provided',
544
+ })
545
+ }
546
+ })
547
+
548
+ const entitySelectionConditionSchema = z.object({
549
+ type: z.literal('condition'),
550
+ length: z.number().int().positive(),
551
+ condition: z.record(z.string(), z.any()).superRefine((condition, ctx) => {
552
+ if (!Object.keys(condition).length) {
553
+ ctx.addIssue({
554
+ code: z.ZodIssueCode.custom,
555
+ message: 'condition selection requires a non-empty condition object',
556
+ })
557
+ return
558
+ }
559
+ if (condition.selectAll !== true) {
560
+ ctx.addIssue({
561
+ code: z.ZodIssueCode.custom,
562
+ path: ['selectAll'],
563
+ message: 'condition selection must set selectAll to true',
564
+ })
565
+ }
566
+ const filterKeys = Object.keys(condition).filter((key) => key !== 'selectAll')
567
+ if (filterKeys.length === 0) {
568
+ return
569
+ }
570
+ const hasSupportedQueryKey = filterKeys.some((key) => key === 'q' || key === 'tab' || key.startsWith('filter_'))
571
+ if (!hasSupportedQueryKey) {
572
+ ctx.addIssue({
573
+ code: z.ZodIssueCode.custom,
574
+ message: 'condition selection extra keys must use q, tab, or filter_*',
575
+ })
576
+ }
577
+ }),
578
+ sorts: z.array(entitySelectionSortSchema).optional(),
579
+ ids: z.array(z.number().int().positive()).optional(),
580
+ }).strict()
581
+
582
+ const entitySelectionAllSchema = z.object({
583
+ type: z.literal('all'),
584
+ length: z.number().int().positive().optional(),
585
+ ids: z.array(z.number().int().positive()).optional(),
586
+ condition: z.record(z.string(), z.any()).optional(),
587
+ sorts: z.array(entitySelectionSortSchema).optional(),
588
+ }).strict()
589
+
590
+ const entitySelectionSchema = z.union([
591
+ entitySelectionIdsSchema,
592
+ entitySelectionConditionSchema,
593
+ entitySelectionAllSchema,
594
+ ])
595
+
596
+ const entitySelectionLooseSchema = z.union([
597
+ entitySelectionSchema,
598
+ entitySelectionAllSchema,
599
+ ])
600
+
601
+ export const EntitySelectionActionReqVOSchemaOverride: z.ZodTypeAny = z.lazy(() =>
602
+ z.object({
603
+ limit: z.number().int().positive().optional(),
604
+ offset: z.number().int().min(0).optional(),
605
+ policy_id: z.string().optional(),
606
+ selection: entitySelectionSchema,
607
+ }).strict(),
608
+ )
609
+
610
+ const bulkUpdateActionSchema = z.object({
611
+ type: z.string().min(1),
612
+ value: z.any().optional(),
613
+ text_search: z.string().optional(),
614
+ text_replace: z.string().optional(),
615
+ text_append: z.string().optional(),
616
+ text_prepend: z.string().optional(),
617
+ number_add: z.number().optional(),
618
+ number_mul: z.number().optional(),
619
+ number_sub: z.number().optional(),
620
+ datetime_add: z.number().optional(),
621
+ datetime_sub: z.number().optional(),
622
+ }).strict().superRefine((action, ctx) => {
623
+ switch (action.type) {
624
+ case 'set':
625
+ if (!('value' in action)) {
626
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['value'], message: 'set action requires value' })
627
+ }
628
+ break
629
+ case 'text_replace':
630
+ if (!action.text_search) {
631
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['text_search'], message: 'text_replace requires text_search' })
632
+ }
633
+ if (!action.text_replace) {
634
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['text_replace'], message: 'text_replace requires text_replace' })
635
+ }
636
+ break
637
+ case 'text_append':
638
+ if (!action.text_append) {
639
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['text_append'], message: 'text_append requires text_append' })
640
+ }
641
+ break
642
+ case 'text_prepend':
643
+ if (!action.text_prepend) {
644
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['text_prepend'], message: 'text_prepend requires text_prepend' })
645
+ }
646
+ break
647
+ case 'number_add':
648
+ if (typeof action.number_add !== 'number') {
649
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['number_add'], message: 'number_add requires number_add' })
650
+ }
651
+ break
652
+ case 'number_sub':
653
+ if (typeof action.number_sub !== 'number') {
654
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['number_sub'], message: 'number_sub requires number_sub' })
655
+ }
656
+ break
657
+ case 'number_mul':
658
+ if (typeof action.number_mul !== 'number') {
659
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['number_mul'], message: 'number_mul requires number_mul' })
660
+ }
661
+ break
662
+ case 'datetime_add':
663
+ if (typeof action.datetime_add !== 'number') {
664
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['datetime_add'], message: 'datetime_add requires datetime_add' })
665
+ }
666
+ break
667
+ case 'datetime_sub':
668
+ if (typeof action.datetime_sub !== 'number') {
669
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['datetime_sub'], message: 'datetime_sub requires datetime_sub' })
670
+ }
671
+ break
672
+ case 'datetime_now':
673
+ case 'formula':
674
+ break
675
+ default:
676
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['type'], message: `unsupported bulk update action type: ${action.type}` })
677
+ }
678
+ })
679
+
680
+ export const EntityBulkUpdateReqVOSchemaOverride: z.ZodTypeAny = z.object({
681
+ policy_id: z.string().optional(),
682
+ selection: entitySelectionSchema,
683
+ fields: z.array(z.object({
684
+ id: z.number().int().min(1001),
685
+ action: bulkUpdateActionSchema,
686
+ }).strict()).min(1),
687
+ }).strict()
688
+
689
+ export const EntityTagsSelectionReqVOSchemaOverride: z.ZodTypeAny = z.object({
690
+ policy_id: z.string(),
691
+ selection: entitySelectionSchema,
692
+ }).strict()
693
+
694
+ export const EntityTagsBatchReqVOSchemaOverride: z.ZodTypeAny = z.object({
695
+ policy_id: z.string(),
696
+ selection: entitySelectionSchema,
697
+ tag_names: z.array(z.string().min(1)).min(1),
698
+ }).strict()
699
+
700
+ export const EntityInvokeBatchOperatorReqVOSchemaOverride: z.ZodTypeAny = z.object({
701
+ id: z.string().min(1),
702
+ data: z.object({
703
+ action_id: z.string().min(1),
704
+ policy_id: z.string().optional(),
705
+ selection: entitySelectionLooseSchema,
706
+ }).strict(),
707
+ params: z.record(z.string(), z.any()).optional(),
708
+ }).strict()