@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
@@ -42,54 +42,59 @@ const examplesIndexDocHint = {
42
42
  title: 'Examples index',
43
43
  file: '/opt/arcubase-sdk/docs/runtime-reference/examples/README.md',
44
44
  };
45
- const omsExampleDocHint = {
46
- title: 'OMS example 01',
47
- file: '/opt/arcubase-sdk/docs/runtime-reference/examples/oms-01/README.md',
45
+ const appDiscoveryDocHint = {
46
+ title: 'App discovery',
47
+ file: '/opt/arcubase-sdk/docs/runtime-reference/app-discovery.md',
48
48
  };
49
49
  const docHintIndex = {
50
50
  AppCreateByTenantsReqVO: [
51
+ appDiscoveryDocHint,
51
52
  { title: 'Table lifecycle', file: '/opt/arcubase-sdk/docs/runtime-reference/table-lifecycle.md' },
52
53
  examplesIndexDocHint,
53
- omsExampleDocHint,
54
54
  ],
55
55
  AppCreateEntityReqVO: [
56
+ appDiscoveryDocHint,
56
57
  { title: 'Table lifecycle', file: '/opt/arcubase-sdk/docs/runtime-reference/table-lifecycle.md' },
57
58
  examplesIndexDocHint,
58
- omsExampleDocHint,
59
59
  ],
60
60
  EntitySaveReqVO: [
61
61
  { title: 'Entity schema', file: '/opt/arcubase-sdk/docs/runtime-reference/entity-schema.md' },
62
- omsExampleDocHint,
62
+ examplesIndexDocHint,
63
63
  ],
64
64
  EntityQueryReqVO: [
65
65
  { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
66
66
  { title: 'Search and bulk actions', file: '/opt/arcubase-sdk/docs/runtime-reference/search-and-bulk-actions.md' },
67
- omsExampleDocHint,
67
+ examplesIndexDocHint,
68
68
  ],
69
69
  EntityUpdateReqVO: [
70
70
  { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
71
- omsExampleDocHint,
71
+ { title: 'Uploads', file: '/opt/arcubase-sdk/docs/runtime-reference/uploads.md' },
72
+ examplesIndexDocHint,
72
73
  ],
73
74
  EntitySelectionActionReqVO: [
74
75
  { title: 'Search and bulk actions', file: '/opt/arcubase-sdk/docs/runtime-reference/search-and-bulk-actions.md' },
75
- omsExampleDocHint,
76
+ examplesIndexDocHint,
76
77
  ],
77
78
  InsertReqVO: [
78
79
  { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
79
- omsExampleDocHint,
80
+ { title: 'Uploads', file: '/opt/arcubase-sdk/docs/runtime-reference/uploads.md' },
81
+ examplesIndexDocHint,
80
82
  ],
81
83
  EntityBulkUpdateReqVO: [
82
84
  { title: 'Search and bulk actions', file: '/opt/arcubase-sdk/docs/runtime-reference/search-and-bulk-actions.md' },
83
- omsExampleDocHint,
85
+ examplesIndexDocHint,
84
86
  ],
85
87
  'EntityInvokeBatchOperatorReqVO & InvokeRequestVO': [
86
88
  { title: 'Search and bulk actions', file: '/opt/arcubase-sdk/docs/runtime-reference/search-and-bulk-actions.md' },
89
+ examplesIndexDocHint,
87
90
  ],
88
91
  '{ policy_id: string selection: any }': [
89
92
  { title: 'Search and bulk actions', file: '/opt/arcubase-sdk/docs/runtime-reference/search-and-bulk-actions.md' },
93
+ examplesIndexDocHint,
90
94
  ],
91
95
  '{ policy_id: string selection: any tag_names: string[] }': [
92
96
  { title: 'Search and bulk actions', file: '/opt/arcubase-sdk/docs/runtime-reference/search-and-bulk-actions.md' },
97
+ examplesIndexDocHint,
93
98
  ],
94
99
  };
95
100
  export function getTypeHints(typeName) {
@@ -126,53 +131,68 @@ export function getDocHints(typeName) {
126
131
  }
127
132
  const commandDocHintIndex = {
128
133
  'admin.app.createAppByTenants': [
134
+ appDiscoveryDocHint,
129
135
  { title: 'Table lifecycle', file: '/opt/arcubase-sdk/docs/runtime-reference/table-lifecycle.md' },
130
136
  examplesIndexDocHint,
131
- omsExampleDocHint,
137
+ ],
138
+ 'admin.app.listApps': [
139
+ appDiscoveryDocHint,
140
+ examplesIndexDocHint,
141
+ ],
142
+ 'admin.app.inventory': [
143
+ appDiscoveryDocHint,
144
+ examplesIndexDocHint,
132
145
  ],
133
146
  'admin.app.createEntity': [
147
+ appDiscoveryDocHint,
134
148
  { title: 'Table lifecycle', file: '/opt/arcubase-sdk/docs/runtime-reference/table-lifecycle.md' },
135
- omsExampleDocHint,
149
+ examplesIndexDocHint,
136
150
  ],
137
151
  'admin.entity.adminGetEntityInfo': [
152
+ appDiscoveryDocHint,
138
153
  { title: 'Table lifecycle', file: '/opt/arcubase-sdk/docs/runtime-reference/table-lifecycle.md' },
139
- omsExampleDocHint,
154
+ examplesIndexDocHint,
140
155
  ],
141
156
  'admin.entity.adminSaveEntity': [
142
157
  { title: 'Entity schema', file: '/opt/arcubase-sdk/docs/runtime-reference/entity-schema.md' },
143
- omsExampleDocHint,
158
+ { title: 'Table lifecycle', file: '/opt/arcubase-sdk/docs/runtime-reference/table-lifecycle.md' },
159
+ { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
160
+ examplesIndexDocHint,
144
161
  ],
145
- 'user.workflow.insertEntity': [
162
+ 'user.rows.insertEntity': [
146
163
  { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
147
- omsExampleDocHint,
164
+ { title: 'Uploads', file: '/opt/arcubase-sdk/docs/runtime-reference/uploads.md' },
165
+ examplesIndexDocHint,
148
166
  ],
149
- 'user.workflow.queryEntity': [
167
+ 'user.rows.queryEntity': [
150
168
  { title: 'Search and bulk actions', file: '/opt/arcubase-sdk/docs/runtime-reference/search-and-bulk-actions.md' },
151
- omsExampleDocHint,
169
+ { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
170
+ examplesIndexDocHint,
152
171
  ],
153
- 'user.workflow.getEntityRow': [
172
+ 'user.rows.getEntityRow': [
154
173
  { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
155
- omsExampleDocHint,
174
+ examplesIndexDocHint,
156
175
  ],
157
- 'user.workflow.updateEntityRow': [
176
+ 'user.rows.updateEntityRow': [
158
177
  { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
159
- omsExampleDocHint,
178
+ { title: 'Uploads', file: '/opt/arcubase-sdk/docs/runtime-reference/uploads.md' },
179
+ examplesIndexDocHint,
160
180
  ],
161
- 'user.workflow.queryEntitySelection': [
181
+ 'user.rows.queryEntitySelection': [
162
182
  { title: 'Search and bulk actions', file: '/opt/arcubase-sdk/docs/runtime-reference/search-and-bulk-actions.md' },
163
- omsExampleDocHint,
183
+ examplesIndexDocHint,
164
184
  ],
165
- 'user.workflow.deleteEntityRow': [
185
+ 'user.rows.deleteEntityRow': [
166
186
  { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
167
- omsExampleDocHint,
187
+ examplesIndexDocHint,
168
188
  ],
169
- 'user.workflow.restoreEntityRow': [
189
+ 'user.rows.restoreEntityRow': [
170
190
  { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
171
- omsExampleDocHint,
191
+ examplesIndexDocHint,
172
192
  ],
173
193
  'user.entity.updateEntityBulk': [
174
194
  { title: 'Search and bulk actions', file: '/opt/arcubase-sdk/docs/runtime-reference/search-and-bulk-actions.md' },
175
- omsExampleDocHint,
195
+ examplesIndexDocHint,
176
196
  ],
177
197
  };
178
198
  export function getCommandDocHints(operation) {
@@ -5,8 +5,15 @@ import { findCommandSuggestions } from '../runtime/command_registry.js';
5
5
  test('admin app create-entity command exists', () => {
6
6
  assert.ok(adminCommands.some((c) => c.commandPath.join(' ') === 'app create-entity'));
7
7
  });
8
- test('user workflow query-entity command exists', () => {
9
- assert.ok(userCommands.some((c) => c.commandPath.join(' ') === 'workflow query-entity'));
8
+ test('admin app list-apps command exists', () => {
9
+ assert.ok(adminCommands.some((c) => c.commandPath.join(' ') === 'app list-apps'));
10
+ });
11
+ test('admin app inventory command exists', () => {
12
+ const suggestions = findCommandSuggestions('app', 'inventory', 20);
13
+ assert.ok(suggestions.some((item) => item.binary === 'arcubase-admin' && item.moduleName === 'app' && item.commandName === 'inventory'));
14
+ });
15
+ test('user rows query-entity command exists', () => {
16
+ assert.ok(userCommands.some((c) => c.commandPath.join(' ') === 'rows query-entity'));
10
17
  });
11
18
  test('inline request type keeps full object text instead of truncated brace', () => {
12
19
  const command = adminCommands.find((c) => c.commandPath.join(' ') === 'dashboard clone-dashboard');
@@ -21,7 +28,7 @@ test('tag commands no longer emit truncated inline request type', () => {
21
28
  assert.match(String(typed.requestType), /^\{\s*policy_id:\s*string.*selection:\s*any.*tag_names:\s*string\[\]\s*\}$/);
22
29
  assert.equal(typed.scalarParams.length, 0);
23
30
  });
24
- test('suggestions can point wrong admin delete guess to user workflow delete command', () => {
31
+ test('suggestions can point wrong admin delete guess to user rows delete command', () => {
25
32
  const suggestions = findCommandSuggestions('entity', 'admin-delete-entity-row');
26
- assert.ok(suggestions.some((item) => item.binary === 'arcubase' && item.moduleName === 'workflow' && item.commandName === 'delete-entity-row'));
33
+ assert.ok(suggestions.some((item) => item.binary === 'arcubase' && item.moduleName === 'rows' && item.commandName === 'delete-entity-row'));
27
34
  });
@@ -60,6 +60,61 @@ test('admin api-prefixed command executes against external root path', async ()
60
60
  assert.equal(out.kind, 'result');
61
61
  assert.equal(out.data.url, 'https://arcubase.example.com/apps/app_1/entity-shares');
62
62
  });
63
+ test('app inventory returns apps with entity lists', async () => {
64
+ const calls = [];
65
+ const out = await executeCLI('admin', ['app', 'inventory'], env, async (url, init) => {
66
+ calls.push(`${init?.method ?? 'GET'} ${String(url)}`);
67
+ if (String(url) === 'https://arcubase.example.com/apps') {
68
+ return new Response(JSON.stringify({
69
+ data: [
70
+ { id: 101, name: 'Workspace' },
71
+ { id: 102, name: 'CRM' },
72
+ ],
73
+ }), { status: 200 });
74
+ }
75
+ if (String(url) === 'https://arcubase.example.com/apps/101/entities') {
76
+ return new Response(JSON.stringify({
77
+ data: [
78
+ { id: 201, name: 'Orders' },
79
+ { id: 202, name: 'Customers' },
80
+ ],
81
+ }), { status: 200 });
82
+ }
83
+ if (String(url) === 'https://arcubase.example.com/apps/102/entities') {
84
+ return new Response(JSON.stringify({
85
+ data: [
86
+ { id: 203, name: 'Leads' },
87
+ ],
88
+ }), { status: 200 });
89
+ }
90
+ return new Response('{}', { status: 404 });
91
+ });
92
+ assert.equal(out.kind, 'result');
93
+ assert.deepEqual(out.data, {
94
+ apps: [
95
+ {
96
+ id: '101',
97
+ name: 'Workspace',
98
+ entities: [
99
+ { id: '201', name: 'Orders' },
100
+ { id: '202', name: 'Customers' },
101
+ ],
102
+ },
103
+ {
104
+ id: '102',
105
+ name: 'CRM',
106
+ entities: [
107
+ { id: '203', name: 'Leads' },
108
+ ],
109
+ },
110
+ ],
111
+ });
112
+ assert.deepEqual(calls, [
113
+ 'GET https://arcubase.example.com/apps',
114
+ 'POST https://arcubase.example.com/apps/101/entities',
115
+ 'POST https://arcubase.example.com/apps/102/entities',
116
+ ]);
117
+ });
63
118
  test('admin save entity treats top-level error body as failure even on http 200', async () => {
64
119
  const bodyFile = tempJSONFile({
65
120
  id: 2188889845,
@@ -183,7 +238,7 @@ test('delete entity row rejects selection type outside ids', async () => {
183
238
  },
184
239
  });
185
240
  await assert.rejects(async () => {
186
- await executeCLI('user', ['workflow', 'delete-entity-row', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile], env, async () => new Response('{}', { status: 200 }));
241
+ await executeCLI('user', ['rows', 'delete-entity-row', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile], env, async () => new Response('{}', { status: 200 }));
187
242
  }, (error) => {
188
243
  assert.ok(error instanceof CLIError);
189
244
  assert.equal(error.code, 'BODY_VALIDATION_FAILED');
@@ -228,7 +283,7 @@ test('selection action accepts condition selection with selectAll', async () =>
228
283
  ],
229
284
  },
230
285
  });
231
- const out = await executeCLI('user', ['workflow', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile], env, async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }));
286
+ const out = await executeCLI('user', ['rows', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile], env, async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }));
232
287
  assert.equal(out.kind, 'result');
233
288
  assert.equal(out.data.selection.type, 'condition');
234
289
  assert.equal(out.data.selection.condition.selectAll, true);
@@ -244,7 +299,7 @@ test('selection action accepts condition selection with filter_* keys', async ()
244
299
  },
245
300
  },
246
301
  });
247
- const out = await executeCLI('user', ['workflow', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile], env, async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }));
302
+ const out = await executeCLI('user', ['rows', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile], env, async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }));
248
303
  assert.equal(out.kind, 'result');
249
304
  assert.equal(out.data.selection.type, 'condition');
250
305
  assert.equal(out.data.selection.condition.selectAll, true);
@@ -259,7 +314,7 @@ test('selection action accepts condition selection with selectAll only for batch
259
314
  },
260
315
  },
261
316
  });
262
- const out = await executeCLI('user', ['workflow', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'batch_print', '--body-file', bodyFile], env, async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }));
317
+ const out = await executeCLI('user', ['rows', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'batch_print', '--body-file', bodyFile], env, async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }));
263
318
  assert.equal(out.kind, 'result');
264
319
  assert.equal(out.data.selection.type, 'condition');
265
320
  assert.equal(out.data.selection.condition.selectAll, true);
@@ -270,7 +325,7 @@ test('selection action accepts all selection for query-style actions', async ()
270
325
  type: 'all',
271
326
  },
272
327
  });
273
- const out = await executeCLI('user', ['workflow', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'query', '--body-file', bodyFile], env, async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }));
328
+ const out = await executeCLI('user', ['rows', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'query', '--body-file', bodyFile], env, async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }));
274
329
  assert.equal(out.kind, 'result');
275
330
  assert.equal(out.data.selection.type, 'all');
276
331
  });
@@ -286,7 +341,7 @@ test('selection action rejects condition selection for query action', async () =
286
341
  },
287
342
  });
