@carthooks/arcubase-cli 0.1.7 → 0.1.8

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 (66) hide show
  1. package/bundle/arcubase-admin.mjs +1042 -5056
  2. package/bundle/arcubase.mjs +1042 -5056
  3. package/dist/generated/command_registry.generated.d.ts +693 -2812
  4. package/dist/generated/command_registry.generated.d.ts.map +1 -1
  5. package/dist/generated/command_registry.generated.js +812 -4100
  6. package/dist/generated/type_index.generated.d.ts +8 -397
  7. package/dist/generated/type_index.generated.d.ts.map +1 -1
  8. package/dist/generated/type_index.generated.js +8 -403
  9. package/dist/generated/zod_registry.generated.d.ts +33 -261
  10. package/dist/generated/zod_registry.generated.d.ts.map +1 -1
  11. package/dist/generated/zod_registry.generated.js +36 -365
  12. package/dist/runtime/body_loader.d.ts.map +1 -1
  13. package/dist/runtime/body_loader.js +12 -0
  14. package/dist/runtime/command_registry.d.ts +3 -21
  15. package/dist/runtime/command_registry.d.ts.map +1 -1
  16. package/dist/runtime/command_registry.js +17 -23
  17. package/dist/runtime/execute.d.ts +3 -3
  18. package/dist/runtime/execute.d.ts.map +1 -1
  19. package/dist/runtime/execute.js +53 -157
  20. package/dist/runtime/http.js +1 -1
  21. package/dist/runtime/upload.js +3 -3
  22. package/dist/runtime/zod_registry.d.ts.map +1 -1
  23. package/dist/runtime/zod_registry.js +7 -24
  24. package/dist/tests/command_registry.test.js +81 -28
  25. package/dist/tests/execute_validation.test.js +175 -510
  26. package/dist/tests/help.test.js +39 -62
  27. package/dist/tests/upload.test.js +4 -4
  28. package/dist/tests/zod_registry.test.js +0 -10
  29. package/package.json +1 -1
  30. package/sdk-dist/docs/runtime-reference/README.md +20 -30
  31. package/sdk-dist/docs/runtime-reference/entity-schema/README.md +4 -5
  32. package/sdk-dist/docs/runtime-reference/entity-schema/linkto.md +3 -3
  33. package/sdk-dist/docs/runtime-reference/entity-schema/query.md +3 -3
  34. package/sdk-dist/docs/runtime-reference/entity-schema/relation.md +3 -3
  35. package/sdk-dist/docs/runtime-reference/entity-schema/relationfield.md +3 -3
  36. package/sdk-dist/docs/runtime-reference/entity-schema/subform.md +2 -2
  37. package/sdk-dist/docs/runtime-reference/entity-schema.md +4 -4
  38. package/sdk-dist/docs/runtime-reference/examples/README.md +2 -2
  39. package/sdk-dist/docs/runtime-reference/examples/oms-01/README.md +12 -12
  40. package/sdk-dist/docs/runtime-reference/examples/oms-01/app-overview.md +1 -1
  41. package/sdk-dist/docs/runtime-reference/row-crud.md +11 -32
  42. package/sdk-dist/docs/runtime-reference/search-and-bulk-actions.md +16 -47
  43. package/sdk-dist/docs/runtime-reference/table-lifecycle.md +11 -27
  44. package/sdk-dist/docs/runtime-reference/uploads.md +1 -1
  45. package/sdk-dist/docs/runtime-reference/workflow/README.md +19 -0
  46. package/sdk-dist/generated/command_registry.generated.ts +814 -4102
  47. package/sdk-dist/generated/type_index.generated.ts +8 -403
  48. package/sdk-dist/generated/zod_registry.generated.ts +44 -500
  49. package/sdk-dist/types/app.ts +3 -10
  50. package/sdk-dist/types/common.ts +7 -0
  51. package/sdk-dist/types/global-action.ts +2 -1
  52. package/src/generated/command_registry.generated.ts +814 -4102
  53. package/src/generated/type_index.generated.ts +8 -403
  54. package/src/generated/zod_registry.generated.ts +44 -500
  55. package/src/runtime/body_loader.ts +11 -0
  56. package/src/runtime/command_registry.ts +22 -44
  57. package/src/runtime/execute.ts +58 -168
  58. package/src/runtime/http.ts +1 -1
  59. package/src/runtime/upload.ts +3 -3
  60. package/src/runtime/zod_registry.ts +7 -25
  61. package/src/tests/command_registry.test.ts +81 -31
  62. package/src/tests/execute_validation.test.ts +199 -640
  63. package/src/tests/help.test.ts +36 -69
  64. package/src/tests/upload.test.ts +4 -4
  65. package/src/tests/zod_registry.test.ts +0 -13
  66. package/sdk-dist/docs/runtime-reference/app-discovery.md +0 -68
