@carthooks/arcubase-cli 0.1.13 → 0.1.15
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 +14934 -3808
- package/bundle/arcubase.mjs +14934 -3808
- package/dist/generated/command_registry.generated.d.ts +108 -0
- package/dist/generated/command_registry.generated.d.ts.map +1 -1
- package/dist/generated/command_registry.generated.js +145 -0
- package/dist/generated/zod_registry.generated.d.ts +23 -23
- package/dist/runtime/entity_import_mapping.d.ts +12 -0
- package/dist/runtime/entity_import_mapping.d.ts.map +1 -0
- package/dist/runtime/entity_import_mapping.js +133 -0
- package/dist/runtime/entity_save_schema.d.ts.map +1 -1
- package/dist/runtime/entity_save_schema.js +23 -2
- package/dist/runtime/execute.d.ts.map +1 -1
- package/dist/runtime/execute.js +309 -5
- package/dist/runtime/local_upload.d.ts +10 -0
- package/dist/runtime/local_upload.d.ts.map +1 -0
- package/dist/runtime/local_upload.js +73 -0
- package/dist/tests/entity_save_schema.test.js +38 -0
- package/dist/tests/execute_validation.test.js +2 -2
- package/package.json +7 -7
- package/sdk-dist/docs/runtime-reference/README.md +4 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/textarea.md +2 -2
- package/sdk-dist/docs/runtime-reference/examples/crm-01/lead.query.json +1 -1
- package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.query.json +1 -1
- package/sdk-dist/docs/runtime-reference/examples/wms-01/inventory-snapshot.query.json +1 -1
- package/sdk-dist/docs/runtime-reference/row-crud.md +55 -0
- package/sdk-dist/docs/runtime-reference/search-and-bulk-actions.md +26 -1
- package/sdk-dist/docs/runtime-reference/table-lifecycle.md +1 -1
- package/sdk-dist/generated/command_registry.generated.ts +145 -0
- package/sdk-dist/types/common.ts +1 -0
- package/src/generated/command_registry.generated.ts +145 -0
- package/src/runtime/entity_import_mapping.ts +172 -0
- package/src/runtime/entity_save_schema.ts +27 -3
- package/src/runtime/execute.ts +324 -6
- package/src/runtime/local_upload.ts +84 -0
- package/src/tests/command_registry.test.ts +4 -0
- package/src/tests/entity_import_mapping.test.ts +87 -0
- package/src/tests/entity_save_schema.test.ts +40 -0
- package/src/tests/execute_validation.test.ts +230 -5
- package/src/tests/help.test.ts +14 -0
- package/src/tests/local_upload.test.ts +47 -0
- package/src/tests/zod_registry.test.ts +27 -0
|
@@ -146,15 +146,196 @@ test('table update-schema rejects invalid body file', async () => {
|
|
|
146
146
|
})
|
|
147
147
|
|
|
148
148
|
test('app create rejects name longer than 20 chars', async () => {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
149
|
+
const bodyFile = tempJSONFile({ name: 'CLI Verify Happy Path' })
|
|
150
|
+
await assert.rejects(async () => {
|
|
151
|
+
await executeCLI('admin', ['app', 'create', '--body-file', bodyFile], env as any, async () => new Response('{}', { status: 200 }))
|
|
152
152
|
}, (error: unknown) => {
|
|
153
153
|
assert.ok(error instanceof CLIError)
|
|
154
154
|
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
155
155
|
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.name'))
|
|
156
156
|
return true
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
test('app create-from-bundle-url posts url body and returns task id', async () => {
|
|
161
|
+
const calls: Array<{ url: string; method?: string; body?: string }> = []
|
|
162
|
+
const out = await executeCLI(
|
|
163
|
+
'admin',
|
|
164
|
+
['app', 'create-from-bundle-url', '--url', 'https://cdn.example.com/app-bundles/2026/06/17/a.zip', '--name', 'Demo'],
|
|
165
|
+
env as any,
|
|
166
|
+
async (url, init) => {
|
|
167
|
+
calls.push({ url: String(url), method: init?.method, body: String(init?.body ?? '') })
|
|
168
|
+
return new Response(JSON.stringify({ data: { task_id: 'task-1' } }), { status: 200 })
|
|
169
|
+
},
|
|
170
|
+
)
|
|
171
|
+
assert.equal(calls[0].method, 'POST')
|
|
172
|
+
assert.match(calls[0].url, /\/api\/apps\/import-task$/)
|
|
173
|
+
assert.deepEqual(JSON.parse(calls[0].body ?? '{}'), {
|
|
174
|
+
url: 'https://cdn.example.com/app-bundles/2026/06/17/a.zip',
|
|
175
|
+
name: 'Demo',
|
|
176
|
+
})
|
|
177
|
+
assert.equal((out as any).data.data.task_id, 'task-1')
|
|
178
|
+
assert.deepEqual((out as any).nextAction, {
|
|
179
|
+
command: 'app watch-import-task',
|
|
180
|
+
args: ['--task-id', 'task-1', '--ttl-seconds', '300'],
|
|
181
|
+
hint: 'run this command until the task returns status=finished and app_id > 0; task_id alone is not success',
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
test('app create --url points to bundle import flow', async () => {
|
|
186
|
+
await assert.rejects(async () => {
|
|
187
|
+
await executeCLI(
|
|
188
|
+
'admin',
|
|
189
|
+
['app', 'create', '--url', 'https://cdn.example.com/app-bundles/2026/06/17/a.zip', '--name', 'Demo'],
|
|
190
|
+
env as any,
|
|
191
|
+
async () => new Response('{}', { status: 200 }),
|
|
192
|
+
)
|
|
193
|
+
}, (error: unknown) => {
|
|
194
|
+
assert.ok(error instanceof CLIError)
|
|
195
|
+
assert.equal(error.code, 'UNKNOWN_FLAG')
|
|
196
|
+
assert.equal(error.details?.hint, 'app bundle URLs must use app create-from-bundle-url; do not retry app create')
|
|
197
|
+
assert.deepEqual(error.details?.commonMistakes, [
|
|
198
|
+
'do not use app create for app bundle URLs',
|
|
199
|
+
'do not put url inside app create --body-json',
|
|
200
|
+
])
|
|
201
|
+
assert.deepEqual(error.details?.suggestions, [
|
|
202
|
+
'retry with: app create-from-bundle-url --url <public_http_zip_url> --name <app_name>',
|
|
203
|
+
'then watch with: app watch-import-task --task-id <task_id> --ttl-seconds 300',
|
|
204
|
+
])
|
|
205
|
+
return true
|
|
206
|
+
})
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
test('app create-from-bundle-url rejects unsupported url scheme before request', async () => {
|
|
210
|
+
let called = false
|
|
211
|
+
await assert.rejects(async () => {
|
|
212
|
+
await executeCLI(
|
|
213
|
+
'admin',
|
|
214
|
+
['app', 'create-from-bundle-url', '--url', 'file:///tmp/app.zip', '--name', 'Demo'],
|
|
215
|
+
env as any,
|
|
216
|
+
async () => {
|
|
217
|
+
called = true
|
|
218
|
+
return new Response('{}', { status: 200 })
|
|
219
|
+
},
|
|
220
|
+
)
|
|
221
|
+
}, (error: unknown) => {
|
|
222
|
+
assert.ok(error instanceof CLIError)
|
|
223
|
+
assert.equal(error.code, 'INVALID_IMPORT_URL')
|
|
224
|
+
assert.equal(called, false)
|
|
225
|
+
return true
|
|
226
|
+
})
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
test('app watch-import-task polls until finished', async () => {
|
|
230
|
+
const statuses = [
|
|
231
|
+
{ data: { task_id: 'task-1', status: 'processing' } },
|
|
232
|
+
{ data: { task_id: 'task-1', status: 'finished', app_id: 2188889999 } },
|
|
233
|
+
]
|
|
234
|
+
const calls: string[] = []
|
|
235
|
+
const out = await executeCLI(
|
|
236
|
+
'admin',
|
|
237
|
+
['app', 'watch-import-task', '--task-id', 'task-1', '--ttl-seconds', '1'],
|
|
238
|
+
{ ...env, ARCUBASE_IMPORT_TASK_WATCH_INTERVAL_MS: '1' } as any,
|
|
239
|
+
async (url) => {
|
|
240
|
+
calls.push(String(url))
|
|
241
|
+
return new Response(JSON.stringify(statuses.shift()), { status: 200 })
|
|
242
|
+
},
|
|
243
|
+
)
|
|
244
|
+
assert.equal(calls.length, 2)
|
|
245
|
+
assert.equal((out as any).data.data.status, 'finished')
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
test('app watch-import-task rejects finished status without app id', async () => {
|
|
249
|
+
await assert.rejects(async () => {
|
|
250
|
+
await executeCLI(
|
|
251
|
+
'admin',
|
|
252
|
+
['app', 'watch-import-task', '--task-id', 'task-1', '--ttl-seconds', '1'],
|
|
253
|
+
{ ...env, ARCUBASE_IMPORT_TASK_WATCH_INTERVAL_MS: '1' } as any,
|
|
254
|
+
async () => new Response(JSON.stringify({ data: { task_id: 'task-1', status: 'finished' } }), { status: 200 }),
|
|
255
|
+
)
|
|
256
|
+
}, (error: unknown) => {
|
|
257
|
+
assert.ok(error instanceof CLIError)
|
|
258
|
+
assert.equal(error.code, 'IMPORT_TASK_APP_ID_MISSING')
|
|
259
|
+
return true
|
|
260
|
+
})
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
test('row import-csv validates mapping, uploads local file, starts import, and watches task', async () => {
|
|
264
|
+
const csvFile = tempJSONFile('ID,客户名称\n12,Alice\n').replace(/payload\.json$/, 'records.csv')
|
|
265
|
+
fs.writeFileSync(csvFile, 'ID,客户名称\n12,Alice\n')
|
|
266
|
+
const mappingFile = tempJSONFile({
|
|
267
|
+
mode: 'addorupdate',
|
|
268
|
+
header: true,
|
|
269
|
+
dataStartRow: 2,
|
|
270
|
+
recordId: { header: 'ID' },
|
|
271
|
+
fields: [{ fieldId: 1001, header: '客户名称' }],
|
|
157
272
|
})
|
|
273
|
+
const calls: Array<{ url: string; method?: string; body?: string; isForm?: boolean }> = []
|
|
274
|
+
const out = await executeCLI(
|
|
275
|
+
'user',
|
|
276
|
+
['row', 'import-csv', '--app-id', 'app_1', '--table-id', 'table_1', '--file', csvFile, '--mapping-file', mappingFile, '--ttl-seconds', '1'],
|
|
277
|
+
{ ...env, ARCUBASE_IMPORT_TASK_WATCH_INTERVAL_MS: '1' } as any,
|
|
278
|
+
async (url, init) => {
|
|
279
|
+
calls.push({
|
|
280
|
+
url: String(url),
|
|
281
|
+
method: init?.method,
|
|
282
|
+
body: typeof init?.body === 'string' ? init.body : undefined,
|
|
283
|
+
isForm: init?.body instanceof FormData,
|
|
284
|
+
})
|
|
285
|
+
if (String(url).endsWith('/api/entity/app_1/table_1/import') && init?.method === 'POST') {
|
|
286
|
+
const body = JSON.parse(String(init.body))
|
|
287
|
+
if (body._import_action === 'init-token') {
|
|
288
|
+
return new Response(JSON.stringify({ data: { taskId: 'task-1', token: { id: 123, mode: 's3', token_data: { host: 'https://uploads.example.com', dir: 'upload/123/', policy: 'p', signature: 's', accessid: 'a' } } } }), { status: 200 })
|
|
289
|
+
}
|
|
290
|
+
if (body._import_action === 'uploaded') {
|
|
291
|
+
return new Response(JSON.stringify({ data: 'task-1' }), { status: 200 })
|
|
292
|
+
}
|
|
293
|
+
if (body._import_action === 'do-process') {
|
|
294
|
+
assert.deepEqual(body.config, {
|
|
295
|
+
fileFormat: 'csv',
|
|
296
|
+
importMode: 'addorupdate',
|
|
297
|
+
titleRowIndex: 0,
|
|
298
|
+
csvDelimiter: ',',
|
|
299
|
+
mapper: { '$id': 0, '1001': 1 },
|
|
300
|
+
})
|
|
301
|
+
return new Response(JSON.stringify({ data: { taskId: 'task-1', status: 'processing', state: {} } }), { status: 200 })
|
|
302
|
+
}
|
|
303
|
+
if (body._import_action === 'get-state') {
|
|
304
|
+
return new Response(JSON.stringify({ data: { taskId: 'task-1', status: 'finished', state: { state: { SuccessNum: 1 } } } }), { status: 200 })
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (String(url) === 'https://uploads.example.com') {
|
|
308
|
+
return new Response(null, { status: 204 })
|
|
309
|
+
}
|
|
310
|
+
throw new Error(`unexpected fetch ${url}`)
|
|
311
|
+
},
|
|
312
|
+
)
|
|
313
|
+
assert.equal(out.kind, 'result')
|
|
314
|
+
assert.equal(out.data.data.status, 'finished')
|
|
315
|
+
assert.ok(calls.some((call) => call.isForm), 'expected local file upload form request')
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
test('row import-csv missing header fails before fetch', async () => {
|
|
319
|
+
const csvFile = tempJSONFile('Name\nAlice\n').replace(/payload\.json$/, 'records.csv')
|
|
320
|
+
fs.writeFileSync(csvFile, 'Name\nAlice\n')
|
|
321
|
+
const mappingFile = tempJSONFile({
|
|
322
|
+
mode: 'addonly',
|
|
323
|
+
header: true,
|
|
324
|
+
dataStartRow: 2,
|
|
325
|
+
fields: [{ fieldId: 1001, header: 'Missing' }],
|
|
326
|
+
})
|
|
327
|
+
let called = false
|
|
328
|
+
await assert.rejects(async () => {
|
|
329
|
+
await executeCLI('user', ['row', 'import-csv', '--app-id', 'app_1', '--table-id', 'table_1', '--file', csvFile, '--mapping-file', mappingFile], env as any, async () => {
|
|
330
|
+
called = true
|
|
331
|
+
return new Response('{}')
|
|
332
|
+
})
|
|
333
|
+
}, (error: unknown) => {
|
|
334
|
+
assert.ok(error instanceof CLIError)
|
|
335
|
+
assert.equal(error.code, 'INVALID_IMPORT_MAPPING')
|
|
336
|
+
return true
|
|
337
|
+
})
|
|
338
|
+
assert.equal(called, false)
|
|
158
339
|
})
|
|
159
340
|
|
|
160
341
|
test('table create rejects empty schema body before request', async () => {
|
|
@@ -387,7 +568,7 @@ test('table update-schema rejects per-user permission patches', async () => {
|
|
|
387
568
|
layout: [[1001], [1002]],
|
|
388
569
|
fields: [
|
|
389
570
|
{ id: 1001, label: 'Title', type: 'text', required: true, unique: false, editable: true, visible: true, show_label: true, scannable: false, default_value_mode: 0, description: '', value: null, number_decimal: 0, depends: [], key: 'title', code_index: false, options: { type: 'text', placeholder: '', lengthLimit: false, lengthMin: 0, lengthMax: 255 }, transfers: null, children: null },
|
|
390
|
-
{ id: 1002, label: 'Description', type: 'textarea', required: false, unique: false, editable: true, visible: true, show_label: true, scannable: false, default_value_mode: 0, description: '', value: null, number_decimal: 0, depends: [], key: 'description', code_index: false, options: { placeholder: '', html: false, size:
|
|
571
|
+
{ id: 1002, label: 'Description', type: 'textarea', required: false, unique: false, editable: true, visible: true, show_label: true, scannable: false, default_value_mode: 0, description: '', value: null, number_decimal: 0, depends: [], key: 'description', code_index: false, options: { placeholder: '', html: false, size: 6, lengthLimit: false, lengthMin: 0, lengthMax: 2000 }, transfers: null, children: null, permission: { user: { read: false } } },
|
|
391
572
|
],
|
|
392
573
|
options: { Buttons: [], CustomActions: [], FrontendEvents: [], TitleFormat: { Fields: [], Parts: [], Body: '' }, misc: {} },
|
|
393
574
|
workflow_enabled: false,
|
|
@@ -770,6 +951,45 @@ test('row query rejects camelCase viewMode and points to row crud doc', async ()
|
|
|
770
951
|
})
|
|
771
952
|
})
|
|
772
953
|
|
|
954
|
+
test('row query rejects invalid search shape through EntityQueryReqVO zod schema', async () => {
|
|
955
|
+
for (const search of [
|
|
956
|
+
{ conditions: [{ field_id: 1002, op: 'eq', value: 'member_1' }] },
|
|
957
|
+
{ field_filters: [{ field_id: 1002, operator: 'eq', value: 'member_1' }] },
|
|
958
|
+
{ q: ['SO-1001'] },
|
|
959
|
+
]) {
|
|
960
|
+
const bodyFile = tempJSONFile({ limit: 20, offset: 0, search })
|
|
961
|
+
let dataQueryFetchCount = 0
|
|
962
|
+
await assert.rejects(async () => {
|
|
963
|
+
await executeCLI(
|
|
964
|
+
'user',
|
|
965
|
+
['row', 'query', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile],
|
|
966
|
+
env as any,
|
|
967
|
+
withRowPolicyResolver('table_1', ['view'], async () => {
|
|
968
|
+
dataQueryFetchCount += 1
|
|
969
|
+
return new Response('{}', { status: 200 })
|
|
970
|
+
}),
|
|
971
|
+
)
|
|
972
|
+
}, (error: unknown) => {
|
|
973
|
+
assert.ok(error instanceof CLIError)
|
|
974
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
975
|
+
assert.equal(error.details?.requestType, 'EntityQueryReqVO')
|
|
976
|
+
assert.ok((error.details?.issues ?? []).some((item) => item.path.startsWith('body.search.')))
|
|
977
|
+
assert.ok(error.details?.tsHints?.some((item) => item.symbol === 'EntityQueryReqVO' && item.file === '/runtime/arcubase-sdk/types/user-action.ts'))
|
|
978
|
+
assert.deepEqual(error.details?.shapeHint, {
|
|
979
|
+
limit: 20,
|
|
980
|
+
offset: 0,
|
|
981
|
+
search: { q: 'SO-1001' },
|
|
982
|
+
sorts: [{ column: ':1001', order: 'asc' }],
|
|
983
|
+
})
|
|
984
|
+
assert.ok(error.details?.commonMistakes?.some((item) => item.includes('string route-query keys')))
|
|
985
|
+
assert.ok(error.details?.suggestions?.some((item) => item.includes('"search":{"q":"SO-1001"}')))
|
|
986
|
+
assert.ok(error.details?.suggestions?.some((item) => item.includes('"filter_:1002"')))
|
|
987
|
+
return true
|
|
988
|
+
})
|
|
989
|
+
assert.equal(dataQueryFetchCount, 0)
|
|
990
|
+
}
|
|
991
|
+
})
|
|
992
|
+
|
|
773
993
|
test('row query unknown limit flag gives body-json retry path', async () => {
|
|
774
994
|
await assert.rejects(async () => {
|
|
775
995
|
await executeCLI('user', ['row', 'query', '--app-id', '2188900691', '--table-id', '2188900694', '--limit', '1', '--offset', '0'], env as any, async () => new Response('{}', { status: 200 }))
|
|
@@ -777,7 +997,12 @@ test('row query unknown limit flag gives body-json retry path', async () => {
|
|
|
777
997
|
assert.ok(error instanceof CLIError)
|
|
778
998
|
assert.equal(error.code, 'UNKNOWN_FLAG')
|
|
779
999
|
assert.equal(error.details?.requestType, 'EntityQueryReqVO')
|
|
780
|
-
assert.deepEqual(error.details?.shapeHint, {
|
|
1000
|
+
assert.deepEqual(error.details?.shapeHint, {
|
|
1001
|
+
limit: 20,
|
|
1002
|
+
offset: 0,
|
|
1003
|
+
search: { q: 'SO-1001' },
|
|
1004
|
+
sorts: [{ column: ':1001', order: 'asc' }],
|
|
1005
|
+
})
|
|
781
1006
|
assert.ok(error.details?.suggestions?.some((item) => item.includes('--body-json')))
|
|
782
1007
|
return true
|
|
783
1008
|
})
|
package/src/tests/help.test.ts
CHANGED
|
@@ -66,6 +66,20 @@ test('user row help does not expose internal policy id input', async () => {
|
|
|
66
66
|
assert.doesNotMatch(out.text, /policy_id/)
|
|
67
67
|
})
|
|
68
68
|
|
|
69
|
+
test('row import help explains mapping formats and local files', async () => {
|
|
70
|
+
const excel = await executeCLI('user', ['row', 'import-excel', '--help'], env as any, async () => new Response('{}'))
|
|
71
|
+
assert.equal(excel.kind, 'help')
|
|
72
|
+
assert.match(excel.text, /mapping-file/)
|
|
73
|
+
assert.match(excel.text, /Excel columns use letters/)
|
|
74
|
+
assert.match(excel.text, /local file/)
|
|
75
|
+
|
|
76
|
+
const csv = await executeCLI('user', ['row', 'import-csv', '--help'], env as any, async () => new Response('{}'))
|
|
77
|
+
assert.equal(csv.kind, 'help')
|
|
78
|
+
assert.match(csv.text, /CSV mapping uses headers/)
|
|
79
|
+
assert.match(csv.text, /does not support embedded images/)
|
|
80
|
+
assert.match(csv.text, /arcubase-admin table get/)
|
|
81
|
+
})
|
|
82
|
+
|
|
69
83
|
test('admin table update-schema help uses table nouns and body-json', async () => {
|
|
70
84
|
const out = await executeCLI('admin', ['table', 'update-schema', '--help'], env as any, async () => new Response('{}'))
|
|
71
85
|
assert.equal(out.kind, 'help')
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import fs from 'fs'
|
|
4
|
+
import os from 'os'
|
|
5
|
+
import path from 'path'
|
|
6
|
+
import { uploadLocalFileWithToken } from '../runtime/local_upload.js'
|
|
7
|
+
import { CLIError } from '../runtime/errors.js'
|
|
8
|
+
|
|
9
|
+
function tempFile(name: string, content = 'hello'): string {
|
|
10
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'arcubase-upload-'))
|
|
11
|
+
const file = path.join(dir, name)
|
|
12
|
+
fs.writeFileSync(file, content)
|
|
13
|
+
return file
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
test('uploadLocalFileWithToken uploads aliyun-compatible post policy', async () => {
|
|
17
|
+
const file = tempFile('records.csv')
|
|
18
|
+
const calls: Array<{ url: string; method?: string; body: any }> = []
|
|
19
|
+
await uploadLocalFileWithToken(file, {
|
|
20
|
+
mode: 's3',
|
|
21
|
+
token_data: {
|
|
22
|
+
host: 'https://uploads.example.com',
|
|
23
|
+
dir: 'upload/123/',
|
|
24
|
+
policy: 'policy',
|
|
25
|
+
signature: 'signature',
|
|
26
|
+
accessid: 'access',
|
|
27
|
+
expire: 123,
|
|
28
|
+
},
|
|
29
|
+
}, async (url, init) => {
|
|
30
|
+
calls.push({ url: String(url), method: init?.method, body: init?.body })
|
|
31
|
+
return new Response(null, { status: 204 })
|
|
32
|
+
})
|
|
33
|
+
assert.equal(calls[0].url, 'https://uploads.example.com')
|
|
34
|
+
assert.equal(calls[0].method, 'POST')
|
|
35
|
+
assert.ok(calls[0].body instanceof FormData)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('uploadLocalFileWithToken rejects unsupported token shape', async () => {
|
|
39
|
+
const file = tempFile('records.csv')
|
|
40
|
+
await assert.rejects(async () => {
|
|
41
|
+
await uploadLocalFileWithToken(file, { mode: 'unknown', token_data: {} }, async () => new Response(''))
|
|
42
|
+
}, (error: unknown) => {
|
|
43
|
+
assert.ok(error instanceof CLIError)
|
|
44
|
+
assert.equal(error.code, 'UPLOAD_TOKEN_UNSUPPORTED')
|
|
45
|
+
return true
|
|
46
|
+
})
|
|
47
|
+
})
|
|
@@ -14,6 +14,33 @@ test('EntityQueryReqVO schema exists', () => {
|
|
|
14
14
|
assert.ok(getBodySchema('EntityQueryReqVO'))
|
|
15
15
|
})
|
|
16
16
|
|
|
17
|
+
test('EntityQueryReqVO search only accepts route-query string keys', () => {
|
|
18
|
+
const schema = getBodySchema('EntityQueryReqVO')
|
|
19
|
+
assert.ok(schema)
|
|
20
|
+
|
|
21
|
+
assert.equal(schema.safeParse({
|
|
22
|
+
limit: 100,
|
|
23
|
+
offset: 0,
|
|
24
|
+
search: {
|
|
25
|
+
q: 'SO-1001',
|
|
26
|
+
tab: 'all',
|
|
27
|
+
'filter_:1002': 'eq:member_1',
|
|
28
|
+
},
|
|
29
|
+
}).success, true)
|
|
30
|
+
|
|
31
|
+
for (const search of [
|
|
32
|
+
{ conditions: [{ field_id: 1002, op: 'eq', value: 'member_1' }] },
|
|
33
|
+
{ condition: { field_id: 1002, op: 'eq', value: 'member_1' } },
|
|
34
|
+
{ field_filters: [{ field_id: 1002, operator: 'eq', value: 'member_1' }] },
|
|
35
|
+
{ filter: { field_id: 1002, value: 'member_1' } },
|
|
36
|
+
{ order: [{ column: ':1001', order: 'asc' }] },
|
|
37
|
+
{ q: ['SO-1001'] },
|
|
38
|
+
{ 'filter_:1002': { op: 'eq', value: 'member_1' } },
|
|
39
|
+
]) {
|
|
40
|
+
assert.equal(schema.safeParse({ limit: 100, offset: 0, search }).success, false)
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
17
44
|
test('unknown request type has no schema and no unsupported reason', () => {
|
|
18
45
|
assert.equal(getBodySchema('DefinitelyMissingReqVO'), null)
|
|
19
46
|
assert.equal(getUnsupportedBodySchemaReason('DefinitelyMissingReqVO'), null)
|