@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
@@ -34,6 +34,25 @@ test('admin save entity rejects invalid body file', async () => {
34
34
  assert.equal(error.details?.requestType, 'EntitySaveReqVO')
35
35
  assert.ok((error.details?.issues ?? []).some((item) => item.path.startsWith('body.layout')))
36
36
  assert.ok((error.details?.tsHints ?? []).some((item) => item.file.startsWith('/opt/arcubase-sdk/')))
37
+ assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/entity-schema.md')))
38
+ return true
39
+ })
40
+ })
41
+
42
+ test('admin create app by tenants rejects name longer than 20 chars', async () => {
43
+ const bodyFile = tempJSONFile({ name: 'CLI Verify Happy Path' })
44
+ await assert.rejects(async () => {
45
+ await executeCLI(
46
+ 'admin',
47
+ ['app', 'create-app-by-tenants', '--body-file', bodyFile],
48
+ env as any,
49
+ async () => new Response('{}', { status: 200 }),
50
+ )
51
+ }, (error: unknown) => {
52
+ assert.ok(error instanceof CLIError)
53
+ assert.equal(error.code, 'BODY_VALIDATION_FAILED')
54
+ assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.name'))
55
+ assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/table-lifecycle.md')))
37
56
  return true
38
57
  })
39
58
  })
@@ -62,3 +81,582 @@ test('admin api-prefixed command executes against external root path', async ()
62
81
  assert.equal(out.kind, 'result')
63
82
  assert.equal(out.data.url, 'https://arcubase.example.com/apps/app_1/entity-shares')
64
83
  })