@@ -19,104 +19,8 @@ function tempJSONFile(payload) {
19
19
  fs.writeFileSync(filePath, JSON.stringify(payload, null, 2));
20
20
  return filePath;
21
21
  }
22
- test('admin save entity rejects invalid body file', async () => {
23
- const invalidFile = tempJSONFile({ name: '订单', layout: 'bad-layout' });
24
- await assert.rejects(async () => {
25
- await executeCLI('admin', ['entity', 'admin-save-entity', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', invalidFile], env, async () => new Response('{}'));
26
- }, (error) => {
27
- assert.ok(error instanceof CLIError);
28
- assert.equal(error.code, 'BODY_VALIDATION_FAILED');
29
- assert.equal(error.details?.requestType, 'EntitySaveReqVO');
30
- assert.ok((error.details?.issues ?? []).some((item) => item.path.startsWith('body.layout')));
31
- assert.ok((error.details?.tsHints ?? []).some((item) => item.file.startsWith('/opt/arcubase-sdk/')));
32
- assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/entity-schema.md')));
33
- return true;
34
- });
35
- });
36
- test('admin create app by tenants rejects name longer than 20 chars', async () => {
37
- const bodyFile = tempJSONFile({ name: 'CLI Verify Happy Path' });
38
- await assert.rejects(async () => {
39
- await executeCLI('admin', ['app', 'create-app-by-tenants', '--body-file', bodyFile], env, async () => new Response('{}', { status: 200 }));
40
- }, (error) => {
41
- assert.ok(error instanceof CLIError);
42
- assert.equal(error.code, 'BODY_VALIDATION_FAILED');
43
- assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.name'));
44
- assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/table-lifecycle.md')));
45
- return true;
46
- });
47
- });
48
- test('admin create entity executes with valid body file', async () => {
49
- const bodyFile = tempJSONFile({ name: '订单', parent: 0 });
50
- const out = await executeCLI('admin', ['app', 'create-entity', '--id', 'app_1', '--body-file', bodyFile], env, async (url, init) => new Response(JSON.stringify({ ok: true, url, method: init?.method, headers: init?.headers }), { status: 200 }));
51
- assert.equal(out.kind, 'result');
52
- assert.equal(out.status, 200);
53
- assert.equal(out.data.url, 'https://arcubase.example.com/apps/app_1/entities');
54
- assert.equal(out.data.headers['Authorization'], 'Bearer tok');
55
- assert.equal(out.data.headers['X-Host'], undefined);
56
- assert.equal(out.data.headers['X-Tenant-Id'], undefined);
57
- });
58
- test('admin api-prefixed command executes against external root path', async () => {
59
- const out = await executeCLI('admin', ['app-entity-share', 'get-app-entity-shares', '--app-id', 'app_1'], env, async (url, init) => new Response(JSON.stringify({ ok: true, url, method: init?.method }), { status: 200 }));
60
- assert.equal(out.kind, 'result');
61
- assert.equal(out.data.url, 'https://arcubase.example.com/apps/app_1/entity-shares');
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
- });
118
- test('admin save entity treats top-level error body as failure even on http 200', async () => {
119
- const bodyFile = tempJSONFile({
22
+ function validEntitySavePayload(overrides = {}) {
23
+ return {
120
24
  id: 2188889845,
121
25
  app_id: 2188889844,
122
26
  name: '订单',
@@ -141,83 +45,141 @@ test('admin save entity treats top-level error body as failure even on http 200'
141
45
  depends: [],
142
46
  key: 'order_id',
143
47
  code_index: false,
144
- options: {},
48
+ options: {
49
+ placeholder: '',
50
+ type: 'text',
51
+ lengthLimit: false,
52
+ lengthMin: 0,
53
+ lengthMax: 255,
54
+ },
145
55
  transfers: null,
146
56
  children: null,
147
57
  },
148
58
  ],
59
+ options: {
60
+ Buttons: [],
61
+ CustomActions: [],
62
+ FrontendEvents: [],
63
+ TitleFormat: {
64
+ Fields: [],
65
+ Parts: [],
66
+ Body: '',
67
+ },
68
+ misc: {},
69
+ },
149
70
  workflow_enabled: false,
71
+ ...overrides,
72
+ };
73
+ }
74
+ test('table update-schema rejects invalid body file', async () => {
75
+ const invalidFile = tempJSONFile({ name: '订单', layout: 'bad-layout' });
76
+ await assert.rejects(async () => {
77
+ await executeCLI('admin', ['table', 'update-schema', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', invalidFile], env, async () => new Response('{}'));
78
+ }, (error) => {
79
+ assert.ok(error instanceof CLIError);
80
+ assert.equal(error.code, 'BODY_VALIDATION_FAILED');
81
+ assert.equal(error.details?.requestType, 'EntitySaveReqVO');
82
+ assert.ok((error.details?.issues ?? []).some((item) => item.path.startsWith('body.layout')));
83
+ return true;
150
84
  });
85
+ });
86
+ test('app create rejects name longer than 20 chars', async () => {
87
+ const bodyFile = tempJSONFile({ name: 'CLI Verify Happy Path' });
151
88
  await assert.rejects(async () => {
152
- await executeCLI('admin', ['entity', 'admin-save-entity', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile], env, async () => new Response(JSON.stringify({
153
- error: {
154
- message: 'json: cannot unmarshal object into Go struct field .EntityVO.fields of type []models.Field',
155
- type: 'system',
156
- key: 'error.system',
157
- },
89
+ await executeCLI('admin', ['app', 'create', '--body-file', bodyFile], env, async () => new Response('{}', { status: 200 }));
90
+ }, (error) => {
91
+ assert.ok(error instanceof CLIError);
92
+ assert.equal(error.code, 'BODY_VALIDATION_FAILED');
93
+ assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.name'));
94
+ return true;
95
+ });
96
+ });
97
+ test('table create executes with valid body file', async () => {
98
+ const bodyFile = tempJSONFile({ name: '订单', parent: 0 });
99
+ const out = await executeCLI('admin', ['table', 'create', '--app-id', 'app_1', '--body-file', bodyFile], env, async (url, init) => new Response(JSON.stringify({
100
+ url,
101
+ method: init?.method,
102
+ hasTenantHeader: Object.prototype.hasOwnProperty.call(init?.headers ?? {}, 'X-Tenant-Id'),
103
+ hasHostHeader: Object.prototype.hasOwnProperty.call(init?.headers ?? {}, 'X-Host'),
104
+ }), { status: 200 }));
105
+ assert.equal(out.kind, 'result');
106
+ assert.equal(out.data.url, 'https://arcubase.example.com/api/apps/app_1/entities');
107
+ assert.equal(out.data.hasTenantHeader, false);
108
+ assert.equal(out.data.hasHostHeader, false);
109
+ });
110
+ test('table create executes with valid body json', async () => {
111
+ const out = await executeCLI('admin', ['table', 'create', '--app-id', 'app_1', '--body-json', '{"name":"订单","parent":0}'], env, async (url, init) => new Response(JSON.stringify({
112
+ url,
113
+ method: init?.method,
114
+ body: init?.body ? JSON.parse(String(init.body)) : null,
115
+ }), { status: 200 }));
116
+ assert.equal(out.kind, 'result');
117
+ assert.equal(out.data.url, 'https://arcubase.example.com/api/apps/app_1/entities');
118
+ assert.deepEqual(out.data.body, { name: '订单', parent: 0 });
119
+ });
120
+ test('body json conflicts with body file', async () => {
121
+ const bodyFile = tempJSONFile({ name: '订单', parent: 0 });
122
+ await assert.rejects(async () => {
123
+ await executeCLI('admin', ['table', 'create', '--app-id', 'app_1', '--body-file', bodyFile, '--body-json', '{"name":"订单"}'], env, async () => new Response('{}', { status: 200 }));
124
+ }, (error) => {
125
+ assert.ok(error instanceof CLIError);
126
+ assert.equal(error.code, 'BODY_JSON_CONFLICT');
127
+ return true;
128
+ });
129
+ });
130
+ test('body json is rejected for commands without request type', async () => {
131
+ await assert.rejects(async () => {
132
+ await executeCLI('admin', ['app', 'list', '--body-json', '{"name":"ignored"}'], env, async () => new Response('{}', { status: 200 }));
133
+ }, (error) => {
134
+ assert.ok(error instanceof CLIError);
135
+ assert.equal(error.code, 'BODY_JSON_NOT_SUPPORTED');
136
+ return true;
137
+ });
138
+ });
139
+ test('body json parse errors fail fast', async () => {
140
+ await assert.rejects(async () => {
141
+ await executeCLI('admin', ['table', 'create', '--app-id', 'app_1', '--body-json', '{"name":'], env, async () => new Response('{}', { status: 200 }));
142
+ }, (error) => {
143
+ assert.ok(error instanceof CLIError);
144
+ assert.equal(error.code, 'INVALID_BODY_JSON');
145
+ return true;
146
+ });
147
+ });
148
+ test('access-rule list executes against external root path', async () => {
149
+ const out = await executeCLI('admin', ['access-rule', 'list', '--app-id', 'app_1', '--table-id', 'table_1'], env, async (url, init) => new Response(JSON.stringify({ url, method: init?.method }), { status: 200 }));
150
+ assert.equal(out.kind, 'result');
151
+ assert.equal(out.data.url, 'https://arcubase.example.com/api/apps/app_1/entity/table_1/ingress');
152
+ });
153
+ test('table update-schema treats top-level error body as failure even on http 200', async () => {
154
+ const bodyFile = tempJSONFile(validEntitySavePayload());
155
+ await assert.rejects(async () => {
156
+ await executeCLI('admin', ['table', 'update-schema', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile], env, async () => new Response(JSON.stringify({
157
+ error: { message: 'json: cannot unmarshal object into Go struct field .EntityVO.fields of type []models.Field', type: 'system', key: 'error.system' },
158
158
  trace_id: 'req_xxx',
159
159
  }), { status: 200 }));
160
160
  }, (error) => {
161
161
  assert.ok(error instanceof CLIError);
162
162
  assert.equal(error.code, 'UPSTREAM_BODY_ERROR');
163
- assert.equal(error.details?.endpoint, '/apps/app_1/entity/entity_1');
163
+ assert.equal(error.details?.endpoint, '/api/apps/app_1/entity/table_1');
164
164
  assert.equal(error.details?.method, 'PUT');
165
165
  assert.equal(error.details?.traceId, 'req_xxx');
166
166
  return true;
167
167
  });
168
168
  });
169
- test('admin save entity normalizes shell null placeholders before validation', async () => {
170
- const bodyFile = tempJSONFile({
171
- id: 2188889845,
172
- app_id: 2188889844,
173
- name: '订单',
169
+ test('table update-schema normalizes shell null placeholders before validation', async () => {
170
+ const bodyFile = tempJSONFile(validEntitySavePayload({
174
171
  schema_version: 0,
175
- field_id_seq: 1002,
176
172
  layout: null,
177
173
  current_serial: null,
178
- fields: [
179
- {
180
- id: 1001,
181
- label: '订单号',
182
- type: 'text',
183
- required: true,
184
- unique: false,
185
- editable: true,
186
- visible: true,
187
- show_label: true,
188
- scannable: false,
189
- default_value_mode: 0,
190
- description: '',
191
- value: null,
192
- number_decimal: 0,
193
- depends: [],
194
- key: 'order_id',
195
- code_index: false,
196
- options: {
197
- placeholder: '',
198
- type: 'text',
199
- lengthLimit: false,
200
- lengthMin: 0,
201
- lengthMax: 255,
202
- },
203
- transfers: null,
204
- children: null,
205
- },
206
- ],
207
174
  options: {
208
175
  Buttons: null,
209
176
  CustomActions: null,
210
177
  FrontendEvents: null,
211
- TitleFormat: {
212
- Fields: null,
213
- Parts: null,
214
- Body: '',
215
- },
178
+ TitleFormat: { Fields: null, Parts: null, Body: '' },
216
179
  misc: null,
217
180
  },
218
- workflow_enabled: false,
219
- });
220
- const out = await executeCLI('admin', ['entity', 'admin-save-entity', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile], env, async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }));
181
+ }));
182
+ const out = await executeCLI('admin', ['table', 'update-schema', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile], env, async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }));
221
183
  assert.equal(out.kind, 'result');
222
184
  const requestBody = out.data;
223
185
  assert.deepEqual(requestBody.layout, []);
@@ -229,119 +191,45 @@ test('admin save entity normalizes shell null placeholders before validation', a
229
191
  assert.deepEqual(requestBody.options.TitleFormat.Parts, []);
230
192
  assert.deepEqual(requestBody.options.misc, {});
231
193
  });
232
- test('delete entity row rejects selection type outside ids', async () => {
233
- const bodyFile = tempJSONFile({
234
- selection: {
235
- type: 'selection',
236
- ids: [4000000003],
237
- length: 1,
238
- },
239
- });
194
+ test('row delete rejects selection type outside ids', async () => {
195
+ const bodyFile = tempJSONFile({ selection: { type: 'selection', ids: [4000000003], length: 1 } });
240
196
  await assert.rejects(async () => {
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 }));
197
+ await executeCLI('user', ['row', 'delete', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile], env, async () => new Response('{}', { status: 200 }));
242
198
  }, (error) => {
243
199
  assert.ok(error instanceof CLIError);
244
200
  assert.equal(error.code, 'BODY_VALIDATION_FAILED');
245
201
  assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.selection'));
246
- assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/search-and-bulk-actions.md')));
247
202
  return true;
248
203
  });
249
204
  });
250
- test('bulk update accepts ids selection with number_add action', async () => {
205
+ test('row bulk-update accepts ids selection with number_add action', async () => {
251
206
  const bodyFile = tempJSONFile({
252
- selection: {
253
- type: 'ids',
254
- ids: [4000000003],
255
- length: 1,
256
- },
257
- fields: [
258
- {
259
- id: 1002,
260
- action: {
261
- type: 'number_add',
262
- number_add: 10,
263
- },
264
- },
265
- ],
207
+ selection: { type: 'ids', ids: [4000000003], length: 1 },
208
+ fields: [{ id: 1002, action: { type: 'number_add', number_add: 10 } }],
266
209
  });
267
- const out = await executeCLI('user', ['entity', 'update-entity-bulk', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile], env, async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }));
210
+ const out = await executeCLI('user', ['row', 'bulk-update', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile], env, async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }));
268
211
  assert.equal(out.kind, 'result');
269
212
  assert.deepEqual(out.data.selection, { type: 'ids', ids: [4000000003], length: 1 });
270
213
  assert.equal(out.data.fields[0].action.type, 'number_add');
271
214
  });
272
- test('selection action accepts condition selection with selectAll', async () => {
215
+ test('row selection-action accepts condition selection with selectAll', async () => {
273
216
  const bodyFile = tempJSONFile({
274
217
  selection: {
275
218
  type: 'condition',
276
219
  length: 1,
277
- condition: {
278
- q: 'A-100',
279
- selectAll: true,
280
- },
281
- sorts: [
282
- { column: ':1002', order: 'desc' },
283
- ],
220
+ condition: { q: 'A-100', selectAll: true },
221
+ sorts: [{ column: ':1002', order: 'desc' }],
284
222
  },
285
223
  });
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 }));
224
+ const out = await executeCLI('user', ['row', 'selection-action', '--app-id', 'app_1', '--table-id', 'table_1', '--action', 'archive', '--body-file', bodyFile], env, async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }));
287
225
  assert.equal(out.kind, 'result');
288
226
  assert.equal(out.data.selection.type, 'condition');
289
227
  assert.equal(out.data.selection.condition.selectAll, true);
290
228
  });
291
- test('selection action accepts condition selection with filter_* keys', async () => {
292
- const bodyFile = tempJSONFile({
293
- selection: {
294
- type: 'condition',
295
- length: 1,
296
- condition: {
297
- 'filter_:1001': '%7B%22is%22%3A%22A-100%22%7D',
298
- selectAll: true,
299
- },
300
- },
301
- });
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 }));
303
- assert.equal(out.kind, 'result');
304
- assert.equal(out.data.selection.type, 'condition');
305
- assert.equal(out.data.selection.condition.selectAll, true);
306
- });
307
- test('selection action accepts condition selection with selectAll only for batch_print', async () => {
308
- const bodyFile = tempJSONFile({
309
- selection: {
310
- type: 'condition',
311
- length: 1,
312
- condition: {
313
- selectAll: true,
314
- },
315
- },
316
- });
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 }));
318
- assert.equal(out.kind, 'result');
319
- assert.equal(out.data.selection.type, 'condition');
320
- assert.equal(out.data.selection.condition.selectAll, true);
321
- });
322
- test('selection action accepts all selection for query-style actions', async () => {
323
- const bodyFile = tempJSONFile({
324
- selection: {
325
- type: 'all',
326
- },
327
- });
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 }));
329
- assert.equal(out.kind, 'result');
330
- assert.equal(out.data.selection.type, 'all');
331
- });
332
- test('selection action rejects condition selection for query action', async () => {
333
- const bodyFile = tempJSONFile({
334
- selection: {
335
- type: 'condition',
336
- length: 1,
337
- condition: {
338
- q: 'A-100',
339
- selectAll: true,
340
- },
341
- },
342
- });
229
+ test('row selection-action rejects condition selection for query action', async () => {
230
+ const bodyFile = tempJSONFile({ selection: { type: 'condition', length: 1, condition: { q: 'A-100', selectAll: true } } });
343
231
  await assert.rejects(async () => {
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 }));
232
+ await executeCLI('user', ['row', 'selection-action', '--app-id', 'app_1', '--table-id', 'table_1', '--action', 'query', '--body-file', bodyFile], env, async () => new Response('{}', { status: 200 }));
345
233
  }, (error) => {
346
234
  assert.ok(error instanceof CLIError);
347
235
  assert.equal(error.code, 'BODY_VALIDATION_FAILED');
@@ -349,62 +237,14 @@ test('selection action rejects condition selection for query action', async () =
349
237
  return true;
350
238
  });
351
239
  });
