@carthooks/arcubase-cli 0.1.2 → 0.1.4
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.
- package/bundle/arcubase-admin.mjs +1238 -16
- package/bundle/arcubase.mjs +1238 -16
- package/dist/bin/arcubase-admin.js +0 -0
- package/dist/bin/arcubase.js +0 -0
- package/dist/runtime/command_registry.d.ts +8 -0
- package/dist/runtime/command_registry.d.ts.map +1 -1
- package/dist/runtime/command_registry.js +35 -0
- package/dist/runtime/entity_save_schema.d.ts +15 -0
- package/dist/runtime/entity_save_schema.d.ts.map +1 -0
- package/dist/runtime/entity_save_schema.js +641 -0
- package/dist/runtime/errors.d.ts +11 -0
- package/dist/runtime/errors.d.ts.map +1 -1
- package/dist/runtime/execute.d.ts.map +1 -1
- package/dist/runtime/execute.js +294 -12
- package/dist/runtime/upload.d.ts +13 -0
- package/dist/runtime/upload.d.ts.map +1 -0
- package/dist/runtime/upload.js +148 -0
- package/dist/runtime/zod_registry.d.ts +6 -0
- package/dist/runtime/zod_registry.d.ts.map +1 -1
- package/dist/runtime/zod_registry.js +136 -0
- package/dist/tests/command_registry.test.js +5 -0
- package/dist/tests/entity_save_schema.test.d.ts +2 -0
- package/dist/tests/entity_save_schema.test.d.ts.map +1 -0
- package/dist/tests/entity_save_schema.test.js +221 -0
- package/dist/tests/execute_validation.test.js +603 -0
- package/dist/tests/help.test.js +49 -0
- package/dist/tests/upload.test.d.ts +2 -0
- package/dist/tests/upload.test.d.ts.map +1 -0
- package/dist/tests/upload.test.js +107 -0
- package/package.json +1 -1
- package/sdk-dist/docs/runtime-reference/README.md +187 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/README.md +120 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/address.md +34 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/boolean.md +37 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/button.md +47 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/checkbox.md +61 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/datasets.md +35 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/datetime.md +40 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/department.md +33 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/departments.md +34 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/divider.md +39 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/file.md +53 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/formula.md +44 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/image.md +52 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/linkto.md +67 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/location.md +32 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/lookup.md +41 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/member.md +37 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/members.md +31 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/number.md +70 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/query.md +50 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/radio.md +53 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/relation.md +55 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/relationfield.md +38 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/rollup.md +54 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/select.md +63 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/selects.md +51 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/serialnumber.md +48 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/signature.md +31 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/status.md +70 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/subform.md +74 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/tags.md +19 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/text.md +76 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/textarea.md +47 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/workflow_status.md +17 -0
- package/sdk-dist/docs/runtime-reference/entity-schema.md +197 -0
- package/sdk-dist/docs/runtime-reference/examples/README.md +51 -0
- package/sdk-dist/docs/runtime-reference/examples/crm-01/README.md +20 -0
- package/sdk-dist/docs/runtime-reference/examples/mes-01/README.md +21 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/README.md +183 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/app-overview.md +79 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/create-app.json +3 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/goods-receipt.create-entity.json +3 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/goods-receipt.schema.json +159 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/item-master.create-entity.json +3 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/item-master.schema.json +179 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/picking-list.create-entity.json +3 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/picking-list.schema.json +168 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/purchase-order.create-entity.json +3 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/purchase-order.schema.json +417 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.bulk-update.json +22 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.create-entity.json +3 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.query.json +10 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.row.insert.json +28 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.schema.json +398 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.selection.condition.json +13 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.selection.ids.json +7 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/stock-issue.create-entity.json +3 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/stock-issue.schema.json +137 -0
- package/sdk-dist/docs/runtime-reference/examples/wms-01/README.md +21 -0
- package/sdk-dist/docs/runtime-reference/row-crud.md +188 -0
- package/sdk-dist/docs/runtime-reference/search-and-bulk-actions.md +494 -0
- package/sdk-dist/docs/runtime-reference/table-lifecycle.md +135 -0
- package/sdk-dist/docs/runtime-reference/uploads.md +106 -0
- package/sdk-dist/docs/runtime-reference/workflow/README.md +19 -0
- package/src/runtime/command_registry.ts +47 -0
- package/src/runtime/entity_save_schema.ts +708 -0
- package/src/runtime/errors.ts +12 -0
- package/src/runtime/execute.ts +367 -12
- package/src/runtime/upload.ts +196 -0
- package/src/runtime/zod_registry.ts +156 -0
- package/src/tests/command_registry.test.ts +6 -0
- package/src/tests/entity_save_schema.test.ts +236 -0
- package/src/tests/execute_validation.test.ts +781 -0
- package/src/tests/help.test.ts +60 -0
- package/src/tests/upload.test.ts +133 -0
|
@@ -34,6 +34,25 @@ test('admin save entity rejects invalid body file', async () => {
|
|
|
34
34
|
assert.equal(error.details?.requestType, 'EntitySaveReqVO')
|
|
35
35
|
assert.ok((error.details?.issues ?? []).some((item) => item.path.startsWith('body.layout')))
|
|
36
36
|
assert.ok((error.details?.tsHints ?? []).some((item) => item.file.startsWith('/opt/arcubase-sdk/')))
|
|
37
|
+
assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/entity-schema.md')))
|
|
38
|
+
return true
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('admin create app by tenants rejects name longer than 20 chars', async () => {
|
|
43
|
+
const bodyFile = tempJSONFile({ name: 'CLI Verify Happy Path' })
|
|
44
|
+
await assert.rejects(async () => {
|
|
45
|
+
await executeCLI(
|
|
46
|
+
'admin',
|
|
47
|
+
['app', 'create-app-by-tenants', '--body-file', bodyFile],
|
|
48
|
+
env as any,
|
|
49
|
+
async () => new Response('{}', { status: 200 }),
|
|
50
|
+
)
|
|
51
|
+
}, (error: unknown) => {
|
|
52
|
+
assert.ok(error instanceof CLIError)
|
|
53
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
54
|
+
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.name'))
|
|
55
|
+
assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/table-lifecycle.md')))
|
|
37
56
|
return true
|
|
38
57
|
})
|
|
39
58
|
})
|
|
@@ -62,3 +81,765 @@ test('admin api-prefixed command executes against external root path', async ()
|
|
|
62
81
|
assert.equal(out.kind, 'result')
|
|
63
82
|
assert.equal(out.data.url, 'https://arcubase.example.com/apps/app_1/entity-shares')
|
|
64
83
|
})
|
|
84
|
+
|
|
85
|
+
test('admin save entity treats top-level error body as failure even on http 200', async () => {
|
|
86
|
+
const bodyFile = tempJSONFile({
|
|
87
|
+
id: 2188889845,
|
|
88
|
+
app_id: 2188889844,
|
|
89
|
+
name: '订单',
|
|
90
|
+
schema_version: 1,
|
|
91
|
+
field_id_seq: 1002,
|
|
92
|
+
layout: [[1001]],
|
|
93
|
+
fields: [
|
|
94
|
+
{
|
|
95
|
+
id: 1001,
|
|
96
|
+
label: '订单号',
|
|
97
|
+
type: 'text',
|
|
98
|
+
required: true,
|
|
99
|
+
unique: false,
|
|
100
|
+
editable: true,
|
|
101
|
+
visible: true,
|
|
102
|
+
show_label: true,
|
|
103
|
+
scannable: false,
|
|
104
|
+
default_value_mode: 0,
|
|
105
|
+
description: '',
|
|
106
|
+
value: null,
|
|
107
|
+
number_decimal: 0,
|
|
108
|
+
depends: [],
|
|
109
|
+
key: 'order_id',
|
|
110
|
+
code_index: false,
|
|
111
|
+
options: {},
|
|
112
|
+
transfers: null,
|
|
113
|
+
children: null,
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
workflow_enabled: false,
|
|
117
|
+
})
|
|
118
|
+
await assert.rejects(async () => {
|
|
119
|
+
await executeCLI(
|
|
120
|
+
'admin',
|
|
121
|
+
['entity', 'admin-save-entity', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile],
|
|
122
|
+
env as any,
|
|
123
|
+
async () =>
|
|
124
|
+
new Response(
|
|
125
|
+
JSON.stringify({
|
|
126
|
+
error: {
|
|
127
|
+
message: 'json: cannot unmarshal object into Go struct field .EntityVO.fields of type []models.Field',
|
|
128
|
+
type: 'system',
|
|
129
|
+
key: 'error.system',
|
|
130
|
+
},
|
|
131
|
+
trace_id: 'req_xxx',
|
|
132
|
+
}),
|
|
133
|
+
{ status: 200 },
|
|
134
|
+
),
|
|
135
|
+
)
|
|
136
|
+
}, (error: unknown) => {
|
|
137
|
+
assert.ok(error instanceof CLIError)
|
|
138
|
+
assert.equal(error.code, 'UPSTREAM_BODY_ERROR')
|
|
139
|
+
assert.equal(error.details?.endpoint, '/apps/app_1/entity/entity_1')
|
|
140
|
+
assert.equal(error.details?.method, 'PUT')
|
|
141
|
+
assert.equal(error.details?.traceId, 'req_xxx')
|
|
142
|
+
return true
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
test('admin save entity normalizes shell null placeholders before validation', async () => {
|
|
147
|
+
const bodyFile = tempJSONFile({
|
|
148
|
+
id: 2188889845,
|
|
149
|
+
app_id: 2188889844,
|
|
150
|
+
name: '订单',
|
|
151
|
+
schema_version: 0,
|
|
152
|
+
field_id_seq: 1002,
|
|
153
|
+
layout: null,
|
|
154
|
+
current_serial: null,
|
|
155
|
+
fields: [
|
|
156
|
+
{
|
|
157
|
+
id: 1001,
|
|
158
|
+
label: '订单号',
|
|
159
|
+
type: 'text',
|
|
160
|
+
required: true,
|
|
161
|
+
unique: false,
|
|
162
|
+
editable: true,
|
|
163
|
+
visible: true,
|
|
164
|
+
show_label: true,
|
|
165
|
+
scannable: false,
|
|
166
|
+
default_value_mode: 0,
|
|
167
|
+
description: '',
|
|
168
|
+
value: null,
|
|
169
|
+
number_decimal: 0,
|
|
170
|
+
depends: [],
|
|
171
|
+
key: 'order_id',
|
|
172
|
+
code_index: false,
|
|
173
|
+
options: {
|
|
174
|
+
placeholder: '',
|
|
175
|
+
type: 'text',
|
|
176
|
+
lengthLimit: false,
|
|
177
|
+
lengthMin: 0,
|
|
178
|
+
lengthMax: 255,
|
|
179
|
+
},
|
|
180
|
+
transfers: null,
|
|
181
|
+
children: null,
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
options: {
|
|
185
|
+
Buttons: null,
|
|
186
|
+
CustomActions: null,
|
|
187
|
+
FrontendEvents: null,
|
|
188
|
+
TitleFormat: {
|
|
189
|
+
Fields: null,
|
|
190
|
+
Parts: null,
|
|
191
|
+
Body: '',
|
|
192
|
+
},
|
|
193
|
+
misc: null,
|
|
194
|
+
},
|
|
195
|
+
workflow_enabled: false,
|
|
196
|
+
})
|
|
197
|
+
const out = await executeCLI(
|
|
198
|
+
'admin',
|
|
199
|
+
['entity', 'admin-save-entity', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile],
|
|
200
|
+
env as any,
|
|
201
|
+
async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
|
|
202
|
+
)
|
|
203
|
+
assert.equal(out.kind, 'result')
|
|
204
|
+
const requestBody = out.data
|
|
205
|
+
assert.deepEqual(requestBody.layout, [])
|
|
206
|
+
assert.equal('current_serial' in requestBody, false)
|
|
207
|
+
assert.deepEqual(requestBody.options.Buttons, [])
|
|
208
|
+
assert.deepEqual(requestBody.options.CustomActions, [])
|
|
209
|
+
assert.deepEqual(requestBody.options.FrontendEvents, [])
|
|
210
|
+
assert.deepEqual(requestBody.options.TitleFormat.Fields, [])
|
|
211
|
+
assert.deepEqual(requestBody.options.TitleFormat.Parts, [])
|
|
212
|
+
assert.deepEqual(requestBody.options.misc, {})
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
test('delete entity row rejects selection type outside ids', async () => {
|
|
216
|
+
const bodyFile = tempJSONFile({
|
|
217
|
+
selection: {
|
|
218
|
+
type: 'selection',
|
|
219
|
+
ids: [4000000003],
|
|
220
|
+
length: 1,
|
|
221
|
+
},
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
await assert.rejects(async () => {
|
|
225
|
+
await executeCLI(
|
|
226
|
+
'user',
|
|
227
|
+
['workflow', 'delete-entity-row', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile],
|
|
228
|
+
env as any,
|
|
229
|
+
async () => new Response('{}', { status: 200 }),
|
|
230
|
+
)
|
|
231
|
+
}, (error: unknown) => {
|
|
232
|
+
assert.ok(error instanceof CLIError)
|
|
233
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
234
|
+
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.selection'))
|
|
235
|
+
assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/search-and-bulk-actions.md')))
|
|
236
|
+
return true
|
|
237
|
+
})
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
test('bulk update accepts ids selection with number_add action', async () => {
|
|
241
|
+
const bodyFile = tempJSONFile({
|
|
242
|
+
selection: {
|
|
243
|
+
type: 'ids',
|
|
244
|
+
ids: [4000000003],
|
|
245
|
+
length: 1,
|
|
246
|
+
},
|
|
247
|
+
fields: [
|
|
248
|
+
{
|
|
249
|
+
id: 1002,
|
|
250
|
+
action: {
|
|
251
|
+
type: 'number_add',
|
|
252
|
+
number_add: 10,
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
const out = await executeCLI(
|
|
259
|
+
'user',
|
|
260
|
+
['entity', 'update-entity-bulk', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile],
|
|
261
|
+
env as any,
|
|
262
|
+
async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
|
|
263
|
+
)
|
|
264
|
+
assert.equal(out.kind, 'result')
|
|
265
|
+
assert.deepEqual(out.data.selection, { type: 'ids', ids: [4000000003], length: 1 })
|
|
266
|
+
assert.equal(out.data.fields[0].action.type, 'number_add')
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
test('selection action accepts condition selection with selectAll', async () => {
|
|
270
|
+
const bodyFile = tempJSONFile({
|
|
271
|
+
selection: {
|
|
272
|
+
type: 'condition',
|
|
273
|
+
length: 1,
|
|
274
|
+
condition: {
|
|
275
|
+
q: 'A-100',
|
|
276
|
+
selectAll: true,
|
|
277
|
+
},
|
|
278
|
+
sorts: [
|
|
279
|
+
{ column: ':1002', order: 'desc' },
|
|
280
|
+
],
|
|
281
|
+
},
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
const out = await executeCLI(
|
|
285
|
+
'user',
|
|
286
|
+
['workflow', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile],
|
|
287
|
+
env as any,
|
|
288
|
+
async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
|
|
289
|
+
)
|
|
290
|
+
assert.equal(out.kind, 'result')
|
|
291
|
+
assert.equal(out.data.selection.type, 'condition')
|
|
292
|
+
assert.equal(out.data.selection.condition.selectAll, true)
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
test('selection action accepts condition selection with filter_* keys', async () => {
|
|
296
|
+
const bodyFile = tempJSONFile({
|
|
297
|
+
selection: {
|
|
298
|
+
type: 'condition',
|
|
299
|
+
length: 1,
|
|
300
|
+
condition: {
|
|
301
|
+
'filter_:1001': '%7B%22is%22%3A%22A-100%22%7D',
|
|
302
|
+
selectAll: true,
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
const out = await executeCLI(
|
|
308
|
+
'user',
|
|
309
|
+
['workflow', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile],
|
|
310
|
+
env as any,
|
|
311
|
+
async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
|
|
312
|
+
)
|
|
313
|
+
assert.equal(out.kind, 'result')
|
|
314
|
+
assert.equal(out.data.selection.type, 'condition')
|
|
315
|
+
assert.equal(out.data.selection.condition.selectAll, true)
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
test('selection action accepts condition selection with selectAll only for batch_print', async () => {
|
|
319
|
+
const bodyFile = tempJSONFile({
|
|
320
|
+
selection: {
|
|
321
|
+
type: 'condition',
|
|
322
|
+
length: 1,
|
|
323
|
+
condition: {
|
|
324
|
+
selectAll: true,
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
const out = await executeCLI(
|
|
330
|
+
'user',
|
|
331
|
+
['workflow', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'batch_print', '--body-file', bodyFile],
|
|
332
|
+
env as any,
|
|
333
|
+
async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
|
|
334
|
+
)
|
|
335
|
+
assert.equal(out.kind, 'result')
|
|
336
|
+
assert.equal(out.data.selection.type, 'condition')
|
|
337
|
+
assert.equal(out.data.selection.condition.selectAll, true)
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
test('selection action accepts all selection for query-style actions', async () => {
|
|
341
|
+
const bodyFile = tempJSONFile({
|
|
342
|
+
selection: {
|
|
343
|
+
type: 'all',
|
|
344
|
+
},
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
const out = await executeCLI(
|
|
348
|
+
'user',
|
|
349
|
+
['workflow', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'query', '--body-file', bodyFile],
|
|
350
|
+
env as any,
|
|
351
|
+
async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
|
|
352
|
+
)
|
|
353
|
+
assert.equal(out.kind, 'result')
|
|
354
|
+
assert.equal(out.data.selection.type, 'all')
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
test('selection action rejects condition selection for query action', async () => {
|
|
358
|
+
const bodyFile = tempJSONFile({
|
|
359
|
+
selection: {
|
|
360
|
+
type: 'condition',
|
|
361
|
+
length: 1,
|
|
362
|
+
condition: {
|
|
363
|
+
q: 'A-100',
|
|
364
|
+
selectAll: true,
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
await assert.rejects(async () => {
|
|
370
|
+
await executeCLI(
|
|
371
|
+
'user',
|
|
372
|
+
['workflow', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'query', '--body-file', bodyFile],
|
|
373
|
+
env as any,
|
|
374
|
+
async () => new Response('{}', { status: 200 }),
|
|
375
|
+
)
|
|
376
|
+
}, (error: unknown) => {
|
|
377
|
+
assert.ok(error instanceof CLIError)
|
|
378
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
379
|
+
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.selection.type'))
|
|
380
|
+
return true
|
|
381
|
+
})
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
test('selection action rejects all selection for archive action', async () => {
|
|
385
|
+
const bodyFile = tempJSONFile({
|
|
386
|
+
selection: {
|
|
387
|
+
type: 'all',
|
|
388
|
+
},
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
await assert.rejects(async () => {
|
|
392
|
+
await executeCLI(
|
|
393
|
+
'user',
|
|
394
|
+
['workflow', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile],
|
|
395
|
+
env as any,
|
|
396
|
+
async () => new Response('{}', { status: 200 }),
|
|
397
|
+
)
|
|
398
|
+
}, (error: unknown) => {
|
|
399
|
+
assert.ok(error instanceof CLIError)
|
|
400
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
401
|
+
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.selection.type'))
|
|
402
|
+
return true
|
|
403
|
+
})
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
test('selection action rejects selectAll-only condition for archive action', async () => {
|
|
407
|
+
const bodyFile = tempJSONFile({
|
|
408
|
+
selection: {
|
|
409
|
+
type: 'condition',
|
|
410
|
+
length: 1,
|
|
411
|
+
condition: {
|
|
412
|
+
selectAll: true,
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
await assert.rejects(async () => {
|
|
418
|
+
await executeCLI(
|
|
419
|
+
'user',
|
|
420
|
+
['workflow', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile],
|
|
421
|
+
env as any,
|
|
422
|
+
async () => new Response('{}', { status: 200 }),
|
|
423
|
+
)
|
|
424
|
+
}, (error: unknown) => {
|
|
425
|
+
assert.ok(error instanceof CLIError)
|
|
426
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
427
|
+
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.selection.condition'))
|
|
428
|
+
return true
|
|
429
|
+
})
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
test('insert entity rejects local file path for file field and points to arcubase upload', async () => {
|
|
433
|
+
const bodyFile = tempJSONFile({
|
|
434
|
+
fields: {
|
|
435
|
+
1001: 'Lead A',
|
|
436
|
+
1007: './contract.pdf',
|
|
437
|
+
},
|
|
438
|
+
})
|
|
439
|
+
const calls: Array<{ url: string; method?: string }> = []
|
|
440
|
+
|
|
441
|
+
await assert.rejects(async () => {
|
|
442
|
+
await executeCLI(
|
|
443
|
+
'user',
|
|
444
|
+
['workflow', 'insert-entity', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile],
|
|
445
|
+
env as any,
|
|
446
|
+
async (url, init) => {
|
|
447
|
+
calls.push({ url: String(url), method: init?.method })
|
|
448
|
+
if (String(url) === 'https://arcubase.example.com/entity/app_1/entity_1' && init?.method === 'GET') {
|
|
449
|
+
return new Response(JSON.stringify({
|
|
450
|
+
data: {
|
|
451
|
+
id: 1,
|
|
452
|
+
app_id: 1,
|
|
453
|
+
fields: [
|
|
454
|
+
{ id: 1001, type: 'text', label: 'Lead Name' },
|
|
455
|
+
{ id: 1007, type: 'file', label: 'Contract File' },
|
|
456
|
+
],
|
|
457
|
+
},
|
|
458
|
+
}), { status: 200 })
|
|
459
|
+
}
|
|
460
|
+
return new Response('{}', { status: 200 })
|
|
461
|
+
},
|
|
462
|
+
)
|
|
463
|
+
}, (error: unknown) => {
|
|
464
|
+
assert.ok(error instanceof CLIError)
|
|
465
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
466
|
+
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.fields.1007'))
|
|
467
|
+
assert.match(error.message, /arcubase upload \.\/contract\.pdf/)
|
|
468
|
+
assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/uploads.md')))
|
|
469
|
+
return true
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
assert.deepEqual(calls, [
|
|
473
|
+
{ url: 'https://arcubase.example.com/entity/app_1/entity_1', method: 'GET' },
|
|
474
|
+
])
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
test('update row rejects object value for image field and requires upload array', async () => {
|
|
478
|
+
const bodyFile = tempJSONFile({
|
|
479
|
+
changed_fields: [1008],
|
|
480
|
+
data: {
|
|
481
|
+
1008: {
|
|
482
|
+
upload_id: 123456,
|
|
483
|
+
file: 'lead-photo.jpg',
|
|
484
|
+
file_name: 'lead-photo.jpg',
|
|
485
|
+
meta: {},
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
await assert.rejects(async () => {
|
|
491
|
+
await executeCLI(
|
|
492
|
+
'user',
|
|
493
|
+
['workflow', 'update-entity-row', '--app-id', 'app_1', '--entity-id', 'entity_1', '--row-id', 'row_1', '--body-file', bodyFile],
|
|
494
|
+
env as any,
|
|
495
|
+
async (url, init) => {
|
|
496
|
+
if (String(url) === 'https://arcubase.example.com/entity/app_1/entity_1' && init?.method === 'GET') {
|
|
497
|
+
return new Response(JSON.stringify({
|
|
498
|
+
data: {
|
|
499
|
+
id: 1,
|
|
500
|
+
app_id: 1,
|
|
501
|
+
fields: [
|
|
502
|
+
{ id: 1008, type: 'image', label: 'Lead Photo' },
|
|
503
|
+
],
|
|
504
|
+
},
|
|
505
|
+
}), { status: 200 })
|
|
506
|
+
}
|
|
507
|
+
return new Response('{}', { status: 200 })
|
|
508
|
+
},
|
|
509
|
+
)
|
|
510
|
+
}, (error: unknown) => {
|
|
511
|
+
assert.ok(error instanceof CLIError)
|
|
512
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
513
|
+
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.data.1008'))
|
|
514
|
+
assert.match(error.message, /must be an array/)
|
|
515
|
+
assert.match(error.message, /arcubase upload <local-file>/)
|
|
516
|
+
return true
|
|
517
|
+
})
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
test('insert entity accepts upload array for file field after schema preflight', async () => {
|
|
521
|
+
const bodyFile = tempJSONFile({
|
|
522
|
+
fields: {
|
|
523
|
+
1001: 'Lead A',
|
|
524
|
+
1007: [
|
|
525
|
+
{
|
|
526
|
+
upload_id: 123456,
|
|
527
|
+
file: 'contract.pdf',
|
|
528
|
+
file_name: 'contract.pdf',
|
|
529
|
+
meta: {},
|
|
530
|
+
},
|
|
531
|
+
],
|
|
532
|
+
},
|
|
533
|
+
})
|
|
534
|
+
const calls: Array<{ url: string; method?: string }> = []
|
|
535
|
+
|
|
536
|
+
const out = await executeCLI(
|
|
537
|
+
'user',
|
|
538
|
+
['workflow', 'insert-entity', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile],
|
|
539
|
+
env as any,
|
|
540
|
+
async (url, init) => {
|
|
541
|
+
calls.push({ url: String(url), method: init?.method })
|
|
542
|
+
if (String(url) === 'https://arcubase.example.com/entity/app_1/entity_1' && init?.method === 'GET') {
|
|
543
|
+
return new Response(JSON.stringify({
|
|
544
|
+
data: {
|
|
545
|
+
id: 1,
|
|
546
|
+
app_id: 1,
|
|
547
|
+
fields: [
|
|
548
|
+
{ id: 1001, type: 'text', label: 'Lead Name' },
|
|
549
|
+
{ id: 1007, type: 'file', label: 'Contract File' },
|
|
550
|
+
],
|
|
551
|
+
},
|
|
552
|
+
}), { status: 200 })
|
|
553
|
+
}
|
|
554
|
+
return new Response(String(init?.body ?? '{}'), { status: 200 })
|
|
555
|
+
},
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
assert.equal(out.kind, 'result')
|
|
559
|
+
assert.deepEqual(calls, [
|
|
560
|
+
{ url: 'https://arcubase.example.com/entity/app_1/entity_1', method: 'GET' },
|
|
561
|
+
{ url: 'https://arcubase.example.com/entity/app_1/entity_1/insert', method: 'POST' },
|
|
562
|
+
])
|
|
563
|
+
assert.equal(out.data.fields['1007'][0].upload_id, 123456)
|
|
564
|
+
})
|
|
565
|
+
|
|
566
|
+
test('insert entity decorates upload bind failures with actionable guidance', async () => {
|
|
567
|
+
const bodyFile = tempJSONFile({
|
|
568
|
+
fields: {
|
|
569
|
+
1001: 'Lead A',
|
|
570
|
+
1007: [
|
|
571
|
+
{
|
|
572
|
+
upload_id: 123456,
|
|
573
|
+
file: 'contract.pdf',
|
|
574
|
+
file_name: 'contract.pdf',
|
|
575
|
+
meta: {},
|
|
576
|
+
},
|
|
577
|
+
],
|
|
578
|
+
},
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
await assert.rejects(async () => {
|
|
582
|
+
await executeCLI(
|
|
583
|
+
'user',
|
|
584
|
+
['workflow', 'insert-entity', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile],
|
|
585
|
+
env as any,
|
|
586
|
+
async (url, init) => {
|
|
587
|
+
if (String(url) === 'https://arcubase.example.com/entity/app_1/entity_1' && init?.method === 'GET') {
|
|
588
|
+
return new Response(JSON.stringify({
|
|
589
|
+
data: {
|
|
590
|
+
fields: [{ id: 1007, type: 'file', label: 'Contract File' }],
|
|
591
|
+
},
|
|
592
|
+
}), { status: 200 })
|
|
593
|
+
}
|
|
594
|
+
return new Response(JSON.stringify({
|
|
595
|
+
error: {
|
|
596
|
+
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',
|
|
597
|
+
type: 'system',
|
|
598
|
+
key: 'error.system',
|
|
599
|
+
},
|
|
600
|
+
trace_id: 'req_upload_bind',
|
|
601
|
+
}), { status: 200 })
|
|
602
|
+
},
|
|
603
|
+
)
|
|
604
|
+
}, (error: unknown) => {
|
|
605
|
+
assert.ok(error instanceof CLIError)
|
|
606
|
+
assert.equal(error.code, 'UPSTREAM_BODY_ERROR')
|
|
607
|
+
assert.equal(error.details?.traceId, 'req_upload_bind')
|
|
608
|
+
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')
|
|
609
|
+
assert.match(error.details?.hint ?? '', /rerun arcubase upload/)
|
|
610
|
+
assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/uploads.md')))
|
|
611
|
+
return true
|
|
612
|
+
})
|
|
613
|
+
})
|
|
614
|
+
|
|
615
|
+
test('selection action rejects condition selection without selectAll', async () => {
|
|
616
|
+
const bodyFile = tempJSONFile({
|
|
617
|
+
selection: {
|
|
618
|
+
type: 'condition',
|
|
619
|
+
length: 1,
|
|
620
|
+
condition: {
|
|
621
|
+
q: 'A-100',
|
|
622
|
+
},
|
|
623
|
+
},
|
|
624
|
+
})
|
|
625
|
+
|
|
626
|
+
await assert.rejects(async () => {
|
|
627
|
+
await executeCLI(
|
|
628
|
+
'user',
|
|
629
|
+
['workflow', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile],
|
|
630
|
+
env as any,
|
|
631
|
+
async () => new Response('{}', { status: 200 }),
|
|
632
|
+
)
|
|
633
|
+
}, (error: unknown) => {
|
|
634
|
+
assert.ok(error instanceof CLIError)
|
|
635
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
636
|
+
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.selection.condition.selectAll'))
|
|
637
|
+
return true
|
|
638
|
+
})
|
|
639
|
+
})
|
|
640
|
+
|
|
641
|
+
test('selection action rejects condition selection that uses text_search instead of q/filter keys', async () => {
|
|
642
|
+
const bodyFile = tempJSONFile({
|
|
643
|
+
selection: {
|
|
644
|
+
type: 'condition',
|
|
645
|
+
length: 1,
|
|
646
|
+
condition: {
|
|
647
|
+
text_search: 'A-100',
|
|
648
|
+
selectAll: true,
|
|
649
|
+
},
|
|
650
|
+
},
|
|
651
|
+
})
|
|
652
|
+
|
|
653
|
+
await assert.rejects(async () => {
|
|
654
|
+
await executeCLI(
|
|
655
|
+
'user',
|
|
656
|
+
['workflow', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile],
|
|
657
|
+
env as any,
|
|
658
|
+
async () => new Response('{}', { status: 200 }),
|
|
659
|
+
)
|
|
660
|
+
}, (error: unknown) => {
|
|
661
|
+
assert.ok(error instanceof CLIError)
|
|
662
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
663
|
+
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.selection.condition'))
|
|
664
|
+
assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/search-and-bulk-actions.md')))
|
|
665
|
+
return true
|
|
666
|
+
})
|
|
667
|
+
})
|
|
668
|
+
|
|
669
|
+
test('bulk update accepts condition selection with q', async () => {
|
|
670
|
+
const bodyFile = tempJSONFile({
|
|
671
|
+
selection: {
|
|
672
|
+
type: 'condition',
|
|
673
|
+
length: 1,
|
|
674
|
+
condition: {
|
|
675
|
+
q: 'A-100',
|
|
676
|
+
selectAll: true,
|
|
677
|
+
},
|
|
678
|
+
sorts: [
|
|
679
|
+
{ column: ':1002', order: 'desc' },
|
|
680
|
+
],
|
|
681
|
+
},
|
|
682
|
+
fields: [
|
|
683
|
+
{
|
|
684
|
+
id: 1002,
|
|
685
|
+
action: {
|
|
686
|
+
type: 'number_add',
|
|
687
|
+
number_add: 2,
|
|
688
|
+
},
|
|
689
|
+
},
|
|
690
|
+
],
|
|
691
|
+
})
|
|
692
|
+
|
|
693
|
+
const out = await executeCLI(
|
|
694
|
+
'user',
|
|
695
|
+
['entity', 'update-entity-bulk', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile],
|
|
696
|
+
env as any,
|
|
697
|
+
async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
|
|
698
|
+
)
|
|
699
|
+
assert.equal(out.kind, 'result')
|
|
700
|
+
assert.equal(out.data.selection.type, 'condition')
|
|
701
|
+
assert.equal(out.data.fields[0].action.type, 'number_add')
|
|
702
|
+
})
|
|
703
|
+
|
|
704
|
+
test('entity tags get-selection-tags accepts ids selection', async () => {
|
|
705
|
+
const bodyFile = tempJSONFile({
|
|
706
|
+
policy_id: '',
|
|
707
|
+
selection: {
|
|
708
|
+
type: 'ids',
|
|
709
|
+
ids: [4000000003],
|
|
710
|
+
length: 1,
|
|
711
|
+
},
|
|
712
|
+
})
|
|
713
|
+
|
|
714
|
+
const out = await executeCLI(
|
|
715
|
+
'admin',
|
|
716
|
+
['entity-tags', 'get-selection-tags', '--appId', 'app_1', '--entityId', 'entity_1', '--body-file', bodyFile],
|
|
717
|
+
env as any,
|
|
718
|
+
async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
|
|
719
|
+
)
|
|
720
|
+
assert.equal(out.kind, 'result')
|
|
721
|
+
assert.equal(out.data.selection.type, 'ids')
|
|
722
|
+
})
|
|
723
|
+
|
|
724
|
+
test('entity tags batch-add accepts tag_names and condition selection', async () => {
|
|
725
|
+
const bodyFile = tempJSONFile({
|
|
726
|
+
policy_id: '',
|
|
727
|
+
selection: {
|
|
728
|
+
type: 'condition',
|
|
729
|
+
length: 1,
|
|
730
|
+
condition: {
|
|
731
|
+
q: 'A-100',
|
|
732
|
+
selectAll: true,
|
|
733
|
+
},
|
|
734
|
+
},
|
|
735
|
+
tag_names: ['tag-a'],
|
|
736
|
+
})
|
|
737
|
+
|
|
738
|
+
const out = await executeCLI(
|
|
739
|
+
'admin',
|
|
740
|
+
['entity-tags', 'batch-add-entity-tags', '--appId', 'app_1', '--entityId', 'entity_1', '--body-file', bodyFile],
|
|
741
|
+
env as any,
|
|
742
|
+
async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
|
|
743
|
+
)
|
|
744
|
+
assert.equal(out.kind, 'result')
|
|
745
|
+
assert.equal(out.data.tag_names[0], 'tag-a')
|
|
746
|
+
})
|
|
747
|
+
|
|
748
|
+
test('invoke batch operator accepts backend request shape', async () => {
|
|
749
|
+
const bodyFile = tempJSONFile({
|
|
750
|
+
id: 'event_1',
|
|
751
|
+
data: {
|
|
752
|
+
action_id: 'action_1',
|
|
753
|
+
policy_id: '',
|
|
754
|
+
selection: {
|
|
755
|
+
type: 'all',
|
|
756
|
+
},
|
|
757
|
+
},
|
|
758
|
+
params: {},
|
|
759
|
+
})
|
|
760
|
+
|
|
761
|
+
const out = await executeCLI(
|
|
762
|
+
'user',
|
|
763
|
+
['entity', 'invoke-entity-batch-operator', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile],
|
|
764
|
+
env as any,
|
|
765
|
+
async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
|
|
766
|
+
)
|
|
767
|
+
assert.equal(out.kind, 'result')
|
|
768
|
+
assert.equal(out.data.id, 'event_1')
|
|
769
|
+
assert.equal(out.data.data.action_id, 'action_1')
|
|
770
|
+
assert.equal(out.data.data.selection.type, 'all')
|
|
771
|
+
})
|
|
772
|
+
|
|
773
|
+
test('query entity rejects camelCase viewMode and points to row crud doc', async () => {
|
|
774
|
+
const bodyFile = tempJSONFile({
|
|
775
|
+
limit: 20,
|
|
776
|
+
offset: 0,
|
|
777
|
+
viewMode: 'grid',
|
|
778
|
+
})
|
|
779
|
+
|
|
780
|
+
await assert.rejects(async () => {
|
|
781
|
+
await executeCLI(
|
|
782
|
+
'user',
|
|
783
|
+
['workflow', 'query-entity', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile],
|
|
784
|
+
env as any,
|
|
785
|
+
async () => new Response('{}', { status: 200 }),
|
|
786
|
+
)
|
|
787
|
+
}, (error: unknown) => {
|
|
788
|
+
assert.ok(error instanceof CLIError)
|
|
789
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
790
|
+
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.viewMode'))
|
|
791
|
+
assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/row-crud.md')))
|
|
792
|
+
return true
|
|
793
|
+
})
|
|
794
|
+
})
|
|
795
|
+
|
|
796
|
+
test('update entity row rejects fields key', async () => {
|
|
797
|
+
const bodyFile = tempJSONFile({
|
|
798
|
+
fields: {
|
|
799
|
+
1002: 99,
|
|
800
|
+
},
|
|
801
|
+
data: {
|
|
802
|
+
'1002': 99,
|
|
803
|
+
},
|
|
804
|
+
changed_fields: [1002],
|
|
805
|
+
})
|
|
806
|
+
|
|
807
|
+
await assert.rejects(async () => {
|
|
808
|
+
await executeCLI(
|
|
809
|
+
'user',
|
|
810
|
+
['workflow', 'update-entity-row', '--app-id', 'app_1', '--entity-id', 'entity_1', '--row-id', 'row_1', '--body-file', bodyFile],
|
|
811
|
+
env as any,
|
|
812
|
+
async () => new Response('{}', { status: 200 }),
|
|
813
|
+
)
|
|
814
|
+
}, (error: unknown) => {
|
|
815
|
+
assert.ok(error instanceof CLIError)
|
|
816
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
817
|
+
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.fields'))
|
|
818
|
+
assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/row-crud.md')))
|
|
819
|
+
return true
|
|
820
|
+
})
|
|
821
|
+
})
|
|
822
|
+
|
|
823
|
+
test('update entity row rejects changed_fields that are missing from data', async () => {
|
|
824
|
+
const bodyFile = tempJSONFile({
|
|
825
|
+
data: {
|
|
826
|
+
'1002': 99,
|
|
827
|
+
},
|
|
828
|
+
changed_fields: [1003],
|
|
829
|
+
})
|
|
830
|
+
|
|
831
|
+
await assert.rejects(async () => {
|
|
832
|
+
await executeCLI(
|
|
833
|
+
'user',
|
|
834
|
+
['workflow', 'update-entity-row', '--app-id', 'app_1', '--entity-id', 'entity_1', '--row-id', 'row_1', '--body-file', bodyFile],
|
|
835
|
+
env as any,
|
|
836
|
+
async () => new Response('{}', { status: 200 }),
|
|
837
|
+
)
|
|
838
|
+
}, (error: unknown) => {
|
|
839
|
+
assert.ok(error instanceof CLIError)
|
|
840
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
841
|
+
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.changed_fields.0'))
|
|
842
|
+
assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/row-crud.md')))
|
|
843
|
+
return true
|
|
844
|
+
})
|
|
845
|
+
})
|