@carthooks/arcubase-cli 0.1.3 → 0.1.5

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 (64) hide show
  1. package/bundle/arcubase-admin.mjs +546 -243
  2. package/bundle/arcubase.mjs +546 -243
  3. package/dist/bin/arcubase-admin.js +0 -0
  4. package/dist/bin/arcubase.js +0 -0
  5. package/dist/generated/command_registry.generated.d.ts +36 -112
  6. package/dist/generated/command_registry.generated.d.ts.map +1 -1
  7. package/dist/generated/command_registry.generated.js +40 -163
  8. package/dist/generated/type_index.generated.d.ts +0 -12
  9. package/dist/generated/type_index.generated.d.ts.map +1 -1
  10. package/dist/generated/type_index.generated.js +0 -12
  11. package/dist/generated/zod_registry.generated.d.ts +1 -19
  12. package/dist/generated/zod_registry.generated.d.ts.map +1 -1
  13. package/dist/generated/zod_registry.generated.js +0 -21
  14. package/dist/runtime/command_registry.d.ts +19 -1
  15. package/dist/runtime/command_registry.d.ts.map +1 -1
  16. package/dist/runtime/command_registry.js +19 -1
  17. package/dist/runtime/errors.d.ts +1 -0
  18. package/dist/runtime/errors.d.ts.map +1 -1
  19. package/dist/runtime/execute.d.ts.map +1 -1
  20. package/dist/runtime/execute.js +321 -8
  21. package/dist/runtime/upload.d.ts +13 -0
  22. package/dist/runtime/upload.d.ts.map +1 -0
  23. package/dist/runtime/upload.js +148 -0
  24. package/dist/runtime/zod_registry.d.ts.map +1 -1
  25. package/dist/runtime/zod_registry.js +50 -30
  26. package/dist/tests/command_registry.test.js +11 -4
  27. package/dist/tests/execute_validation.test.js +221 -13
  28. package/dist/tests/help.test.js +29 -6
  29. package/dist/tests/upload.test.d.ts +2 -0
  30. package/dist/tests/upload.test.d.ts.map +1 -0
  31. package/dist/tests/upload.test.js +107 -0
  32. package/package.json +1 -1
  33. package/sdk-dist/api/admin/app.ts +8 -1
  34. package/sdk-dist/docs/runtime-reference/README.md +33 -15
  35. package/sdk-dist/docs/runtime-reference/app-discovery.md +68 -0
  36. package/sdk-dist/docs/runtime-reference/entity-schema/README.md +2 -2
  37. package/sdk-dist/docs/runtime-reference/entity-schema/file.md +9 -0
  38. package/sdk-dist/docs/runtime-reference/entity-schema/image.md +9 -0
  39. package/sdk-dist/docs/runtime-reference/entity-schema/subform.md +2 -2
  40. package/sdk-dist/docs/runtime-reference/entity-schema.md +2 -2
  41. package/sdk-dist/docs/runtime-reference/examples/README.md +1 -1
  42. package/sdk-dist/docs/runtime-reference/examples/oms-01/README.md +23 -4
  43. package/sdk-dist/docs/runtime-reference/examples/oms-01/app-overview.md +1 -1
  44. package/sdk-dist/docs/runtime-reference/row-crud.md +8 -6
  45. package/sdk-dist/docs/runtime-reference/search-and-bulk-actions.md +5 -5
  46. package/sdk-dist/docs/runtime-reference/table-lifecycle.md +34 -6
  47. package/sdk-dist/docs/runtime-reference/uploads.md +106 -0
  48. package/sdk-dist/generated/command_registry.generated.ts +40 -163
  49. package/sdk-dist/generated/type_index.generated.ts +0 -12
  50. package/sdk-dist/generated/zod_registry.generated.ts +0 -36
  51. package/sdk-dist/types/app.ts +7 -0
  52. package/src/generated/command_registry.generated.ts +40 -163
  53. package/src/generated/type_index.generated.ts +0 -12
  54. package/src/generated/zod_registry.generated.ts +0 -36
  55. package/src/runtime/command_registry.ts +38 -2
  56. package/src/runtime/errors.ts +1 -0
  57. package/src/runtime/execute.ts +368 -8
  58. package/src/runtime/upload.ts +196 -0
  59. package/src/runtime/zod_registry.ts +50 -30
  60. package/src/tests/command_registry.test.ts +13 -4
  61. package/src/tests/execute_validation.test.ts +257 -13
  62. package/src/tests/help.test.ts +35 -6
  63. package/src/tests/upload.test.ts +133 -0
  64. package/sdk-dist/docs/runtime-reference/workflow/README.md +0 -19