288
343
  await assert.rejects(async () => {
289
- await executeCLI('user', ['workflow', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'query', '--body-file', bodyFile], env, async () => new Response('{}', { status: 200 }));
344
+ await executeCLI('user', ['rows', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'query', '--body-file', bodyFile], env, async () => new Response('{}', { status: 200 }));
290
345
  }, (error) => {
291
346
  assert.ok(error instanceof CLIError);
292
347
  assert.equal(error.code, 'BODY_VALIDATION_FAILED');
@@ -301,7 +356,7 @@ test('selection action rejects all selection for archive action', async () => {
301
356
  },
302
357
  });
303
358
  await assert.rejects(async () => {
304
- await executeCLI('user', ['workflow', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile], env, async () => new Response('{}', { status: 200 }));
359
+ await executeCLI('user', ['rows', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile], env, async () => new Response('{}', { status: 200 }));
305
360
  }, (error) => {
306
361
  assert.ok(error instanceof CLIError);
307
362
  assert.equal(error.code, 'BODY_VALIDATION_FAILED');
@@ -320,7 +375,7 @@ test('selection action rejects selectAll-only condition for archive action', asy
320
375
  },
321
376
  });
322
377
  await assert.rejects(async () => {
323
- await executeCLI('user', ['workflow', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile], env, async () => new Response('{}', { status: 200 }));
378
+ await executeCLI('user', ['rows', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile], env, async () => new Response('{}', { status: 200 }));
324
379
  }, (error) => {
325
380
  assert.ok(error instanceof CLIError);
326
381
  assert.equal(error.code, 'BODY_VALIDATION_FAILED');
@@ -328,6 +383,159 @@ test('selection action rejects selectAll-only condition for archive action', asy
328
383
  return true;
329
384
  });
330
385
  });