352
- test('selection action rejects all selection for archive action', async () => {
353
- const bodyFile = tempJSONFile({
354
- selection: {
355
- type: 'all',
356
- },
357
- });
358
- await assert.rejects(async () => {
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 }));
360
- }, (error) => {
361
- assert.ok(error instanceof CLIError);
362
- assert.equal(error.code, 'BODY_VALIDATION_FAILED');
363
- assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.selection.type'));
364
- return true;
365
- });
366
- });
367
- test('selection action rejects selectAll-only condition for archive action', async () => {
368
- const bodyFile = tempJSONFile({
369
- selection: {
370
- type: 'condition',
371
- length: 1,
372
- condition: {
373
- selectAll: true,
374
- },
375
- },
376
- });
377
- await assert.rejects(async () => {
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 }));
379
- }, (error) => {
380
- assert.ok(error instanceof CLIError);
381
- assert.equal(error.code, 'BODY_VALIDATION_FAILED');
382
- assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.selection.condition'));
383
- return true;
384
- });
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
- });
240
+ test('row create rejects local file path for file field and points to arcubase upload', async () => {
241
+ const bodyFile = tempJSONFile({ fields: { 1001: 'Lead A', 1007: './contract.pdf' } });
393
242
  const calls = [];
394
243
  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) => {
244
+ await executeCLI('user', ['row', 'create', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile], env, async (url, init) => {
396
245
  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 });
246
+ if (String(url) === 'https://arcubase.example.com/api/entity/app_1/table_1' && init?.method === 'GET') {
247
+ return new Response(JSON.stringify({ data: { id: 1, app_id: 1, fields: [{ id: 1001, type: 'text', label: 'Lead Name' }, { id: 1007, type: 'file', label: 'Contract File' }] } }), { status: 200 });
408
248
  }
409
249
  return new Response('{}', { status: 200 });
410
250
  });