@@ -67,55 +67,60 @@ const examplesIndexDocHint: RuntimeDocHint = {
67
67
  file: '/opt/arcubase-sdk/docs/runtime-reference/examples/README.md',
68
68
  }
69
69
 
70
- const omsExampleDocHint: RuntimeDocHint = {
71
- title: 'OMS example 01',
72
- file: '/opt/arcubase-sdk/docs/runtime-reference/examples/oms-01/README.md',
70
+ const appDiscoveryDocHint: RuntimeDocHint = {
71
+ title: 'App discovery',
72
+ file: '/opt/arcubase-sdk/docs/runtime-reference/app-discovery.md',
73
73
  }
74
74
 
75
75
  const docHintIndex: Record<string, RuntimeDocHint[]> = {
76
76
  AppCreateByTenantsReqVO: [
77
+ appDiscoveryDocHint,
77
78
  { title: 'Table lifecycle', file: '/opt/arcubase-sdk/docs/runtime-reference/table-lifecycle.md' },
78
79
  examplesIndexDocHint,
79
- omsExampleDocHint,
80
80
  ],
81
81
  AppCreateEntityReqVO: [
82
+ appDiscoveryDocHint,
82
83
  { title: 'Table lifecycle', file: '/opt/arcubase-sdk/docs/runtime-reference/table-lifecycle.md' },
83
84
  examplesIndexDocHint,
84
- omsExampleDocHint,
85
85
  ],
86
86
  EntitySaveReqVO: [
87
87
  { title: 'Entity schema', file: '/opt/arcubase-sdk/docs/runtime-reference/entity-schema.md' },
88
- omsExampleDocHint,
88
+ examplesIndexDocHint,
89
89
  ],
90
90
  EntityQueryReqVO: [
91
91
  { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
92
92
  { title: 'Search and bulk actions', file: '/opt/arcubase-sdk/docs/runtime-reference/search-and-bulk-actions.md' },
93
- omsExampleDocHint,
93
+ examplesIndexDocHint,
94
94
  ],
95
95
  EntityUpdateReqVO: [
96
96
  { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
97
- omsExampleDocHint,
97
+ { title: 'Uploads', file: '/opt/arcubase-sdk/docs/runtime-reference/uploads.md' },
98
+ examplesIndexDocHint,
98
99
  ],
99
100
  EntitySelectionActionReqVO: [
100
101
  { title: 'Search and bulk actions', file: '/opt/arcubase-sdk/docs/runtime-reference/search-and-bulk-actions.md' },
101
- omsExampleDocHint,
102
+ examplesIndexDocHint,
102
103
  ],
103
104
  InsertReqVO: [
104
105
  { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
105
- omsExampleDocHint,
106
+ { title: 'Uploads', file: '/opt/arcubase-sdk/docs/runtime-reference/uploads.md' },
107
+ examplesIndexDocHint,
106
108
  ],
107
109
  EntityBulkUpdateReqVO: [
108
110
  { title: 'Search and bulk actions', file: '/opt/arcubase-sdk/docs/runtime-reference/search-and-bulk-actions.md' },
109
- omsExampleDocHint,
111
+ examplesIndexDocHint,
110
112
  ],
111
113
  'EntityInvokeBatchOperatorReqVO & InvokeRequestVO': [
112
114
  { title: 'Search and bulk actions', file: '/opt/arcubase-sdk/docs/runtime-reference/search-and-bulk-actions.md' },
115
+ examplesIndexDocHint,
113
116
  ],
114
117
  '{ policy_id: string selection: any }': [
115
118
  { title: 'Search and bulk actions', file: '/opt/arcubase-sdk/docs/runtime-reference/search-and-bulk-actions.md' },
119
+ examplesIndexDocHint,
116
120
  ],
117
121
  '{ policy_id: string selection: any tag_names: string[] }': [
118
122
  { title: 'Search and bulk actions', file: '/opt/arcubase-sdk/docs/runtime-reference/search-and-bulk-actions.md' },
123
+ examplesIndexDocHint,
119
124
  ],
120
125
  }
121
126
 
@@ -150,53 +155,68 @@ export function getDocHints(typeName: string): RuntimeDocHint[] {
150
155
 
151
156
  const commandDocHintIndex: Record<string, RuntimeDocHint[]> = {
152
157
  'admin.app.createAppByTenants': [
158
+ appDiscoveryDocHint,
153
159
  { title: 'Table lifecycle', file: '/opt/arcubase-sdk/docs/runtime-reference/table-lifecycle.md' },
154
160
  examplesIndexDocHint,
155
- omsExampleDocHint,
161
+ ],
162
+ 'admin.app.listApps': [
163
+ appDiscoveryDocHint,
164
+ examplesIndexDocHint,
165
+ ],
166
+ 'admin.app.inventory': [
167
+ appDiscoveryDocHint,
168
+ examplesIndexDocHint,
156
169
  ],
157
170
  'admin.app.createEntity': [
171
+ appDiscoveryDocHint,
158
172
  { title: 'Table lifecycle', file: '/opt/arcubase-sdk/docs/runtime-reference/table-lifecycle.md' },
159
- omsExampleDocHint,
173
+ examplesIndexDocHint,
160
174
  ],
161
175
  'admin.entity.adminGetEntityInfo': [
176
+ appDiscoveryDocHint,
162
177
  { title: 'Table lifecycle', file: '/opt/arcubase-sdk/docs/runtime-reference/table-lifecycle.md' },
163
- omsExampleDocHint,
178
+ examplesIndexDocHint,
164
179
  ],
165
180
  'admin.entity.adminSaveEntity': [
166
181
  { title: 'Entity schema', file: '/opt/arcubase-sdk/docs/runtime-reference/entity-schema.md' },
167
- omsExampleDocHint,
182
+ { title: 'Table lifecycle', file: '/opt/arcubase-sdk/docs/runtime-reference/table-lifecycle.md' },
183
+ { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
184
+ examplesIndexDocHint,
168
185
  ],
169
- 'user.workflow.insertEntity': [
186
+ 'user.rows.insertEntity': [
170
187
  { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
171
- omsExampleDocHint,
188
+ { title: 'Uploads', file: '/opt/arcubase-sdk/docs/runtime-reference/uploads.md' },
189
+ examplesIndexDocHint,
172
190
  ],
173
- 'user.workflow.queryEntity': [
191
+ 'user.rows.queryEntity': [
174
192
  { title: 'Search and bulk actions', file: '/opt/arcubase-sdk/docs/runtime-reference/search-and-bulk-actions.md' },
175
- omsExampleDocHint,
193
+ { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
194
+ examplesIndexDocHint,
176
195
  ],
177
- 'user.workflow.getEntityRow': [
196
+ 'user.rows.getEntityRow': [
178
197
  { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
179
- omsExampleDocHint,
198
+ examplesIndexDocHint,
180
199
  ],
181
- 'user.workflow.updateEntityRow': [
200
+ 'user.rows.updateEntityRow': [
182
201
  { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
183
- omsExampleDocHint,
202
+ { title: 'Uploads', file: '/opt/arcubase-sdk/docs/runtime-reference/uploads.md' },
203
+ examplesIndexDocHint,
184
204
  ],
185
- 'user.workflow.queryEntitySelection': [
205
+ 'user.rows.queryEntitySelection': [
186
206
  { title: 'Search and bulk actions', file: '/opt/arcubase-sdk/docs/runtime-reference/search-and-bulk-actions.md' },
187
- omsExampleDocHint,
207
+ examplesIndexDocHint,
188
208
  ],
189
- 'user.workflow.deleteEntityRow': [
209
+ 'user.rows.deleteEntityRow': [
190
210
  { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
191
- omsExampleDocHint,
211
+ examplesIndexDocHint,
192
212
  ],
193
- 'user.workflow.restoreEntityRow': [
213
+ 'user.rows.restoreEntityRow': [
194
214
  { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
195
- omsExampleDocHint,
215
+ examplesIndexDocHint,
196
216
  ],
197
217
  'user.entity.updateEntityBulk': [
198
218
  { title: 'Search and bulk actions', file: '/opt/arcubase-sdk/docs/runtime-reference/search-and-bulk-actions.md' },
199
- omsExampleDocHint,
219
+ examplesIndexDocHint,
200
220
  ],
201
221
  }
202
222
 
@@ -8,8 +8,17 @@ test('admin app create-entity command exists', () => {
8
8
  assert.ok(adminCommands.some((c) => c.commandPath.join(' ') === 'app create-entity'))
9
9
  })
10
10
 
11
- test('user workflow query-entity command exists', () => {
12
- assert.ok(userCommands.some((c) => c.commandPath.join(' ') === 'workflow query-entity'))
11
+ test('admin app list-apps command exists', () => {
12
+ assert.ok(adminCommands.some((c) => c.commandPath.join(' ') === 'app list-apps'))
13
+ })
14
+
15
+ test('admin app inventory command exists', () => {
16
+ const suggestions = findCommandSuggestions('app', 'inventory', 20)
17
+ assert.ok(suggestions.some((item) => item.binary === 'arcubase-admin' && item.moduleName === 'app' && item.commandName === 'inventory'))
18
+ })
19
+
20
+ test('user rows query-entity command exists', () => {
21
+ assert.ok(userCommands.some((c) => c.commandPath.join(' ') === 'rows query-entity'))
13
22
  })
14
23
 
15
24
  test('inline request type keeps full object text instead of truncated brace', () => {
@@ -27,7 +36,7 @@ test('tag commands no longer emit truncated inline request type', () => {
27
36
  assert.equal(typed.scalarParams.length, 0)
28
37
  })
29
38
 
30
- test('suggestions can point wrong admin delete guess to user workflow delete command', () => {
39
+ test('suggestions can point wrong admin delete guess to user rows delete command', () => {
31
40
  const suggestions = findCommandSuggestions('entity', 'admin-delete-entity-row')
32
- assert.ok(suggestions.some((item) => item.binary === 'arcubase' && item.moduleName === 'workflow' && item.commandName === 'delete-entity-row'))
41
+ assert.ok(suggestions.some((item) => item.binary === 'arcubase' && item.moduleName === 'rows' && item.commandName === 'delete-entity-row'))
33
42
  })
@@ -82,6 +82,67 @@ test('admin api-prefixed command executes against external root path', async ()
82
82
  assert.equal(out.data.url, 'https://arcubase.example.com/apps/app_1/entity-shares')
83
83
  })
84
84
 
85
+ test('app inventory returns apps with entity lists', async () => {
86
+ const calls: string[] = []
87
+ const out = await executeCLI(
88
+ 'admin',
89
+ ['app', 'inventory'],
90
+ env as any,
91
+ async (url, init) => {
92
+ calls.push(`${init?.method ?? 'GET'} ${String(url)}`)
93
+ if (String(url) === 'https://arcubase.example.com/apps') {
94
+ return new Response(JSON.stringify({
95
+ data: [
96
+ { id: 101, name: 'Workspace' },
97
+ { id: 102, name: 'CRM' },
98
+ ],
99
+ }), { status: 200 })
100
+ }
101
+ if (String(url) === 'https://arcubase.example.com/apps/101/entities') {
102
+ return new Response(JSON.stringify({
103
+ data: [
104
+ { id: 201, name: 'Orders' },
105
+ { id: 202, name: 'Customers' },
106
+ ],
107
+ }), { status: 200 })
108
+ }
109
+ if (String(url) === 'https://arcubase.example.com/apps/102/entities') {
110
+ return new Response(JSON.stringify({
111
+ data: [
112
+ { id: 203, name: 'Leads' },
113
+ ],
114
+ }), { status: 200 })
115
+ }
116
+ return new Response('{}', { status: 404 })
117
+ },
118
+ )
119
+ assert.equal(out.kind, 'result')
120
+ assert.deepEqual(out.data, {
121
+ apps: [
122
+ {
123
+ id: '101',
124
+ name: 'Workspace',
125
+ entities: [
126
+ { id: '201', name: 'Orders' },
127
+ { id: '202', name: 'Customers' },
128
+ ],
129
+ },
130
+ {
131
+ id: '102',
132
+ name: 'CRM',
133
+ entities: [
134
+ { id: '203', name: 'Leads' },
135
+ ],
136
+ },
137
+ ],
138
+ })
139
+ assert.deepEqual(calls, [
140
+ 'GET https://arcubase.example.com/apps',
141
+ 'POST https://arcubase.example.com/apps/101/entities',
142
+ 'POST https://arcubase.example.com/apps/102/entities',
143
+ ])
144
+ })
145
+
85
146
  test('admin save entity treats top-level error body as failure even on http 200', async () => {
86
147
  const bodyFile = tempJSONFile({
87
148
  id: 2188889845,
@@ -224,7 +285,7 @@ test('delete entity row rejects selection type outside ids', async () => {
224
285
  await assert.rejects(async () => {
225
286
  await executeCLI(
226
287
  'user',
227
- ['workflow', 'delete-entity-row', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile],
288
+ ['rows', 'delete-entity-row', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile],
228
289
  env as any,
229
290
  async () => new Response('{}', { status: 200 }),
230
291
  )
@@ -283,7 +344,7 @@ test('selection action accepts condition selection with selectAll', async () =>
283
344
 
284
345
  const out = await executeCLI(
285
346
  'user',
286
- ['workflow', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile],
347
+ ['rows', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile],
287
348
  env as any,
288
349
  async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
289
350
  )
@@ -306,7 +367,7 @@ test('selection action accepts condition selection with filter_* keys', async ()
306
367
 
307
368
  const out = await executeCLI(
308
369
  'user',
309
- ['workflow', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile],
370
+ ['rows', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile],
310
371
  env as any,
311
372
  async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
312
373
  )
@@ -328,7 +389,7 @@ test('selection action accepts condition selection with selectAll only for batch
328
389
 
329
390
  const out = await executeCLI(
330
391
  'user',
331
- ['workflow', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'batch_print', '--body-file', bodyFile],
392
+ ['rows', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'batch_print', '--body-file', bodyFile],
332
393
  env as any,
333
394
  async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
334
395
  )
@@ -346,7 +407,7 @@ test('selection action accepts all selection for query-style actions', async ()
346
407
 
347
408
  const out = await executeCLI(
348
409
  'user',
349
- ['workflow', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'query', '--body-file', bodyFile],
410
+ ['rows', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'query', '--body-file', bodyFile],
350
411
  env as any,
351
412
  async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
352
413
  )
@@ -369,7 +430,7 @@ test('selection action rejects condition selection for query action', async () =
369
430
  await assert.rejects(async () => {
370
431
  await executeCLI(
371
432
  'user',
372
- ['workflow', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'query', '--body-file', bodyFile],
433
+ ['rows', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'query', '--body-file', bodyFile],
373
434
  env as any,
374
435
  async () => new Response('{}', { status: 200 }),
375
436
  )
@@ -391,7 +452,7 @@ test('selection action rejects all selection for archive action', async () => {
391
452
  await assert.rejects(async () => {
392
453
  await executeCLI(
393
454
  'user',
394
- ['workflow', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile],
455
+ ['rows', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile],
395
456
  env as any,
396
457
  async () => new Response('{}', { status: 200 }),
397
458
  )
@@ -417,7 +478,7 @@ test('selection action rejects selectAll-only condition for archive action', asy
417
478
  await assert.rejects(async () => {
418
479
  await executeCLI(
419
480
  'user',
420
- ['workflow', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile],
481
+ ['rows', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile],
421
482
  env as any,
422
483
  async () => new Response('{}', { status: 200 }),
423
484
  )
@@ -429,6 +490,189 @@ test('selection action rejects selectAll-only condition for archive action', asy
429
490
  })
430
491
  })
431
492
 
493
+ test('insert entity rejects local file path for file field and points to arcubase upload', async () => {
494
+ const bodyFile = tempJSONFile({
495
+ fields: {
496
+ 1001: 'Lead A',
497
+ 1007: './contract.pdf',
498
+ },
499
+ })
500
+ const calls: Array<{ url: string; method?: string }> = []
501
+
502
+ await assert.rejects(async () => {
503
+ await executeCLI(
504
+ 'user',
505
+ ['rows', 'insert-entity', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile],
506
+ env as any,
507
+ async (url, init) => {
508
+ calls.push({ url: String(url), method: init?.method })
509
+ if (String(url) === 'https://arcubase.example.com/entity/app_1/entity_1' && init?.method === 'GET') {
510
+ return new Response(JSON.stringify({
511
+ data: {
512
+ id: 1,
513
+ app_id: 1,
514
+ fields: [
515
+ { id: 1001, type: 'text', label: 'Lead Name' },
516
+ { id: 1007, type: 'file', label: 'Contract File' },
517
+ ],
518
+ },
519
+ }), { status: 200 })
520
+ }
521
+ return new Response('{}', { status: 200 })
522
+ },
523
+ )
524
+ }, (error: unknown) => {
525
+ assert.ok(error instanceof CLIError)
526
+ assert.equal(error.code, 'BODY_VALIDATION_FAILED')
527
+ assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.fields.1007'))
528
+ assert.match(error.message, /arcubase upload \.\/contract\.pdf/)
529
+ assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/uploads.md')))
530
+ return true
531
+ })
532
+
533
+ assert.deepEqual(calls, [
534
+ { url: 'https://arcubase.example.com/entity/app_1/entity_1', method: 'GET' },
535
+ ])
536
+ })
537
+
538
+ test('update row rejects object value for image field and requires upload array', async () => {
539
+ const bodyFile = tempJSONFile({
540
+ changed_fields: [1008],
541
+ data: {
542
+ 1008: {
543
+ upload_id: 123456,
544
+ file: 'lead-photo.jpg',
545
+ file_name: 'lead-photo.jpg',
546
+ meta: {},
547
+ },
548
+ },
549
+ })
550
+
551
+ await assert.rejects(async () => {
552
+ await executeCLI(
553
+ 'user',
554
+ ['rows', 'update-entity-row', '--app-id', 'app_1', '--entity-id', 'entity_1', '--row-id', 'row_1', '--body-file', bodyFile],
555
+ env as any,
556
+ async (url, init) => {
557
+ if (String(url) === 'https://arcubase.example.com/entity/app_1/entity_1' && init?.method === 'GET') {
558
+ return new Response(JSON.stringify({
559
+ data: {
560
+ id: 1,
561
+ app_id: 1,
562
+ fields: [
563
+ { id: 1008, type: 'image', label: 'Lead Photo' },
564
+ ],
565
+ },
566
+ }), { status: 200 })
567
+ }
568
+ return new Response('{}', { status: 200 })
569
+ },
570
+ )
571
+ }, (error: unknown) => {
572
+ assert.ok(error instanceof CLIError)
573
+ assert.equal(error.code, 'BODY_VALIDATION_FAILED')
574
+ assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.data.1008'))
575
+ assert.match(error.message, /must be an array/)
576
+ assert.match(error.message, /arcubase upload <local-file>/)
577
+ return true
578
+ })
579
+ })
580
+
581
+ test('insert entity accepts upload array for file field after schema preflight', async () => {
582
+ const bodyFile = tempJSONFile({
583
+ fields: {
584
+ 1001: 'Lead A',
585
+ 1007: [
586
+ {
587
+ upload_id: 123456,
588
+ file: 'contract.pdf',
589
+ file_name: 'contract.pdf',
590
+ meta: {},
591
+ },
592
+ ],
593
+ },
594
+ })
595
+ const calls: Array<{ url: string; method?: string }> = []
596
+
597
+ const out = await executeCLI(
598
+ 'user',
599
+ ['rows', 'insert-entity', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile],
600
+ env as any,
601
+ async (url, init) => {
602
+ calls.push({ url: String(url), method: init?.method })
603
+ if (String(url) === 'https://arcubase.example.com/entity/app_1/entity_1' && init?.method === 'GET') {
604
+ return new Response(JSON.stringify({
605
+ data: {
606
+ id: 1,
607
+ app_id: 1,
608
+ fields: [
609
+ { id: 1001, type: 'text', label: 'Lead Name' },
610
+ { id: 1007, type: 'file', label: 'Contract File' },
611
+ ],
612
+ },
613
+ }), { status: 200 })
614
+ }
615
+ return new Response(String(init?.body ?? '{}'), { status: 200 })
616
+ },
617
+ )
618
+
619
+ assert.equal(out.kind, 'result')
620
+ assert.deepEqual(calls, [
621
+ { url: 'https://arcubase.example.com/entity/app_1/entity_1', method: 'GET' },
622
+ { url: 'https://arcubase.example.com/entity/app_1/entity_1/insert', method: 'POST' },
623
+ ])
624
+ assert.equal(out.data.fields['1007'][0].upload_id, 123456)
625
+ })
626
+
627
+ test('insert entity decorates upload bind failures with actionable guidance', async () => {
628
+ const bodyFile = tempJSONFile({
629
+ fields: {
630
+ 1001: 'Lead A',
631
+ 1007: [
632
+ {
633
+ upload_id: 123456,
634
+ file: 'contract.pdf',
635
+ file_name: 'contract.pdf',
636
+ meta: {},
637
+ },
638
+ ],
639
+ },
640
+ })
641
+
642
+ await assert.rejects(async () => {
643
+ await executeCLI(
644
+ 'user',
645
+ ['rows', 'insert-entity', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile],
646
+ env as any,
647
+ async (url, init) => {
648
+ if (String(url) === 'https://arcubase.example.com/entity/app_1/entity_1' && init?.method === 'GET') {
649
+ return new Response(JSON.stringify({
650
+ data: {
651
+ fields: [{ id: 1007, type: 'file', label: 'Contract File' }],
652
+ },
653
+ }), { status: 200 })
654
+ }
655
+ return new Response(JSON.stringify({
656
+ error: {
657
+ message: 'failed to bind upload to assets: Head \"http://test-bucket.test.aliyuncs.com/upload/123456/contract.pdf\": dial tcp: lookup test-bucket.test.aliyuncs.com: no such host',
658
+ type: 'system',
659
+ key: 'error.system',
660
+ },
661
+ trace_id: 'req_upload_bind',
662
+ }), { status: 200 })
663
+ },
664
+ )
665
+ }, (error: unknown) => {
666
+ assert.ok(error instanceof CLIError)
667
+ assert.equal(error.code, 'UPSTREAM_BODY_ERROR')
668
+ assert.equal(error.details?.traceId, 'req_upload_bind')
669
+ assert.equal(error.details?.reason, 'the row payload used a file/image upload entry, but Arcubase could not bind that upload into an asset record')
670
+ assert.match(error.details?.hint ?? '', /rerun arcubase upload/)
671
+ assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/uploads.md')))
672
+ return true
673
+ })
674
+ })
675
+
432
676
  test('selection action rejects condition selection without selectAll', async () => {
433
677
  const bodyFile = tempJSONFile({
434
678
  selection: {
@@ -443,7 +687,7 @@ test('selection action rejects condition selection without selectAll', async ()
443
687
  await assert.rejects(async () => {
444
688
  await executeCLI(
445
689
  'user',
446
- ['workflow', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile],
690
+ ['rows', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile],
447
691
  env as any,
448
692
  async () => new Response('{}', { status: 200 }),
449
693
  )
@@ -470,7 +714,7 @@ test('selection action rejects condition selection that uses text_search instead
470
714
  await assert.rejects(async () => {
471
715
  await executeCLI(
472
716
  'user',
473
- ['workflow', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile],
717
+ ['rows', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile],
474
718
  env as any,
475
719
  async () => new Response('{}', { status: 200 }),
476
720
  )
@@ -597,7 +841,7 @@ test('query entity rejects camelCase viewMode and points to row crud doc', async
597
841
  await assert.rejects(async () => {
598
842
  await executeCLI(
599
843
  'user',
600
- ['workflow', 'query-entity', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile],
844
+ ['rows', 'query-entity', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile],
601
845
  env as any,
602
846
  async () => new Response('{}', { status: 200 }),
603
847
  )
@@ -624,7 +868,7 @@ test('update entity row rejects fields key', async () => {
624
868
  await assert.rejects(async () => {
625
869
  await executeCLI(
626
870
  'user',
627
- ['workflow', 'update-entity-row', '--app-id', 'app_1', '--entity-id', 'entity_1', '--row-id', 'row_1', '--body-file', bodyFile],
871
+ ['rows', 'update-entity-row', '--app-id', 'app_1', '--entity-id', 'entity_1', '--row-id', 'row_1', '--body-file', bodyFile],
628
872
  env as any,
629
873
  async () => new Response('{}', { status: 200 }),
630
874
  )
@@ -648,7 +892,7 @@ test('update entity row rejects changed_fields that are missing from data', asyn
648
892
  await assert.rejects(async () => {
649
893
  await executeCLI(
650
894
  'user',
651
- ['workflow', 'update-entity-row', '--app-id', 'app_1', '--entity-id', 'entity_1', '--row-id', 'row_1', '--body-file', bodyFile],
895
+ ['rows', 'update-entity-row', '--app-id', 'app_1', '--entity-id', 'entity_1', '--row-id', 'row_1', '--body-file', bodyFile],
652
896
  env as any,
653
897
  async () => new Response('{}', { status: 200 }),
654
898
  )
@@ -28,10 +28,11 @@ test('module help prints create-entity command', async () => {
28
28
  const out = await executeCLI('admin', ['app', '--help'], env as any, async () => new Response('{}'))
29
29
  assert.equal(out.kind, 'help')
30
30
  assert.match(out.text, /create-entity/)
31
+ assert.match(out.text, /inventory/)
31
32
  })
32
33
 
33
34
  test('module help does not require runtime env', async () => {
34
- const out = await executeCLI('user', ['workflow', '--help'], undefined, async () => new Response('{}'))
35
+ const out = await executeCLI('user', ['rows', '--help'], undefined, async () => new Response('{}'))
35
36
  assert.equal(out.kind, 'help')
36
37
  assert.match(out.text, /delete-entity-row/)
37
38
  })
@@ -44,21 +45,21 @@ test('command help prints normalized external endpoint', async () => {
44
45
  })
45
46
 
46
47
  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
+ const out = await executeCLI('user', ['rows', 'delete-entity-row', '--help'], undefined, async () => new Response('{}'))
48
49
  assert.equal(out.kind, 'help')
49
50
  assert.match(out.text, /endpoint: \/entity\/:app_id\/:entity_id\/delete-item/)
50
51
  })
51
52
 
52
53
  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
+ const out = await executeCLI('user', ['rows', 'update-entity-row', '--help'], undefined, async () => new Response('{}'))
54
55
  assert.equal(out.kind, 'help')
55
56
  assert.match(out.text, /docs:/)
56
57
  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
+ assert.match(out.text, /\/opt\/arcubase-sdk\/docs\/runtime-reference\/examples\/README\.md/)
58
59
  })
59
60
 
60
61
  test('command help prints query-file support', async () => {
61
- const out = await executeCLI('user', ['workflow', 'query-entity', '--help'], undefined, async () => new Response('{}'))
62
+ const out = await executeCLI('user', ['rows', 'query-entity', '--help'], undefined, async () => new Response('{}'))
62
63
  assert.equal(out.kind, 'help')
63
64
  assert.match(out.text, /query flags: --query-file/)
64
65
  })
@@ -67,5 +68,33 @@ test('command help without request body can still point to lifecycle and example
67
68
  const out = await executeCLI('admin', ['entity', 'admin-get-entity-info', '--help'], undefined, async () => new Response('{}'))
68
69
  assert.equal(out.kind, 'help')
69
70
  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
+ assert.match(out.text, /\/opt\/arcubase-sdk\/docs\/runtime-reference\/examples\/README\.md/)
72
+ })
73
+
74
+ test('app inventory help explains app discovery purpose', async () => {
75
+ const out = await executeCLI('admin', ['app', 'inventory', '--help'], undefined, async () => new Response('{}'))
76
+ assert.equal(out.kind, 'help')
77
+ assert.match(out.text, /list all accessible apps and each app entity list/)
78
+ assert.match(out.text, /use this before asking the user for app_id/)
79
+ })
80
+
81
+ test('admin-save-entity help prints row-operation next step', async () => {
82
+ const out = await executeCLI('admin', ['entity', 'admin-save-entity', '--help'], undefined, async () => new Response('{}'))
83
+ assert.equal(out.kind, 'help')
84
+ assert.match(out.text, /switch to arcubase for row operations/)
85
+ assert.match(out.text, /arcubase rows insert-entity/)
86
+ assert.match(out.text, /arcubase rows query-entity/)
87
+ })
88
+
89
+ test('wrong admin workflow command points to arcubase row command', async () => {
90
+ await assert.rejects(
91
+ () => executeCLI('admin', ['workflow', 'insert-entity'], env as any, async () => new Response('{}')),
92
+ (error: any) => {
93
+ assert.equal(error.code, 'UNKNOWN_COMMAND')
94
+ assert.equal(error.details?.reason, 'workflow insert-entity is not a valid command group; use arcubase rows for row operations')
95
+ assert.equal(error.details?.hint, 'use: arcubase rows insert-entity')
96
+ assert.ok((error.details?.suggestions ?? []).includes('arcubase rows insert-entity'))
97
+ return true
98
+ }
99
+ )
71
100
  })