386
+ test('insert entity rejects local file path for file field and points to arcubase upload', async () => {
387
+ const bodyFile = tempJSONFile({
388
+ fields: {
389
+ 1001: 'Lead A',
390
+ 1007: './contract.pdf',
391
+ },
392
+ });
393
+ const calls = [];
394
+ await assert.rejects(async () => {
395
+ await executeCLI('user', ['rows', 'insert-entity', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile], env, async (url, init) => {
396
+ calls.push({ url: String(url), method: init?.method });
397
+ if (String(url) === 'https://arcubase.example.com/entity/app_1/entity_1' && init?.method === 'GET') {
398
+ return new Response(JSON.stringify({
399
+ data: {
400
+ id: 1,
401
+ app_id: 1,
402
+ fields: [
403
+ { id: 1001, type: 'text', label: 'Lead Name' },
404
+ { id: 1007, type: 'file', label: 'Contract File' },
405
+ ],
406
+ },
407
+ }), { status: 200 });
408
+ }
409
+ return new Response('{}', { status: 200 });
410
+ });
411
+ }, (error) => {
412
+ assert.ok(error instanceof CLIError);
413
+ assert.equal(error.code, 'BODY_VALIDATION_FAILED');
414
+ assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.fields.1007'));
415
+ assert.match(error.message, /arcubase upload \.\/contract\.pdf/);
416
+ assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/uploads.md')));
417
+ return true;
418
+ });
419
+ assert.deepEqual(calls, [
420
+ { url: 'https://arcubase.example.com/entity/app_1/entity_1', method: 'GET' },
421
+ ]);
422
+ });
423
+ test('update row rejects object value for image field and requires upload array', async () => {
424
+ const bodyFile = tempJSONFile({
425
+ changed_fields: [1008],
426
+ data: {
427
+ 1008: {
428
+ upload_id: 123456,
429
+ file: 'lead-photo.jpg',
430
+ file_name: 'lead-photo.jpg',
431
+ meta: {},
432
+ },
433
+ },
434
+ });
435
+ await assert.rejects(async () => {
436
+ await executeCLI('user', ['rows', 'update-entity-row', '--app-id', 'app_1', '--entity-id', 'entity_1', '--row-id', 'row_1', '--body-file', bodyFile], env, async (url, init) => {
437
+ if (String(url) === 'https://arcubase.example.com/entity/app_1/entity_1' && init?.method === 'GET') {
438
+ return new Response(JSON.stringify({
439
+ data: {
440
+ id: 1,
441
+ app_id: 1,
442
+ fields: [
443
+ { id: 1008, type: 'image', label: 'Lead Photo' },
444
+ ],
445
+ },
446
+ }), { status: 200 });
447
+ }
448
+ return new Response('{}', { status: 200 });
449
+ });
450
+ }, (error) => {
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.data.1008'));
454
+ assert.match(error.message, /must be an array/);
455
+ assert.match(error.message, /arcubase upload <local-file>/);
456
+ return true;
457
+ });
458
+ });
459
+ test('insert entity accepts upload array for file field after schema preflight', async () => {
460
+ const bodyFile = tempJSONFile({
461
+ fields: {
462
+ 1001: 'Lead A',
463
+ 1007: [
464
+ {
465
+ upload_id: 123456,
466
+ file: 'contract.pdf',
467
+ file_name: 'contract.pdf',
468
+ meta: {},
469
+ },
470
+ ],
471
+ },
472
+ });
473
+ const calls = [];
474
+ const out = await executeCLI('user', ['rows', 'insert-entity', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile], env, async (url, init) => {
475
+ calls.push({ url: String(url), method: init?.method });
476
+ if (String(url) === 'https://arcubase.example.com/entity/app_1/entity_1' && init?.method === 'GET') {
477
+ return new Response(JSON.stringify({
478
+ data: {
479
+ id: 1,
480
+ app_id: 1,
481
+ fields: [
482
+ { id: 1001, type: 'text', label: 'Lead Name' },
483
+ { id: 1007, type: 'file', label: 'Contract File' },
484
+ ],
485
+ },
486
+ }), { status: 200 });
487
+ }
488
+ return new Response(String(init?.body ?? '{}'), { status: 200 });
489
+ });
490
+ assert.equal(out.kind, 'result');
491
+ assert.deepEqual(calls, [
492
+ { url: 'https://arcubase.example.com/entity/app_1/entity_1', method: 'GET' },
493
+ { url: 'https://arcubase.example.com/entity/app_1/entity_1/insert', method: 'POST' },
494
+ ]);
495
+ assert.equal(out.data.fields['1007'][0].upload_id, 123456);
496
+ });
497
+ test('insert entity decorates upload bind failures with actionable guidance', async () => {
498
+ const bodyFile = tempJSONFile({
499
+ fields: {
500
+ 1001: 'Lead A',
501
+ 1007: [
502
+ {
503
+ upload_id: 123456,
504
+ file: 'contract.pdf',
505
+ file_name: 'contract.pdf',
506
+ meta: {},
507
+ },
508
+ ],
509
+ },
510
+ });
511
+ await assert.rejects(async () => {
512
+ await executeCLI('user', ['rows', 'insert-entity', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile], env, async (url, init) => {
513
+ if (String(url) === 'https://arcubase.example.com/entity/app_1/entity_1' && init?.method === 'GET') {
514
+ return new Response(JSON.stringify({
515
+ data: {
516
+ fields: [{ id: 1007, type: 'file', label: 'Contract File' }],
517
+ },
518
+ }), { status: 200 });
519
+ }
520
+ return new Response(JSON.stringify({
521
+ error: {
522
+ 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',
523
+ type: 'system',
524
+ key: 'error.system',
525
+ },
526
+ trace_id: 'req_upload_bind',
527
+ }), { status: 200 });
528
+ });
529
+ }, (error) => {
530
+ assert.ok(error instanceof CLIError);
531
+ assert.equal(error.code, 'UPSTREAM_BODY_ERROR');
532
+ assert.equal(error.details?.traceId, 'req_upload_bind');
533
+ 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');
534
+ assert.match(error.details?.hint ?? '', /rerun arcubase upload/);
535
+ assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/uploads.md')));
536
+ return true;
537
+ });
538
+ });
331
539
  test('selection action rejects condition selection without selectAll', async () => {
332
540
  const bodyFile = tempJSONFile({
333
541
  selection: {
@@ -339,7 +547,7 @@ test('selection action rejects condition selection without selectAll', async ()
339
547
  },
340
548
  });
341
549
  await assert.rejects(async () => {
342
- await executeCLI('user', ['workflow', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile], env, async () => new Response('{}', { status: 200 }));
550
+ await executeCLI('user', ['rows', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile], env, async () => new Response('{}', { status: 200 }));
343
551
  }, (error) => {
344
552
  assert.ok(error instanceof CLIError);
345
553
  assert.equal(error.code, 'BODY_VALIDATION_FAILED');
@@ -359,7 +567,7 @@ test('selection action rejects condition selection that uses text_search instead
359
567
  },
360
568
  });
361
569
  await assert.rejects(async () => {
362
- await executeCLI('user', ['workflow', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile], env, async () => new Response('{}', { status: 200 }));
570
+ await executeCLI('user', ['rows', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile], env, async () => new Response('{}', { status: 200 }));
363
571
  }, (error) => {
364
572
  assert.ok(error instanceof CLIError);
365
573
  assert.equal(error.code, 'BODY_VALIDATION_FAILED');
@@ -451,7 +659,7 @@ test('query entity rejects camelCase viewMode and points to row crud doc', async
451
659
  viewMode: 'grid',
452
660
  });
453
661
  await assert.rejects(async () => {
454
- await executeCLI('user', ['workflow', 'query-entity', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile], env, async () => new Response('{}', { status: 200 }));
662
+ await executeCLI('user', ['rows', 'query-entity', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile], env, async () => new Response('{}', { status: 200 }));
455
663
  }, (error) => {
456
664
  assert.ok(error instanceof CLIError);
457
665
  assert.equal(error.code, 'BODY_VALIDATION_FAILED');
@@ -471,7 +679,7 @@ test('update entity row rejects fields key', async () => {
471
679
  changed_fields: [1002],
472
680
  });
473
681
  await assert.rejects(async () => {
474
- await executeCLI('user', ['workflow', 'update-entity-row', '--app-id', 'app_1', '--entity-id', 'entity_1', '--row-id', 'row_1', '--body-file', bodyFile], env, async () => new Response('{}', { status: 200 }));
682
+ await executeCLI('user', ['rows', 'update-entity-row', '--app-id', 'app_1', '--entity-id', 'entity_1', '--row-id', 'row_1', '--body-file', bodyFile], env, async () => new Response('{}', { status: 200 }));
475
683
  }, (error) => {
476
684
  assert.ok(error instanceof CLIError);
477
685
  assert.equal(error.code, 'BODY_VALIDATION_FAILED');
@@ -488,7 +696,7 @@ test('update entity row rejects changed_fields that are missing from data', asyn
488
696
  changed_fields: [1003],
489
697
  });
490
698
  await assert.rejects(async () => {
491
- await executeCLI('user', ['workflow', 'update-entity-row', '--app-id', 'app_1', '--entity-id', 'entity_1', '--row-id', 'row_1', '--body-file', bodyFile], env, async () => new Response('{}', { status: 200 }));
699
+ await executeCLI('user', ['rows', 'update-entity-row', '--app-id', 'app_1', '--entity-id', 'entity_1', '--row-id', 'row_1', '--body-file', bodyFile], env, async () => new Response('{}', { status: 200 }));
492
700
  }, (error) => {
493
701
  assert.ok(error instanceof CLIError);
494
702
  assert.equal(error.code, 'BODY_VALIDATION_FAILED');
@@ -24,9 +24,10 @@ test('module help prints create-entity command', async () => {
24
24
  const out = await executeCLI('admin', ['app', '--help'], env, async () => new Response('{}'));
25
25
  assert.equal(out.kind, 'help');
26
26
  assert.match(out.text, /create-entity/);
27
+ assert.match(out.text, /inventory/);
27
28
  });
28
29
  test('module help does not require runtime env', async () => {
29
- const out = await executeCLI('user', ['workflow', '--help'], undefined, async () => new Response('{}'));
30
+ const out = await executeCLI('user', ['rows', '--help'], undefined, async () => new Response('{}'));
30
31
  assert.equal(out.kind, 'help');
31
32
  assert.match(out.text, /delete-entity-row/);
32
33
  });
@@ -37,19 +38,19 @@ test('command help prints normalized external endpoint', async () => {
37
38
  assert.doesNotMatch(out.text, /endpoint: \/api\//);
38
39
  });
39
40
  test('command help does not require runtime env', async () => {
40
- const out = await executeCLI('user', ['workflow', 'delete-entity-row', '--help'], undefined, async () => new Response('{}'));
41
+ const out = await executeCLI('user', ['rows', 'delete-entity-row', '--help'], undefined, async () => new Response('{}'));
41
42
  assert.equal(out.kind, 'help');
42
43
  assert.match(out.text, /endpoint: \/entity\/:app_id\/:entity_id\/delete-item/);
43
44
  });
44
45
  test('command help prints runtime doc hints for request body commands', async () => {
45
- const out = await executeCLI('user', ['workflow', 'update-entity-row', '--help'], undefined, async () => new Response('{}'));
46
+ const out = await executeCLI('user', ['rows', 'update-entity-row', '--help'], undefined, async () => new Response('{}'));
46
47
  assert.equal(out.kind, 'help');
47
48
  assert.match(out.text, /docs:/);
48
49
  assert.match(out.text, /\/opt\/arcubase-sdk\/docs\/runtime-reference\/row-crud\.md/);
49
- assert.match(out.text, /\/opt\/arcubase-sdk\/docs\/runtime-reference\/examples\/oms-01\/README\.md/);
50
+ assert.match(out.text, /\/opt\/arcubase-sdk\/docs\/runtime-reference\/examples\/README\.md/);
50
51
  });
51
52
  test('command help prints query-file support', async () => {
52
- const out = await executeCLI('user', ['workflow', 'query-entity', '--help'], undefined, async () => new Response('{}'));
53
+ const out = await executeCLI('user', ['rows', 'query-entity', '--help'], undefined, async () => new Response('{}'));
53
54
  assert.equal(out.kind, 'help');
54
55
  assert.match(out.text, /query flags: --query-file/);
55
56
  });
@@ -57,5 +58,27 @@ test('command help without request body can still point to lifecycle and example
57
58
  const out = await executeCLI('admin', ['entity', 'admin-get-entity-info', '--help'], undefined, async () => new Response('{}'));
58
59
  assert.equal(out.kind, 'help');
59
60
  assert.match(out.text, /\/opt\/arcubase-sdk\/docs\/runtime-reference\/table-lifecycle\.md/);
60
- assert.match(out.text, /\/opt\/arcubase-sdk\/docs\/runtime-reference\/examples\/oms-01\/README\.md/);
61
+ assert.match(out.text, /\/opt\/arcubase-sdk\/docs\/runtime-reference\/examples\/README\.md/);
62
+ });
63
+ test('app inventory help explains app discovery purpose', async () => {
64
+ const out = await executeCLI('admin', ['app', 'inventory', '--help'], undefined, async () => new Response('{}'));
65
+ assert.equal(out.kind, 'help');
66
+ assert.match(out.text, /list all accessible apps and each app entity list/);
67
+ assert.match(out.text, /use this before asking the user for app_id/);
68
+ });
69
+ test('admin-save-entity help prints row-operation next step', async () => {
70
+ const out = await executeCLI('admin', ['entity', 'admin-save-entity', '--help'], undefined, async () => new Response('{}'));
71
+ assert.equal(out.kind, 'help');
72
+ assert.match(out.text, /switch to arcubase for row operations/);
73
+ assert.match(out.text, /arcubase rows insert-entity/);
74
+ assert.match(out.text, /arcubase rows query-entity/);
75
+ });
76
+ test('wrong admin workflow command points to arcubase row command', async () => {
77
+ await assert.rejects(() => executeCLI('admin', ['workflow', 'insert-entity'], env, async () => new Response('{}')), (error) => {
78
+ assert.equal(error.code, 'UNKNOWN_COMMAND');
79
+ assert.equal(error.details?.reason, 'workflow insert-entity is not a valid command group; use arcubase rows for row operations');
80
+ assert.equal(error.details?.hint, 'use: arcubase rows insert-entity');
81
+ assert.ok((error.details?.suggestions ?? []).includes('arcubase rows insert-entity'));
82
+ return true;
83
+ });
61
84
  });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=upload.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upload.test.d.ts","sourceRoot":"","sources":["../../src/tests/upload.test.ts"],"names":[],"mappings":""}