@@ -413,37 +253,16 @@ test('insert entity rejects local file path for file field and points to arcubas
413
253
  assert.equal(error.code, 'BODY_VALIDATION_FAILED');
414
254
  assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.fields.1007'));
415
255
  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
256
  return true;
418
257
  });
419
- assert.deepEqual(calls, [
420
- { url: 'https://arcubase.example.com/entity/app_1/entity_1', method: 'GET' },
421
- ]);
258
+ assert.deepEqual(calls, [{ url: 'https://arcubase.example.com/api/entity/app_1/table_1', method: 'GET' }]);
422
259
  });
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
- });
260
+ test('row update rejects object value for image field and requires upload array', async () => {
261
+ const bodyFile = tempJSONFile({ changed_fields: [1008], data: { 1008: { upload_id: 123456, file: 'lead-photo.jpg', file_name: 'lead-photo.jpg', meta: {} } } });
435
262
  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 });
263
+ await executeCLI('user', ['row', 'update', '--app-id', 'app_1', '--table-id', 'table_1', '--row-id', 'row_1', '--body-file', bodyFile], env, async (url, init) => {
264
+ if (String(url) === 'https://arcubase.example.com/api/entity/app_1/table_1' && init?.method === 'GET') {
265
+ return new Response(JSON.stringify({ data: { id: 1, app_id: 1, fields: [{ id: 1008, type: 'image', label: 'Lead Photo' }] } }), { status: 200 });
447
266
  }
448
267
  return new Response('{}', { status: 200 });
449
268
  });
