@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
@@ -22,242 +22,200 @@ function tempJSONFile(payload: unknown): string {
22
22
  return filePath
23
23
  }
24
24
 
25
- test('admin save entity rejects invalid body file', async () => {
25
+ function validEntitySavePayload(overrides: Record<string, unknown> = {}) {
26
+ return {
27
+ id: 2188889845,
28
+ app_id: 2188889844,
29
+ name: '订单',
30
+ schema_version: 1,
31
+ field_id_seq: 1002,
32
+ layout: [[1001]],
33
+ fields: [
34
+ {
35
+ id: 1001,
36
+ label: '订单号',
37
+ type: 'text',
38
+ required: true,
39
+ unique: false,
40
+ editable: true,
41
+ visible: true,
42
+ show_label: true,
43
+ scannable: false,
44
+ default_value_mode: 0,
45
+ description: '',
46
+ value: null,
47
+ number_decimal: 0,
48
+ depends: [],
49
+ key: 'order_id',
50
+ code_index: false,
51
+ options: {
52
+ placeholder: '',
53
+ type: 'text',
54
+ lengthLimit: false,
55
+ lengthMin: 0,
56
+ lengthMax: 255,
57
+ },
58
+ transfers: null,
59
+ children: null,
60
+ },
61
+ ],
62
+ options: {
63
+ Buttons: [],
64
+ CustomActions: [],
65
+ FrontendEvents: [],
66
+ TitleFormat: {
67
+ Fields: [],
68
+ Parts: [],
69
+ Body: '',
70
+ },
71
+ misc: {},
72
+ },
73
+ workflow_enabled: false,
74
+ ...overrides,
75
+ }
76
+ }
77
+
78
+ test('table update-schema rejects invalid body file', async () => {
26
79
  const invalidFile = tempJSONFile({ name: '订单', layout: 'bad-layout' })
27
80
  await assert.rejects(async () => {
28
- await executeCLI('admin', ['entity', 'admin-save-entity', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', invalidFile], env as any, async () => new Response('{}'))
81
+ await executeCLI('admin', ['table', 'update-schema', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', invalidFile], env as any, async () => new Response('{}'))
29
82
  }, (error: unknown) => {
30
83
  assert.ok(error instanceof CLIError)
31
84
  assert.equal(error.code, 'BODY_VALIDATION_FAILED')
32
85
  assert.equal(error.details?.requestType, 'EntitySaveReqVO')
33
86
  assert.ok((error.details?.issues ?? []).some((item) => item.path.startsWith('body.layout')))
34
- assert.ok((error.details?.tsHints ?? []).some((item) => item.file.startsWith('/opt/arcubase-sdk/')))
35
- assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/entity-schema.md')))
36
87
  return true
37
88
  })
38
89
  })
39
90
 
40
- test('admin create app by tenants rejects name longer than 20 chars', async () => {
91
+ test('app create rejects name longer than 20 chars', async () => {
41
92
  const bodyFile = tempJSONFile({ name: 'CLI Verify Happy Path' })
42
93
  await assert.rejects(async () => {
43
- await executeCLI(
44
- 'admin',
45
- ['app', 'create-app-by-tenants', '--body-file', bodyFile],
46
- env as any,
47
- async () => new Response('{}', { status: 200 }),
48
- )
94
+ await executeCLI('admin', ['app', 'create', '--body-file', bodyFile], env as any, async () => new Response('{}', { status: 200 }))
49
95
  }, (error: unknown) => {
50
96
  assert.ok(error instanceof CLIError)
51
97
  assert.equal(error.code, 'BODY_VALIDATION_FAILED')
52
98
  assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.name'))
53
- assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/table-lifecycle.md')))
54
99
  return true
55
100
  })
56
101
  })
57
102
 
58
- test('admin create entity executes with valid body file', async () => {
103
+ test('table create executes with valid body file', async () => {
59
104
  const bodyFile = tempJSONFile({ name: '订单', parent: 0 })
60
105
  const out = await executeCLI(
61
106
  'admin',
62
- ['app', 'create-entity', '--id', 'app_1', '--body-file', bodyFile],
107
+ ['table', 'create', '--app-id', 'app_1', '--body-file', bodyFile],
63
108
  env as any,
64
- async (url, init) => new Response(JSON.stringify({ ok: true, url, method: init?.method, headers: init?.headers }), { status: 200 })
109
+ async (url, init) =>
110
+ new Response(JSON.stringify({
111
+ url,
112
+ method: init?.method,
113
+ hasTenantHeader: Object.prototype.hasOwnProperty.call((init?.headers as Record<string, string>) ?? {}, 'X-Tenant-Id'),
114
+ hasHostHeader: Object.prototype.hasOwnProperty.call((init?.headers as Record<string, string>) ?? {}, 'X-Host'),
115
+ }), { status: 200 }),
65
116
  )
66
117
  assert.equal(out.kind, 'result')
67
- assert.equal(out.status, 200)
68
- assert.equal(out.data.url, 'https://arcubase.example.com/apps/app_1/entities')
69
- assert.equal(out.data.headers['Authorization'], 'Bearer tok')
70
- assert.equal(out.data.headers['X-Host'], undefined)
71
- assert.equal(out.data.headers['X-Tenant-Id'], undefined)
118
+ assert.equal(out.data.url, 'https://arcubase.example.com/api/apps/app_1/entities')
119
+ assert.equal(out.data.hasTenantHeader, false)
120
+ assert.equal(out.data.hasHostHeader, false)
72
121
  })
73
122
 
74
- test('admin api-prefixed command executes against external root path', async () => {
123
+ test('table create executes with valid body json', async () => {
75
124
  const out = await executeCLI(
76
125
  'admin',
77
- ['app-entity-share', 'get-app-entity-shares', '--app-id', 'app_1'],
126
+ ['table', 'create', '--app-id', 'app_1', '--body-json', '{"name":"订单","parent":0}'],
78
127
  env as any,
79
- async (url, init) => new Response(JSON.stringify({ ok: true, url, method: init?.method }), { status: 200 })
128
+ async (url, init) => new Response(JSON.stringify({
129
+ url,
130
+ method: init?.method,
131
+ body: init?.body ? JSON.parse(String(init.body)) : null,
132
+ }), { status: 200 }),
80
133
  )
81
134
  assert.equal(out.kind, 'result')
82
- assert.equal(out.data.url, 'https://arcubase.example.com/apps/app_1/entity-shares')
135
+ assert.equal(out.data.url, 'https://arcubase.example.com/api/apps/app_1/entities')
136
+ assert.deepEqual(out.data.body, { name: '订单', parent: 0 })
137
+ })
138
+
139
+ test('body json conflicts with body file', async () => {
140
+ const bodyFile = tempJSONFile({ name: '订单', parent: 0 })
141
+ await assert.rejects(async () => {
142
+ await executeCLI('admin', ['table', 'create', '--app-id', 'app_1', '--body-file', bodyFile, '--body-json', '{"name":"订单"}'], env as any, async () => new Response('{}', { status: 200 }))
143
+ }, (error: unknown) => {
144
+ assert.ok(error instanceof CLIError)
145
+ assert.equal(error.code, 'BODY_JSON_CONFLICT')
146
+ return true
147
+ })
83
148
  })
84
149
 
85
- test('app inventory returns apps with entity lists', async () => {
86
- const calls: string[] = []
150
+ test('body json is rejected for commands without request type', async () => {
151
+ await assert.rejects(async () => {
152
+ await executeCLI('admin', ['app', 'list', '--body-json', '{"name":"ignored"}'], env as any, async () => new Response('{}', { status: 200 }))
153
+ }, (error: unknown) => {
154
+ assert.ok(error instanceof CLIError)
155
+ assert.equal(error.code, 'BODY_JSON_NOT_SUPPORTED')
156
+ return true
157
+ })
158
+ })
159
+
160
+ test('body json parse errors fail fast', async () => {
161
+ await assert.rejects(async () => {
162
+ await executeCLI('admin', ['table', 'create', '--app-id', 'app_1', '--body-json', '{"name":'], env as any, async () => new Response('{}', { status: 200 }))
163
+ }, (error: unknown) => {
164
+ assert.ok(error instanceof CLIError)
165
+ assert.equal(error.code, 'INVALID_BODY_JSON')
166
+ return true
167
+ })
168
+ })
169
+
170
+ test('access-rule list executes against external root path', async () => {
87
171
  const out = await executeCLI(
88
172
  'admin',
89
- ['app', 'inventory'],
173
+ ['access-rule', 'list', '--app-id', 'app_1', '--table-id', 'table_1'],
90
174
  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
- },
175
+ async (url, init) => new Response(JSON.stringify({ url, method: init?.method }), { status: 200 }),
118
176
  )
119
177
  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
- ])
178
+ assert.equal(out.data.url, 'https://arcubase.example.com/api/apps/app_1/entity/table_1/ingress')
144
179
  })
145
180
 
146
- test('admin save entity treats top-level error body as failure even on http 200', async () => {
147
- const bodyFile = tempJSONFile({
148
- id: 2188889845,
149
- app_id: 2188889844,
150
- name: '订单',
151
- schema_version: 1,
152
- field_id_seq: 1002,
153
- layout: [[1001]],
154
- fields: [
155
- {
156
- id: 1001,
157
- label: '订单号',
158
- type: 'text',
159
- required: true,
160
- unique: false,
161
- editable: true,
162
- visible: true,
163
- show_label: true,
164
- scannable: false,
165
- default_value_mode: 0,
166
- description: '',
167
- value: null,
168
- number_decimal: 0,
169
- depends: [],
170
- key: 'order_id',
171
- code_index: false,
172
- options: {},
173
- transfers: null,
174
- children: null,
175
- },
176
- ],
177
- workflow_enabled: false,
178
- })
181
+ test('table update-schema treats top-level error body as failure even on http 200', async () => {
182
+ const bodyFile = tempJSONFile(validEntitySavePayload())
179
183
  await assert.rejects(async () => {
180
184
  await executeCLI(
181
185
  'admin',
182
- ['entity', 'admin-save-entity', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile],
186
+ ['table', 'update-schema', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile],
183
187
  env as any,
184
- async () =>
185
- new Response(
186
- JSON.stringify({
187
- error: {
188
- message: 'json: cannot unmarshal object into Go struct field .EntityVO.fields of type []models.Field',
189
- type: 'system',
190
- key: 'error.system',
191
- },
192
- trace_id: 'req_xxx',
193
- }),
194
- { status: 200 },
195
- ),
188
+ async () => new Response(JSON.stringify({
189
+ error: { message: 'json: cannot unmarshal object into Go struct field .EntityVO.fields of type []models.Field', type: 'system', key: 'error.system' },
190
+ trace_id: 'req_xxx',
191
+ }), { status: 200 }),
196
192
  )
197
193
  }, (error: unknown) => {
198
194
  assert.ok(error instanceof CLIError)
199
195
  assert.equal(error.code, 'UPSTREAM_BODY_ERROR')
200
- assert.equal(error.details?.endpoint, '/apps/app_1/entity/entity_1')
196
+ assert.equal(error.details?.endpoint, '/api/apps/app_1/entity/table_1')
201
197
  assert.equal(error.details?.method, 'PUT')
202
198
  assert.equal(error.details?.traceId, 'req_xxx')
203
199
  return true
204
200
  })
205
201
  })
206
202
 
207
- test('admin save entity normalizes shell null placeholders before validation', async () => {
208
- const bodyFile = tempJSONFile({
209
- id: 2188889845,
210
- app_id: 2188889844,
211
- name: '订单',
203
+ test('table update-schema normalizes shell null placeholders before validation', async () => {
204
+ const bodyFile = tempJSONFile(validEntitySavePayload({
212
205
  schema_version: 0,
213
- field_id_seq: 1002,
214
206
  layout: null,
215
207
  current_serial: null,
216
- fields: [
217
- {
218
- id: 1001,
219
- label: '订单号',
220
- type: 'text',
221
- required: true,
222
- unique: false,
223
- editable: true,
224
- visible: true,
225
- show_label: true,
226
- scannable: false,
227
- default_value_mode: 0,
228
- description: '',
229
- value: null,
230
- number_decimal: 0,
231
- depends: [],
232
- key: 'order_id',
233
- code_index: false,
234
- options: {
235
- placeholder: '',
236
- type: 'text',
237
- lengthLimit: false,
238
- lengthMin: 0,
239
- lengthMax: 255,
240
- },
241
- transfers: null,
242
- children: null,
243
- },
244
- ],
245
208
  options: {
246
209
  Buttons: null,
247
210
  CustomActions: null,
248
211
  FrontendEvents: null,
249
- TitleFormat: {
250
- Fields: null,
251
- Parts: null,
252
- Body: '',
253
- },
212
+ TitleFormat: { Fields: null, Parts: null, Body: '' },
254
213
  misc: null,
255
214
  },
256
- workflow_enabled: false,
257
- })
215
+ }))
258
216
  const out = await executeCLI(
259
217
  'admin',
260
- ['entity', 'admin-save-entity', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile],
218
+ ['table', 'update-schema', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile],
261
219
  env as any,
262
220
  async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
263
221
  )
@@ -273,52 +231,26 @@ test('admin save entity normalizes shell null placeholders before validation', a
273
231
  assert.deepEqual(requestBody.options.misc, {})
274
232
  })
275
233
 
276
- test('delete entity row rejects selection type outside ids', async () => {
277
- const bodyFile = tempJSONFile({
278
- selection: {
279
- type: 'selection',
280
- ids: [4000000003],
281
- length: 1,
282
- },
283
- })
284
-
234
+ test('row delete rejects selection type outside ids', async () => {
235
+ const bodyFile = tempJSONFile({ selection: { type: 'selection', ids: [4000000003], length: 1 } })
285
236
  await assert.rejects(async () => {
286
- await executeCLI(
287
- 'user',
288
- ['rows', 'delete-entity-row', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile],
289
- env as any,
290
- async () => new Response('{}', { status: 200 }),
291
- )
237
+ await executeCLI('user', ['row', 'delete', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile], env as any, async () => new Response('{}', { status: 200 }))
292
238
  }, (error: unknown) => {
293
239
  assert.ok(error instanceof CLIError)
294
240
  assert.equal(error.code, 'BODY_VALIDATION_FAILED')
295
241
  assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.selection'))
296
- assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/search-and-bulk-actions.md')))
297
242
  return true
298
243
  })
299
244
  })
300
245
 
301
- test('bulk update accepts ids selection with number_add action', async () => {
246
+ test('row bulk-update accepts ids selection with number_add action', async () => {
302
247
  const bodyFile = tempJSONFile({
303
- selection: {
304
- type: 'ids',
305
- ids: [4000000003],
306
- length: 1,
307
- },
308
- fields: [
309
- {
310
- id: 1002,
311
- action: {
312
- type: 'number_add',
313
- number_add: 10,
314
- },
315
- },
316
- ],
248
+ selection: { type: 'ids', ids: [4000000003], length: 1 },
249
+ fields: [{ id: 1002, action: { type: 'number_add', number_add: 10 } }],
317
250
  })
318
-
319
251
  const out = await executeCLI(
320
252
  'user',
321
- ['entity', 'update-entity-bulk', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile],
253
+ ['row', 'bulk-update', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile],
322
254
  env as any,
323
255
  async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
324
256
  )
@@ -327,24 +259,18 @@ test('bulk update accepts ids selection with number_add action', async () => {
327
259
  assert.equal(out.data.fields[0].action.type, 'number_add')
328
260
  })
329
261
 
330
- test('selection action accepts condition selection with selectAll', async () => {
262
+ test('row selection-action accepts condition selection with selectAll', async () => {
331
263
  const bodyFile = tempJSONFile({
332
264
  selection: {
333
265
  type: 'condition',
334
266
  length: 1,
335
- condition: {
336
- q: 'A-100',
337
- selectAll: true,
338
- },
339
- sorts: [
340
- { column: ':1002', order: 'desc' },
341
- ],
267
+ condition: { q: 'A-100', selectAll: true },
268
+ sorts: [{ column: ':1002', order: 'desc' }],
342
269
  },
343
270
  })
344
-
345
271
  const out = await executeCLI(
346
272
  'user',
347
- ['rows', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile],
273
+ ['row', 'selection-action', '--app-id', 'app_1', '--table-id', 'table_1', '--action', 'archive', '--body-file', bodyFile],
348
274
  env as any,
349
275
  async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
350
276
  )
@@ -353,87 +279,10 @@ test('selection action accepts condition selection with selectAll', async () =>
353
279
  assert.equal(out.data.selection.condition.selectAll, true)
354
280
  })
355
281
 
356
- test('selection action accepts condition selection with filter_* keys', async () => {
357
- const bodyFile = tempJSONFile({
358
- selection: {
359
- type: 'condition',
360
- length: 1,
361
- condition: {
362
- 'filter_:1001': '%7B%22is%22%3A%22A-100%22%7D',
363
- selectAll: true,
364
- },
365
- },
366
- })
367
-
368
- const out = await executeCLI(
369
- 'user',
370
- ['rows', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile],
371
- env as any,
372
- async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
373
- )
374
- assert.equal(out.kind, 'result')
375
- assert.equal(out.data.selection.type, 'condition')
376
- assert.equal(out.data.selection.condition.selectAll, true)
377
- })
378
-
379
- test('selection action accepts condition selection with selectAll only for batch_print', async () => {
380
- const bodyFile = tempJSONFile({
381
- selection: {
382
- type: 'condition',
383
- length: 1,
384
- condition: {
385
- selectAll: true,
386
- },
387
- },
388
- })
389
-
390
- const out = await executeCLI(
391
- 'user',
392
- ['rows', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'batch_print', '--body-file', bodyFile],
393
- env as any,
394
- async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
395
- )
396
- assert.equal(out.kind, 'result')
397
- assert.equal(out.data.selection.type, 'condition')
398
- assert.equal(out.data.selection.condition.selectAll, true)
399
- })
400
-
401
- test('selection action accepts all selection for query-style actions', async () => {
402
- const bodyFile = tempJSONFile({
403
- selection: {
404
- type: 'all',
405
- },
406
- })
407
-
408
- const out = await executeCLI(
409
- 'user',
410
- ['rows', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'query', '--body-file', bodyFile],
411
- env as any,
412
- async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
413
- )
414
- assert.equal(out.kind, 'result')
415
- assert.equal(out.data.selection.type, 'all')
416
- })
417
-
418
- test('selection action rejects condition selection for query action', async () => {
419
- const bodyFile = tempJSONFile({
420
- selection: {
421
- type: 'condition',
422
- length: 1,
423
- condition: {
424
- q: 'A-100',
425
- selectAll: true,
426
- },
427
- },
428
- })
429
-
282
+ test('row selection-action rejects condition selection for query action', async () => {
283
+ const bodyFile = tempJSONFile({ selection: { type: 'condition', length: 1, condition: { q: 'A-100', selectAll: true } } })
430
284
  await assert.rejects(async () => {
431
- await executeCLI(
432
- 'user',
433
- ['rows', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'query', '--body-file', bodyFile],
434
- env as any,
435
- async () => new Response('{}', { status: 200 }),
436
- )
285
+ await executeCLI('user', ['row', 'selection-action', '--app-id', 'app_1', '--table-id', 'table_1', '--action', 'query', '--body-file', bodyFile], env as any, async () => new Response('{}', { status: 200 }))
437
286
  }, (error: unknown) => {
438
287
  assert.ok(error instanceof CLIError)
439
288
  assert.equal(error.code, 'BODY_VALIDATION_FAILED')
@@ -442,81 +291,18 @@ test('selection action rejects condition selection for query action', async () =
442
291
  })
443
292
  })
444
293
 
445
- test('selection action rejects all selection for archive action', async () => {
446
- const bodyFile = tempJSONFile({
447
- selection: {
448
- type: 'all',
449
- },
450
- })
451
-
452
- await assert.rejects(async () => {
453
- await executeCLI(
454
- 'user',
455
- ['rows', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile],
456
- env as any,
457
- async () => new Response('{}', { status: 200 }),
458
- )
459
- }, (error: unknown) => {
460
- assert.ok(error instanceof CLIError)
461
- assert.equal(error.code, 'BODY_VALIDATION_FAILED')
462
- assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.selection.type'))
463
- return true
464
- })
465
- })
466
-
467
- test('selection action rejects selectAll-only condition for archive action', async () => {
468
- const bodyFile = tempJSONFile({
469
- selection: {
470
- type: 'condition',
471
- length: 1,
472
- condition: {
473
- selectAll: true,
474
- },
475
- },
476
- })
477
-
478
- await assert.rejects(async () => {
479
- await executeCLI(
480
- 'user',
481
- ['rows', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile],
482
- env as any,
483
- async () => new Response('{}', { status: 200 }),
484
- )
485
- }, (error: unknown) => {
486
- assert.ok(error instanceof CLIError)
487
- assert.equal(error.code, 'BODY_VALIDATION_FAILED')
488
- assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.selection.condition'))
489
- return true
490
- })
491
- })
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
- })
294
+ test('row create rejects local file path for file field and points to arcubase upload', async () => {
295
+ const bodyFile = tempJSONFile({ fields: { 1001: 'Lead A', 1007: './contract.pdf' } })
500
296
  const calls: Array<{ url: string; method?: string }> = []
501
-
502
297
  await assert.rejects(async () => {
503
298
  await executeCLI(
504
299
  'user',
505
- ['rows', 'insert-entity', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile],
300
+ ['row', 'create', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile],
506
301
  env as any,
507
302
  async (url, init) => {
508
303
  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 })
304
+ if (String(url) === 'https://arcubase.example.com/api/entity/app_1/table_1' && init?.method === 'GET') {
305
+ 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 })
520
306
  }
521
307
  return new Response('{}', { status: 200 })
522
308
  },
@@ -526,44 +312,21 @@ test('insert entity rejects local file path for file field and points to arcubas
526
312
  assert.equal(error.code, 'BODY_VALIDATION_FAILED')
527
313
  assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.fields.1007'))
528
314
  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
315
  return true
531
316
  })
532
-
533
- assert.deepEqual(calls, [
534
- { url: 'https://arcubase.example.com/entity/app_1/entity_1', method: 'GET' },
535
- ])
317
+ assert.deepEqual(calls, [{ url: 'https://arcubase.example.com/api/entity/app_1/table_1', method: 'GET' }])
536
318
  })
537
319
 
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
-
320
+ test('row update rejects object value for image field and requires upload array', async () => {
321
+ const bodyFile = tempJSONFile({ changed_fields: [1008], data: { 1008: { upload_id: 123456, file: 'lead-photo.jpg', file_name: 'lead-photo.jpg', meta: {} } } })
551
322
  await assert.rejects(async () => {
552
323
  await executeCLI(
553
324
  'user',
554
- ['rows', 'update-entity-row', '--app-id', 'app_1', '--entity-id', 'entity_1', '--row-id', 'row_1', '--body-file', bodyFile],
325
+ ['row', 'update', '--app-id', 'app_1', '--table-id', 'table_1', '--row-id', 'row_1', '--body-file', bodyFile],
555
326
  env as any,
556
327
  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 })
328
+ if (String(url) === 'https://arcubase.example.com/api/entity/app_1/table_1' && init?.method === 'GET') {
329
+ return new Response(JSON.stringify({ data: { id: 1, app_id: 1, fields: [{ id: 1008, type: 'image', label: 'Lead Photo' }] } }), { status: 200 })
567
330
  }
568
331
  return new Response('{}', { status: 200 })
569
332
  },
@@ -573,88 +336,47 @@ test('update row rejects object value for image field and requires upload array'
573
336
  assert.equal(error.code, 'BODY_VALIDATION_FAILED')
574
337
  assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.data.1008'))
575
338
  assert.match(error.message, /must be an array/)
576
- assert.match(error.message, /arcubase upload <local-file>/)
577
339
  return true
578
340
  })
579
341
  })
580
342
 
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
- })
343
+ test('row create accepts upload array for file field after schema preflight', async () => {
344
+ const bodyFile = tempJSONFile({ fields: { 1001: 'Lead A', 1007: [{ upload_id: 123456, file: 'contract.pdf', file_name: 'contract.pdf', meta: {} }] } })
595
345
  const calls: Array<{ url: string; method?: string }> = []
596
-
597
346
  const out = await executeCLI(
598
347
  'user',
599
- ['rows', 'insert-entity', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile],
348
+ ['row', 'create', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile],
600
349
  env as any,
601
350
  async (url, init) => {
602
351
  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 })
352
+ if (String(url) === 'https://arcubase.example.com/api/entity/app_1/table_1' && init?.method === 'GET') {
353
+ 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 })
614
354
  }
615
355
  return new Response(String(init?.body ?? '{}'), { status: 200 })
616
356
  },
617
357
  )
618
-
619
358
  assert.equal(out.kind, 'result')
620
359
  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' },
360
+ { url: 'https://arcubase.example.com/api/entity/app_1/table_1', method: 'GET' },
361
+ { url: 'https://arcubase.example.com/api/entity/app_1/table_1/insert', method: 'POST' },
623
362
  ])
624
363
  assert.equal(out.data.fields['1007'][0].upload_id, 123456)
625
364
  })
626
365
 
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
-
366
+ test('row create decorates upload bind failures with actionable guidance', async () => {
367
+ const bodyFile = tempJSONFile({ fields: { 1001: 'Lead A', 1007: [{ upload_id: 123456, file: 'contract.pdf', file_name: 'contract.pdf', meta: {} }] } })
642
368
  await assert.rejects(async () => {
643
369
  await executeCLI(
644
370
  'user',
645
- ['rows', 'insert-entity', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile],
371
+ ['row', 'create', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile],
646
372
  env as any,
647
373
  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 })
374
+ if (String(url) === 'https://arcubase.example.com/api/entity/app_1/table_1' && init?.method === 'GET') {
375
+ return new Response(JSON.stringify({ data: { fields: [{ id: 1007, type: 'file', label: 'Contract File' }] } }), { status: 200 })
654
376
  }
655
377
  return new Response(JSON.stringify({
656
378
  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',
379
+ 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
380
  type: 'system',
659
381
  key: 'error.system',
660
382
  },
@@ -668,239 +390,76 @@ test('insert entity decorates upload bind failures with actionable guidance', as
668
390
  assert.equal(error.details?.traceId, 'req_upload_bind')
669
391
  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
392
  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
393
  return true
673
394
  })
674
395
  })
675
396
 
676
- test('selection action rejects condition selection without selectAll', async () => {
677
- const bodyFile = tempJSONFile({
678
- selection: {
679
- type: 'condition',
680
- length: 1,
681
- condition: {
682
- q: 'A-100',
683
- },
684
- },
685
- })
686
-
397
+ test('row query rejects camelCase viewMode and points to row crud doc', async () => {
398
+ const bodyFile = tempJSONFile({ limit: 20, offset: 0, viewMode: 'grid' })
687
399
  await assert.rejects(async () => {
688
- await executeCLI(
689
- 'user',
690
- ['rows', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile],
691
- env as any,
692
- async () => new Response('{}', { status: 200 }),
693
- )
400
+ await executeCLI('user', ['row', 'query', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile], env as any, async () => new Response('{}', { status: 200 }))
694
401
  }, (error: unknown) => {
695
402
  assert.ok(error instanceof CLIError)
696
403
  assert.equal(error.code, 'BODY_VALIDATION_FAILED')
697
- assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.selection.condition.selectAll'))
404
+ assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.viewMode'))
698
405
  return true
699
406
  })
700
407
  })
701
408
 
702
- test('selection action rejects condition selection that uses text_search instead of q/filter keys', async () => {
703
- const bodyFile = tempJSONFile({
704
- selection: {
705
- type: 'condition',
706
- length: 1,
707
- condition: {
708
- text_search: 'A-100',
709
- selectAll: true,
710
- },
711
- },
712
- })
713
-
409
+ test('row update rejects fields key', async () => {
410
+ const bodyFile = tempJSONFile({ fields: { 1002: 99 }, data: { '1002': 99 }, changed_fields: [1002] })
714
411
  await assert.rejects(async () => {
715
- await executeCLI(
716
- 'user',
717
- ['rows', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile],
718
- env as any,
719
- async () => new Response('{}', { status: 200 }),
720
- )
412
+ await executeCLI('user', ['row', 'update', '--app-id', 'app_1', '--table-id', 'table_1', '--row-id', 'row_1', '--body-file', bodyFile], env as any, async () => new Response('{}', { status: 200 }))
721
413
  }, (error: unknown) => {
722
414
  assert.ok(error instanceof CLIError)
723
415
  assert.equal(error.code, 'BODY_VALIDATION_FAILED')
724
- assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.selection.condition'))
725
- assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/search-and-bulk-actions.md')))
416
+ assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.fields'))
726
417
  return true
727
418
  })
728
419
  })
729
420
 
730
- test('bulk update accepts condition selection with q', async () => {
731
- const bodyFile = tempJSONFile({
732
- selection: {
733
- type: 'condition',
734
- length: 1,
735
- condition: {
736
- q: 'A-100',
737
- selectAll: true,
738
- },
739
- sorts: [
740
- { column: ':1002', order: 'desc' },
741
- ],
742
- },
743
- fields: [
744
- {
745
- id: 1002,
746
- action: {
747
- type: 'number_add',
748
- number_add: 2,
749
- },
750
- },
751
- ],
752
- })
753
-
754
- const out = await executeCLI(
755
- 'user',
756
- ['entity', 'update-entity-bulk', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile],
757
- env as any,
758
- async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
759
- )
760
- assert.equal(out.kind, 'result')
761
- assert.equal(out.data.selection.type, 'condition')
762
- assert.equal(out.data.fields[0].action.type, 'number_add')
763
- })
764
-
765
- test('entity tags get-selection-tags accepts ids selection', async () => {
766
- const bodyFile = tempJSONFile({
767
- policy_id: '',
768
- selection: {
769
- type: 'ids',
770
- ids: [4000000003],
771
- length: 1,
772
- },
421
+ test('row update rejects changed_fields that are missing from data', async () => {
422
+ const bodyFile = tempJSONFile({ data: { '1002': 99 }, changed_fields: [1003] })
423
+ await assert.rejects(async () => {
424
+ await executeCLI('user', ['row', 'update', '--app-id', 'app_1', '--table-id', 'table_1', '--row-id', 'row_1', '--body-file', bodyFile], env as any, async () => new Response('{}', { status: 200 }))
425
+ }, (error: unknown) => {
426
+ assert.ok(error instanceof CLIError)
427
+ assert.equal(error.code, 'BODY_VALIDATION_FAILED')
428
+ assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.changed_fields.0'))
429
+ return true
773
430
  })
774
-
775
- const out = await executeCLI(
776
- 'admin',
777
- ['entity-tags', 'get-selection-tags', '--appId', 'app_1', '--entityId', 'entity_1', '--body-file', bodyFile],
778
- env as any,
779
- async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
780
- )
781
- assert.equal(out.kind, 'result')
782
- assert.equal(out.data.selection.type, 'ids')
783
431
  })
784
432
 
785
- test('entity tags batch-add accepts tag_names and condition selection', async () => {
786
- const bodyFile = tempJSONFile({
787
- policy_id: '',
788
- selection: {
789
- type: 'condition',
790
- length: 1,
791
- condition: {
792
- q: 'A-100',
793
- selectAll: true,
794
- },
795
- },
796
- tag_names: ['tag-a'],
797
- })
798
-
799
- const out = await executeCLI(
800
- 'admin',
801
- ['entity-tags', 'batch-add-entity-tags', '--appId', 'app_1', '--entityId', 'entity_1', '--body-file', bodyFile],
802
- env as any,
803
- async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
804
- )
805
- assert.equal(out.kind, 'result')
806
- assert.equal(out.data.tag_names[0], 'tag-a')
807
- })
808
-
809
- test('invoke batch operator accepts backend request shape', async () => {
810
- const bodyFile = tempJSONFile({
811
- id: 'event_1',
812
- data: {
813
- action_id: 'action_1',
814
- policy_id: '',
815
- selection: {
816
- type: 'all',
817
- },
818
- },
819
- params: {},
820
- })
821
-
433
+ test('workflow approve maps to fixed action path', async () => {
434
+ const bodyFile = tempJSONFile({ task_id: 1001, fields: {}, changed: [] })
822
435
  const out = await executeCLI(
823
436
  'user',
824
- ['entity', 'invoke-entity-batch-operator', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile],
437
+ ['workflow', 'approve', '--app-id', 'app_1', '--table-id', 'table_1', '--row-id', 'row_1', '--body-file', bodyFile],
825
438
  env as any,
826
- async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
439
+ async (url, init) => new Response(JSON.stringify({ url, method: init?.method, body: init?.body ? JSON.parse(String(init.body)) : null }), { status: 200 }),
827
440
  )
828
441
  assert.equal(out.kind, 'result')
829
- assert.equal(out.data.id, 'event_1')
830
- assert.equal(out.data.data.action_id, 'action_1')
831
- assert.equal(out.data.data.selection.type, 'all')
442
+ assert.equal(out.data.url, 'https://arcubase.example.com/api/entity/app_1/table_1/row/row_1/action/2')
443
+ assert.equal(out.data.body.task_id, 1001)
832
444
  })
833
445
 
834
- test('query entity rejects camelCase viewMode and points to row crud doc', async () => {
835
- const bodyFile = tempJSONFile({
836
- limit: 20,
837
- offset: 0,
838
- viewMode: 'grid',
839
- })
840
-
841
- await assert.rejects(async () => {
842
- await executeCLI(
843
- 'user',
844
- ['rows', 'query-entity', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile],
845
- env as any,
846
- async () => new Response('{}', { status: 200 }),
847
- )
848
- }, (error: unknown) => {
849
- assert.ok(error instanceof CLIError)
850
- assert.equal(error.code, 'BODY_VALIDATION_FAILED')
851
- assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.viewMode'))
852
- assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/row-crud.md')))
853
- return true
854
- })
855
- })
856
-
857
- test('update entity row rejects fields key', async () => {
858
- const bodyFile = tempJSONFile({
859
- fields: {
860
- 1002: 99,
861
- },
862
- data: {
863
- '1002': 99,
864
- },
865
- changed_fields: [1002],
866
- })
867
-
446
+ test('workflow query-approve-users requires request body', async () => {
868
447
  await assert.rejects(async () => {
869
- await executeCLI(
870
- 'user',
871
- ['rows', 'update-entity-row', '--app-id', 'app_1', '--entity-id', 'entity_1', '--row-id', 'row_1', '--body-file', bodyFile],
872
- env as any,
873
- async () => new Response('{}', { status: 200 }),
874
- )
448
+ await executeCLI('user', ['workflow', 'query-approve-users', '--app-id', 'app_1', '--table-id', 'table_1'], env as any, async () => new Response('{}', { status: 200 }))
875
449
  }, (error: unknown) => {
876
450
  assert.ok(error instanceof CLIError)
877
- assert.equal(error.code, 'BODY_VALIDATION_FAILED')
878
- assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.fields'))
879
- assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/row-crud.md')))
451
+ assert.equal(error.code, 'MISSING_BODY_FILE')
880
452
  return true
881
453
  })
882
454
  })
883
455
 
884
- test('update entity row rejects changed_fields that are missing from data', async () => {
885
- const bodyFile = tempJSONFile({
886
- data: {
887
- '1002': 99,
888
- },
889
- changed_fields: [1003],
890
- })
891
-
892
- await assert.rejects(async () => {
893
- await executeCLI(
894
- 'user',
895
- ['rows', 'update-entity-row', '--app-id', 'app_1', '--entity-id', 'entity_1', '--row-id', 'row_1', '--body-file', bodyFile],
896
- env as any,
897
- async () => new Response('{}', { status: 200 }),
898
- )
899
- }, (error: unknown) => {
900
- assert.ok(error instanceof CLIError)
901
- assert.equal(error.code, 'BODY_VALIDATION_FAILED')
902
- assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.changed_fields.0'))
903
- assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/row-crud.md')))
904
- return true
905
- })
456
+ test('entry passes include_app_tables query flag', async () => {
457
+ const out = await executeCLI(
458
+ 'user',
459
+ ['entry', '--include-app-tables', 'true'],
460
+ env as any,
461
+ async (url, init) => new Response(JSON.stringify({ url, method: init?.method }), { status: 200 }),
462
+ )
463
+ assert.equal(out.kind, 'result')
464
+ assert.equal(out.data.url, 'https://arcubase.example.com/api/entry?include_app_tables=true')
906
465
  })