84
+
85
+ test('admin save entity treats top-level error body as failure even on http 200', async () => {
86
+ const bodyFile = tempJSONFile({
87
+ id: 2188889845,
88
+ app_id: 2188889844,
89
+ name: '订单',
90
+ schema_version: 1,
91
+ field_id_seq: 1002,
92
+ layout: [[1001]],
93
+ fields: [
94
+ {
95
+ id: 1001,
96
+ label: '订单号',
97
+ type: 'text',
98
+ required: true,
99
+ unique: false,
100
+ editable: true,
101
+ visible: true,
102
+ show_label: true,
103
+ scannable: false,
104
+ default_value_mode: 0,
105
+ description: '',
106
+ value: null,
107
+ number_decimal: 0,
108
+ depends: [],
109
+ key: 'order_id',
110
+ code_index: false,
111
+ options: {},
112
+ transfers: null,
113
+ children: null,
114
+ },
115
+ ],
116
+ workflow_enabled: false,
117
+ })
118
+ await assert.rejects(async () => {
119
+ await executeCLI(
120
+ 'admin',
121
+ ['entity', 'admin-save-entity', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile],
122
+ env as any,
123
+ async () =>
124
+ new Response(
125
+ JSON.stringify({
126
+ error: {
127
+ message: 'json: cannot unmarshal object into Go struct field .EntityVO.fields of type []models.Field',
128
+ type: 'system',
129
+ key: 'error.system',
130
+ },
131
+ trace_id: 'req_xxx',
132
+ }),
133
+ { status: 200 },
134
+ ),
135
+ )
136
+ }, (error: unknown) => {
137
+ assert.ok(error instanceof CLIError)
138
+ assert.equal(error.code, 'UPSTREAM_BODY_ERROR')
139
+ assert.equal(error.details?.endpoint, '/apps/app_1/entity/entity_1')
140
+ assert.equal(error.details?.method, 'PUT')
141
+ assert.equal(error.details?.traceId, 'req_xxx')
142
+ return true
143
+ })
144
+ })
145
+
146
+ test('admin save entity normalizes shell null placeholders before validation', async () => {
147
+ const bodyFile = tempJSONFile({
148
+ id: 2188889845,
149
+ app_id: 2188889844,
150
+ name: '订单',
151
+ schema_version: 0,
152
+ field_id_seq: 1002,
153
+ layout: null,
154
+ current_serial: null,
155
+ fields: [
156
+ {
157
+ id: 1001,
158
+ label: '订单号',
159
+ type: 'text',
160
+ required: true,
161
+ unique: false,
162
+ editable: true,
163
+ visible: true,
164
+ show_label: true,
165
+ scannable: false,
166
+ default_value_mode: 0,
167
+ description: '',
168
+ value: null,
169
+ number_decimal: 0,
170
+ depends: [],
171
+ key: 'order_id',
172
+ code_index: false,
173
+ options: {
174
+ placeholder: '',
175
+ type: 'text',
176
+ lengthLimit: false,
177
+ lengthMin: 0,
178
+ lengthMax: 255,
179
+ },
180
+ transfers: null,
181
+ children: null,
182
+ },
183
+ ],
184
+ options: {
185
+ Buttons: null,
186
+ CustomActions: null,
187
+ FrontendEvents: null,
188
+ TitleFormat: {
189
+ Fields: null,
190
+ Parts: null,
191
+ Body: '',
192
+ },
193
+ misc: null,
194
+ },
195
+ workflow_enabled: false,
196
+ })
197
+ const out = await executeCLI(
198
+ 'admin',
199
+ ['entity', 'admin-save-entity', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile],
200
+ env as any,
201
+ async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
202
+ )
203
+ assert.equal(out.kind, 'result')
204
+ const requestBody = out.data
205
+ assert.deepEqual(requestBody.layout, [])
206
+ assert.equal('current_serial' in requestBody, false)
207
+ assert.deepEqual(requestBody.options.Buttons, [])
208
+ assert.deepEqual(requestBody.options.CustomActions, [])
209
+ assert.deepEqual(requestBody.options.FrontendEvents, [])
210
+ assert.deepEqual(requestBody.options.TitleFormat.Fields, [])
211
+ assert.deepEqual(requestBody.options.TitleFormat.Parts, [])
212
+ assert.deepEqual(requestBody.options.misc, {})
213
+ })
214
+
215
+ test('delete entity row rejects selection type outside ids', async () => {
216
+ const bodyFile = tempJSONFile({
217
+ selection: {
218
+ type: 'selection',
219
+ ids: [4000000003],
220
+ length: 1,
221
+ },
222
+ })
223
+
224
+ await assert.rejects(async () => {
225
+ await executeCLI(
226
+ 'user',
227
+ ['workflow', 'delete-entity-row', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile],
228
+ env as any,
229
+ async () => new Response('{}', { status: 200 }),
230
+ )
231
+ }, (error: unknown) => {
232
+ assert.ok(error instanceof CLIError)
233
+ assert.equal(error.code, 'BODY_VALIDATION_FAILED')
234
+ assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.selection'))
235
+ assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/search-and-bulk-actions.md')))
236
+ return true
237
+ })
238
+ })
239
+
240
+ test('bulk update accepts ids selection with number_add action', async () => {
241
+ const bodyFile = tempJSONFile({
242
+ selection: {
243
+ type: 'ids',
244
+ ids: [4000000003],
245
+ length: 1,
246
+ },
247
+ fields: [
248
+ {
249
+ id: 1002,
250
+ action: {
251
+ type: 'number_add',
252
+ number_add: 10,
253
+ },
254
+ },
255
+ ],
256
+ })
257
+
258
+ const out = await executeCLI(
259
+ 'user',
260
+ ['entity', 'update-entity-bulk', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile],
261
+ env as any,
262
+ async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
263
+ )
264
+ assert.equal(out.kind, 'result')
265
+ assert.deepEqual(out.data.selection, { type: 'ids', ids: [4000000003], length: 1 })
266
+ assert.equal(out.data.fields[0].action.type, 'number_add')
267
+ })
268
+
269
+ test('selection action accepts condition selection with selectAll', async () => {
270
+ const bodyFile = tempJSONFile({
271
+ selection: {
272
+ type: 'condition',
273
+ length: 1,
274
+ condition: {
275
+ q: 'A-100',
276
+ selectAll: true,
277
+ },
278
+ sorts: [
279
+ { column: ':1002', order: 'desc' },
280
+ ],
281
+ },
282
+ })
283
+
284
+ const out = await executeCLI(
285
+ 'user',
286
+ ['workflow', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile],
287
+ env as any,
288
+ async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
289
+ )
290
+ assert.equal(out.kind, 'result')
291
+ assert.equal(out.data.selection.type, 'condition')
292
+ assert.equal(out.data.selection.condition.selectAll, true)
293
+ })
294
+
295
+ test('selection action accepts condition selection with filter_* keys', async () => {
296
+ const bodyFile = tempJSONFile({
297
+ selection: {
298
+ type: 'condition',
299
+ length: 1,
300
+ condition: {
301
+ 'filter_:1001': '%7B%22is%22%3A%22A-100%22%7D',
302
+ selectAll: true,
303
+ },
304
+ },
305
+ })
306
+
307
+ const out = await executeCLI(
308
+ 'user',
309
+ ['workflow', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile],
310
+ env as any,
311
+ async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
312
+ )
313
+ assert.equal(out.kind, 'result')
314
+ assert.equal(out.data.selection.type, 'condition')
315
+ assert.equal(out.data.selection.condition.selectAll, true)
316
+ })
317
+
318
+ test('selection action accepts condition selection with selectAll only for batch_print', async () => {
319
+ const bodyFile = tempJSONFile({
320
+ selection: {
321
+ type: 'condition',
322
+ length: 1,
323
+ condition: {
324
+ selectAll: true,
325
+ },
326
+ },
327
+ })
328
+
329
+ const out = await executeCLI(
330
+ 'user',
331
+ ['workflow', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'batch_print', '--body-file', bodyFile],
332
+ env as any,
333
+ async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
334
+ )
335
+ assert.equal(out.kind, 'result')
336
+ assert.equal(out.data.selection.type, 'condition')
337
+ assert.equal(out.data.selection.condition.selectAll, true)
338
+ })
339
+
340
+ test('selection action accepts all selection for query-style actions', async () => {
341
+ const bodyFile = tempJSONFile({
342
+ selection: {
343
+ type: 'all',
344
+ },
345
+ })
346
+
347
+ const out = await executeCLI(
348
+ 'user',
349
+ ['workflow', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'query', '--body-file', bodyFile],
350
+ env as any,
351
+ async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
352
+ )
353
+ assert.equal(out.kind, 'result')
354
+ assert.equal(out.data.selection.type, 'all')
355
+ })
356
+
357
+ test('selection action rejects condition selection for query action', async () => {
358
+ const bodyFile = tempJSONFile({
359
+ selection: {
360
+ type: 'condition',
361
+ length: 1,
362
+ condition: {
363
+ q: 'A-100',
364
+ selectAll: true,
365
+ },
366
+ },
367
+ })
368
+
369
+ await assert.rejects(async () => {
370
+ await executeCLI(
371
+ 'user',
372
+ ['workflow', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'query', '--body-file', bodyFile],
373
+ env as any,
374
+ async () => new Response('{}', { status: 200 }),
375
+ )
376
+ }, (error: unknown) => {
377
+ assert.ok(error instanceof CLIError)
378
+ assert.equal(error.code, 'BODY_VALIDATION_FAILED')
379
+ assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.selection.type'))
380
+ return true
381
+ })
382
+ })
383
+
384
+ test('selection action rejects all selection for archive action', async () => {
385
+ const bodyFile = tempJSONFile({
386
+ selection: {
387
+ type: 'all',
388
+ },
389
+ })
390
+
391
+ await assert.rejects(async () => {
392
+ await executeCLI(
393
+ 'user',
394
+ ['workflow', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile],
395
+ env as any,
396
+ async () => new Response('{}', { status: 200 }),
397
+ )
398
+ }, (error: unknown) => {
399
+ assert.ok(error instanceof CLIError)
400
+ assert.equal(error.code, 'BODY_VALIDATION_FAILED')
401
+ assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.selection.type'))
402
+ return true
403
+ })
404
+ })
405
+
406
+ test('selection action rejects selectAll-only condition for archive action', async () => {
407
+ const bodyFile = tempJSONFile({
408
+ selection: {
409
+ type: 'condition',
410
+ length: 1,
411
+ condition: {
412
+ selectAll: true,
413
+ },
414
+ },
415
+ })
416
+
417
+ await assert.rejects(async () => {
418
+ await executeCLI(
419
+ 'user',
420
+ ['workflow', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile],
421
+ env as any,
422
+ async () => new Response('{}', { status: 200 }),
423
+ )
424
+ }, (error: unknown) => {
425
+ assert.ok(error instanceof CLIError)
426
+ assert.equal(error.code, 'BODY_VALIDATION_FAILED')
427
+ assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.selection.condition'))
428
+ return true
429
+ })
430
+ })
431
+
432
+ test('selection action rejects condition selection without selectAll', async () => {
433
+ const bodyFile = tempJSONFile({
434
+ selection: {
435
+ type: 'condition',
436
+ length: 1,
437
+ condition: {
438
+ q: 'A-100',
439
+ },
440
+ },
441
+ })
442
+
443
+ await assert.rejects(async () => {
444
+ await executeCLI(
445
+ 'user',
446
+ ['workflow', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile],
447
+ env as any,
448
+ async () => new Response('{}', { status: 200 }),
449
+ )
450
+ }, (error: unknown) => {
451
+ assert.ok(error instanceof CLIError)
452
+ assert.equal(error.code, 'BODY_VALIDATION_FAILED')
453
+ assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.selection.condition.selectAll'))
454
+ return true
455
+ })
456
+ })
457
+
458
+ test('selection action rejects condition selection that uses text_search instead of q/filter keys', async () => {
459
+ const bodyFile = tempJSONFile({
460
+ selection: {
461
+ type: 'condition',
462
+ length: 1,
463
+ condition: {
464
+ text_search: 'A-100',
465
+ selectAll: true,
466
+ },
467
+ },
468
+ })
469
+
470
+ await assert.rejects(async () => {
471
+ await executeCLI(
472
+ 'user',
473
+ ['workflow', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile],
474
+ env as any,
475
+ async () => new Response('{}', { status: 200 }),
476
+ )
477
+ }, (error: unknown) => {
478
+ assert.ok(error instanceof CLIError)
479
+ assert.equal(error.code, 'BODY_VALIDATION_FAILED')
480
+ assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.selection.condition'))
481
+ assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/search-and-bulk-actions.md')))
482
+ return true
483
+ })
484
+ })
485
+
486
+ test('bulk update accepts condition selection with q', async () => {
487
+ const bodyFile = tempJSONFile({
488
+ selection: {
489
+ type: 'condition',
490
+ length: 1,
491
+ condition: {
492
+ q: 'A-100',
493
+ selectAll: true,
494
+ },
495
+ sorts: [
496
+ { column: ':1002', order: 'desc' },
497
+ ],
498
+ },
499
+ fields: [
500
+ {
501
+ id: 1002,
502
+ action: {
503
+ type: 'number_add',
504
+ number_add: 2,
505
+ },
506
+ },
507
+ ],
508
+ })
509
+
510
+ const out = await executeCLI(
511
+ 'user',
512
+ ['entity', 'update-entity-bulk', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile],
513
+ env as any,
514
+ async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
515
+ )
516
+ assert.equal(out.kind, 'result')
517
+ assert.equal(out.data.selection.type, 'condition')
518
+ assert.equal(out.data.fields[0].action.type, 'number_add')
519
+ })
520
+
521
+ test('entity tags get-selection-tags accepts ids selection', async () => {
522
+ const bodyFile = tempJSONFile({
523
+ policy_id: '',
524
+ selection: {
525
+ type: 'ids',
526
+ ids: [4000000003],
527
+ length: 1,
528
+ },
529
+ })
530
+
531
+ const out = await executeCLI(
532
+ 'admin',
533
+ ['entity-tags', 'get-selection-tags', '--appId', 'app_1', '--entityId', 'entity_1', '--body-file', bodyFile],
534
+ env as any,
535
+ async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
536
+ )
537
+ assert.equal(out.kind, 'result')
538
+ assert.equal(out.data.selection.type, 'ids')
539
+ })
540
+
541
+ test('entity tags batch-add accepts tag_names and condition selection', async () => {
542
+ const bodyFile = tempJSONFile({
543
+ policy_id: '',
544
+ selection: {
545
+ type: 'condition',
546
+ length: 1,
547
+ condition: {
548
+ q: 'A-100',
549
+ selectAll: true,
550
+ },
551
+ },
552
+ tag_names: ['tag-a'],
553
+ })
554
+
555
+ const out = await executeCLI(
556
+ 'admin',
557
+ ['entity-tags', 'batch-add-entity-tags', '--appId', 'app_1', '--entityId', 'entity_1', '--body-file', bodyFile],
558
+ env as any,
559
+ async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
560
+ )
561
+ assert.equal(out.kind, 'result')
562
+ assert.equal(out.data.tag_names[0], 'tag-a')
563
+ })
564
+
565
+ test('invoke batch operator accepts backend request shape', async () => {
566
+ const bodyFile = tempJSONFile({
567
+ id: 'event_1',
568
+ data: {
569
+ action_id: 'action_1',
570
+ policy_id: '',
571
+ selection: {
572
+ type: 'all',
573
+ },
574
+ },
575
+ params: {},
576
+ })
577
+
578
+ const out = await executeCLI(
579
+ 'user',
580
+ ['entity', 'invoke-entity-batch-operator', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile],
581
+ env as any,
582
+ async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
583
+ )
584
+ assert.equal(out.kind, 'result')
585
+ assert.equal(out.data.id, 'event_1')
586
+ assert.equal(out.data.data.action_id, 'action_1')
587
+ assert.equal(out.data.data.selection.type, 'all')
588
+ })
589
+
590
+ test('query entity rejects camelCase viewMode and points to row crud doc', async () => {
591
+ const bodyFile = tempJSONFile({
592
+ limit: 20,
593
+ offset: 0,
594
+ viewMode: 'grid',
595
+ })
596
+
597
+ await assert.rejects(async () => {
598
+ await executeCLI(
599
+ 'user',
600
+ ['workflow', 'query-entity', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile],
601
+ env as any,
602
+ async () => new Response('{}', { status: 200 }),
603
+ )
604
+ }, (error: unknown) => {
605
+ assert.ok(error instanceof CLIError)
606
+ assert.equal(error.code, 'BODY_VALIDATION_FAILED')
607
+ assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.viewMode'))
608
+ assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/row-crud.md')))
609
+ return true
610
+ })
611
+ })
612
+
613
+ test('update entity row rejects fields key', async () => {
614
+ const bodyFile = tempJSONFile({
615
+ fields: {
616
+ 1002: 99,
617
+ },
618
+ data: {
619
+ '1002': 99,
620
+ },
621
+ changed_fields: [1002],
622
+ })
623
+
624
+ await assert.rejects(async () => {
625
+ await executeCLI(
626
+ 'user',
627
+ ['workflow', 'update-entity-row', '--app-id', 'app_1', '--entity-id', 'entity_1', '--row-id', 'row_1', '--body-file', bodyFile],
628
+ env as any,
629
+ async () => new Response('{}', { status: 200 }),
630
+ )
631
+ }, (error: unknown) => {
632
+ assert.ok(error instanceof CLIError)
633
+ assert.equal(error.code, 'BODY_VALIDATION_FAILED')
634
+ assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.fields'))
635
+ assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/row-crud.md')))
636
+ return true
637
+ })
638
+ })
639
+
640
+ test('update entity row rejects changed_fields that are missing from data', async () => {
641
+ const bodyFile = tempJSONFile({
642
+ data: {
643
+ '1002': 99,
644
+ },
645
+ changed_fields: [1003],
646
+ })
647
+
648
+ await assert.rejects(async () => {
649
+ await executeCLI(
650
+ 'user',
651
+ ['workflow', 'update-entity-row', '--app-id', 'app_1', '--entity-id', 'entity_1', '--row-id', 'row_1', '--body-file', bodyFile],
652
+ env as any,
653
+ async () => new Response('{}', { status: 200 }),
654
+ )
655
+ }, (error: unknown) => {
656
+ assert.ok(error instanceof CLIError)
657
+ assert.equal(error.code, 'BODY_VALIDATION_FAILED')
658
+ assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.changed_fields.0'))
659
+ assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/row-crud.md')))
660
+ return true
661
+ })
662
+ })
@@ -18,15 +18,54 @@ test('root help prints modules', async () => {
18
18
  assert.match(out.text, /app/)
19
19
  })