@@ -452,74 +271,36 @@ test('update row rejects object value for image field and requires upload array'
452
271
  assert.equal(error.code, 'BODY_VALIDATION_FAILED');
453
272
  assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.data.1008'));
454
273
  assert.match(error.message, /must be an array/);
455
- assert.match(error.message, /arcubase upload <local-file>/);
456
274
  return true;
457
275
  });
458
276
  });
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
- });
277
+ test('row create accepts upload array for file field after schema preflight', async () => {
278
+ const bodyFile = tempJSONFile({ fields: { 1001: 'Lead A', 1007: [{ upload_id: 123456, file: 'contract.pdf', file_name: 'contract.pdf', meta: {} }] } });
473
279
  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) => {
280
+ const out = await executeCLI('user', ['row', 'create', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile], env, async (url, init) => {
475
281
  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 });
282
+ if (String(url) === 'https://arcubase.example.com/api/entity/app_1/table_1' && init?.method === 'GET') {
283
+ return new Response(JSON.stringify({ data: { id: 1, app_id: 1, fields: [{ id: 1001, type: 'text', label: 'Lead Name' }, { id: 1007, type: 'file', label: 'Contract File' }] } }), { status: 200 });
487
284
  }
488
285
  return new Response(String(init?.body ?? '{}'), { status: 200 });
489
286
  });
490
287
  assert.equal(out.kind, 'result');
491
288
  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' },
289
+ { url: 'https://arcubase.example.com/api/entity/app_1/table_1', method: 'GET' },
290
+ { url: 'https://arcubase.example.com/api/entity/app_1/table_1/insert', method: 'POST' },
494
291
  ]);
495
292
  assert.equal(out.data.fields['1007'][0].upload_id, 123456);
496
293
  });
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
- });
294
+ test('row create decorates upload bind failures with actionable guidance', async () => {
295
+ const bodyFile = tempJSONFile({ fields: { 1001: 'Lead A', 1007: [{ upload_id: 123456, file: 'contract.pdf', file_name: 'contract.pdf', meta: {} }] } });
511
296
  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 });
297
+ await executeCLI('user', ['row', 'create', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile], env, async (url, init) => {
298
+ if (String(url) === 'https://arcubase.example.com/api/entity/app_1/table_1' && init?.method === 'GET') {
299
+ return new Response(JSON.stringify({ data: { fields: [{ id: 1007, type: 'file', label: 'Contract File' }] } }), { status: 200 });
519
300
  }
520
301
  return new Response(JSON.stringify({
521
302
  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',
303
+ 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
304
  type: 'system',
524
305
  key: 'error.system',
525
306
  },
@@ -532,176 +313,60 @@ test('insert entity decorates upload bind failures with actionable guidance', as
532
313
  assert.equal(error.details?.traceId, 'req_upload_bind');
533
314
  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
315
  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
316
  return true;
537
317
  });
538
318
  });
539
- test('selection action rejects condition selection without selectAll', async () => {
540
- const bodyFile = tempJSONFile({
541
- selection: {
542
- type: 'condition',
543
- length: 1,
544
- condition: {
545
- q: 'A-100',
546
- },
547
- },
548
- });
319
+ test('row query rejects camelCase viewMode and points to row crud doc', async () => {
320
+ const bodyFile = tempJSONFile({ limit: 20, offset: 0, viewMode: 'grid' });
549
321
  await assert.rejects(async () => {
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 }));
322
+ await executeCLI('user', ['row', 'query', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile], env, async () => new Response('{}', { status: 200 }));
551
323
  }, (error) => {
552
324
  assert.ok(error instanceof CLIError);
553
325
  assert.equal(error.code, 'BODY_VALIDATION_FAILED');
554
- assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.selection.condition.selectAll'));
326
+ assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.viewMode'));
555
327
  return true;
556
328
  });
557
329
  });