20
20
 
21
+ test('root help does not require runtime env', async () => {
22
+ const out = await executeCLI('admin', ['--help'], undefined, async () => new Response('{}'))
23
+ assert.equal(out.kind, 'help')
24
+ assert.match(out.text, /modules:/)
25
+ })
26
+
21
27
  test('module help prints create-entity command', async () => {
22
28
  const out = await executeCLI('admin', ['app', '--help'], env as any, async () => new Response('{}'))
23
29
  assert.equal(out.kind, 'help')
24
30
  assert.match(out.text, /create-entity/)
25
31
  })
26
32
 
33
+ test('module help does not require runtime env', async () => {
34
+ const out = await executeCLI('user', ['workflow', '--help'], undefined, async () => new Response('{}'))
35
+ assert.equal(out.kind, 'help')
36
+ assert.match(out.text, /delete-entity-row/)
37
+ })
38
+
27
39
  test('command help prints normalized external endpoint', async () => {
28
40
  const out = await executeCLI('admin', ['app-entity-share', 'get-app-entity-shares', '--help'], env as any, async () => new Response('{}'))
29
41
  assert.equal(out.kind, 'help')
30
42
  assert.match(out.text, /endpoint: \/apps\/:app_id\/entity-shares/)
31
43
  assert.doesNotMatch(out.text, /endpoint: \/api\//)
32
44
  })
45
+
46
+ test('command help does not require runtime env', async () => {
47
+ const out = await executeCLI('user', ['workflow', 'delete-entity-row', '--help'], undefined, async () => new Response('{}'))
48
+ assert.equal(out.kind, 'help')
49
+ assert.match(out.text, /endpoint: \/entity\/:app_id\/:entity_id\/delete-item/)
50
+ })
51
+
52
+ test('command help prints runtime doc hints for request body commands', async () => {
53
+ const out = await executeCLI('user', ['workflow', 'update-entity-row', '--help'], undefined, async () => new Response('{}'))
54
+ assert.equal(out.kind, 'help')
55
+ assert.match(out.text, /docs:/)
56
+ assert.match(out.text, /\/opt\/arcubase-sdk\/docs\/runtime-reference\/row-crud\.md/)
57
+ assert.match(out.text, /\/opt\/arcubase-sdk\/docs\/runtime-reference\/examples\/oms-01\/README\.md/)
58
+ })
59
+
60
+ test('command help prints query-file support', async () => {
61
+ const out = await executeCLI('user', ['workflow', 'query-entity', '--help'], undefined, async () => new Response('{}'))
62
+ assert.equal(out.kind, 'help')
63
+ assert.match(out.text, /query flags: --query-file/)
64
+ })
65
+
66
+ test('command help without request body can still point to lifecycle and examples docs', async () => {
67
+ const out = await executeCLI('admin', ['entity', 'admin-get-entity-info', '--help'], undefined, async () => new Response('{}'))
68
+ assert.equal(out.kind, 'help')
69
+ assert.match(out.text, /\/opt\/arcubase-sdk\/docs\/runtime-reference\/table-lifecycle\.md/)
70
+ assert.match(out.text, /\/opt\/arcubase-sdk\/docs\/runtime-reference\/examples\/oms-01\/README\.md/)
71
+ })