558
- test('selection action rejects condition selection that uses text_search instead of q/filter keys', async () => {
559
- const bodyFile = tempJSONFile({
560
- selection: {
561
- type: 'condition',
562
- length: 1,
563
- condition: {
564
- text_search: 'A-100',
565
- selectAll: true,
566
- },
567
- },
568
- });
330
+ test('row update rejects fields key', async () => {
331
+ const bodyFile = tempJSONFile({ fields: { 1002: 99 }, data: { '1002': 99 }, changed_fields: [1002] });
569
332
  await assert.rejects(async () => {
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 }));
333
+ await executeCLI('user', ['row', 'update', '--app-id', 'app_1', '--table-id', 'table_1', '--row-id', 'row_1', '--body-file', bodyFile], env, async () => new Response('{}', { status: 200 }));
571
334
  }, (error) => {
572
335
  assert.ok(error instanceof CLIError);
573
336
  assert.equal(error.code, 'BODY_VALIDATION_FAILED');
574
- assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.selection.condition'));
575
- assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/search-and-bulk-actions.md')));
337
+ assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.fields'));
576
338
  return true;
577
339
  });
578
340
  });
579
- test('bulk update accepts condition selection with q', async () => {
580
- const bodyFile = tempJSONFile({
581
- selection: {
582
- type: 'condition',
583
- length: 1,
584
- condition: {
585
- q: 'A-100',
586
- selectAll: true,
587
- },
588
- sorts: [
589
- { column: ':1002', order: 'desc' },
590
- ],
591
- },
592
- fields: [
593
- {
594
- id: 1002,
595
- action: {
596
- type: 'number_add',
597
- number_add: 2,
598
- },
599
- },
600
- ],
601
- });
602
- const out = await executeCLI('user', ['entity', 'update-entity-bulk', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile], env, async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }));
603
- assert.equal(out.kind, 'result');
604
- assert.equal(out.data.selection.type, 'condition');
605
- assert.equal(out.data.fields[0].action.type, 'number_add');
606
- });
607
- test('entity tags get-selection-tags accepts ids selection', async () => {
608
- const bodyFile = tempJSONFile({
609
- policy_id: '',
610
- selection: {
611
- type: 'ids',
612
- ids: [4000000003],
613
- length: 1,
614
- },
615
- });
616
- const out = await executeCLI('admin', ['entity-tags', 'get-selection-tags', '--appId', 'app_1', '--entityId', 'entity_1', '--body-file', bodyFile], env, async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }));
617
- assert.equal(out.kind, 'result');
618
- assert.equal(out.data.selection.type, 'ids');
619
- });
620
- test('entity tags batch-add accepts tag_names and condition selection', async () => {
621
- const bodyFile = tempJSONFile({
622
- policy_id: '',
623
- selection: {
624
- type: 'condition',
625
- length: 1,
626
- condition: {
627
- q: 'A-100',
628
- selectAll: true,
629
- },
630
- },
631
- tag_names: ['tag-a'],
632
- });
633
- const out = await executeCLI('admin', ['entity-tags', 'batch-add-entity-tags', '--appId', 'app_1', '--entityId', 'entity_1', '--body-file', bodyFile], env, async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }));
634
- assert.equal(out.kind, 'result');
635
- assert.equal(out.data.tag_names[0], 'tag-a');
636
- });
637
- test('invoke batch operator accepts backend request shape', async () => {
638
- const bodyFile = tempJSONFile({
639
- id: 'event_1',
640
- data: {
641
- action_id: 'action_1',
642
- policy_id: '',
643
- selection: {
644
- type: 'all',
645
- },
646
- },
647
- params: {},
648
- });
649
- const out = await executeCLI('user', ['entity', 'invoke-entity-batch-operator', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile], env, async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }));
650
- assert.equal(out.kind, 'result');
651
- assert.equal(out.data.id, 'event_1');
652
- assert.equal(out.data.data.action_id, 'action_1');
653
- assert.equal(out.data.data.selection.type, 'all');
654
- });
655
- test('query entity rejects camelCase viewMode and points to row crud doc', async () => {
656
- const bodyFile = tempJSONFile({
657
- limit: 20,
658
- offset: 0,
659
- viewMode: 'grid',
660
- });
341
+ test('row update rejects changed_fields that are missing from data', async () => {
342
+ const bodyFile = tempJSONFile({ data: { '1002': 99 }, changed_fields: [1003] });
661
343
  await assert.rejects(async () => {
662
- await executeCLI('user', ['rows', 'query-entity', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile], env, async () => new Response('{}', { status: 200 }));
344
+ await executeCLI('user', ['row', 'update', '--app-id', 'app_1', '--table-id', 'table_1', '--row-id', 'row_1', '--body-file', bodyFile], env, async () => new Response('{}', { status: 200 }));
663
345
  }, (error) => {
664
346
  assert.ok(error instanceof CLIError);
665
347
  assert.equal(error.code, 'BODY_VALIDATION_FAILED');
666
- assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.viewMode'));
667
- assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/row-crud.md')));
348
+ assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.changed_fields.0'));
668
349
  return true;
669
350
  });
670
351
  });
671
- test('update entity row rejects fields key', async () => {
672
- const bodyFile = tempJSONFile({
673
- fields: {
674
- 1002: 99,
675
- },
676
- data: {
677
- '1002': 99,
678
- },
679
- changed_fields: [1002],
680
- });
681
- await assert.rejects(async () => {
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 }));
683
- }, (error) => {
684
- assert.ok(error instanceof CLIError);
685
- assert.equal(error.code, 'BODY_VALIDATION_FAILED');
686
- assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.fields'));
687
- assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/row-crud.md')));
688
- return true;
689
- });
352
+ test('workflow approve maps to fixed action path', async () => {
353
+ const bodyFile = tempJSONFile({ task_id: 1001, fields: {}, changed: [] });
354
+ const out = await executeCLI('user', ['workflow', 'approve', '--app-id', 'app_1', '--table-id', 'table_1', '--row-id', 'row_1', '--body-file', bodyFile], env, async (url, init) => new Response(JSON.stringify({ url, method: init?.method, body: init?.body ? JSON.parse(String(init.body)) : null }), { status: 200 }));
355
+ assert.equal(out.kind, 'result');
356
+ assert.equal(out.data.url, 'https://arcubase.example.com/api/entity/app_1/table_1/row/row_1/action/2');
357
+ assert.equal(out.data.body.task_id, 1001);
690
358
  });
691
- test('update entity row rejects changed_fields that are missing from data', async () => {
692
- const bodyFile = tempJSONFile({
693
- data: {
694
- '1002': 99,
695
- },
696
- changed_fields: [1003],
697
- });
359
+ test('workflow query-approve-users requires request body', async () => {
698
360
  await assert.rejects(async () => {
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 }));
361
+ await executeCLI('user', ['workflow', 'query-approve-users', '--app-id', 'app_1', '--table-id', 'table_1'], env, async () => new Response('{}', { status: 200 }));
700
362
  }, (error) => {
701
363
  assert.ok(error instanceof CLIError);
702
- assert.equal(error.code, 'BODY_VALIDATION_FAILED');
703
- assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.changed_fields.0'));
704
- assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/row-crud.md')));
364
+ assert.equal(error.code, 'MISSING_BODY_FILE');
705
365
  return true;
706
366
  });
707
367
  });
368
+ test('entry passes include_app_tables query flag', async () => {
369
+ const out = await executeCLI('user', ['entry', '--include-app-tables', 'true'], env, async (url, init) => new Response(JSON.stringify({ url, method: init?.method }), { status: 200 }));
370
+ assert.equal(out.kind, 'result');
371
+ assert.equal(out.data.url, 'https://arcubase.example.com/api/entry?include_app_tables=true');
372
+ });