@carthooks/arcubase-cli 0.1.13 → 0.1.16
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 +6 -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 +231 -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,197 @@ 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, name: '企业基础业务数据应用' } },
|
|
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
|
+
assert.equal((out as any).data.data.name, '企业基础业务数据应用')
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
test('app watch-import-task rejects finished status without app id', async () => {
|
|
250
|
+
await assert.rejects(async () => {
|
|
251
|
+
await executeCLI(
|
|
252
|
+
'admin',
|
|
253
|
+
['app', 'watch-import-task', '--task-id', 'task-1', '--ttl-seconds', '1'],
|
|
254
|
+
{ ...env, ARCUBASE_IMPORT_TASK_WATCH_INTERVAL_MS: '1' } as any,
|
|
255
|
+
async () => new Response(JSON.stringify({ data: { task_id: 'task-1', status: 'finished' } }), { status: 200 }),
|
|
256
|
+
)
|
|
257
|
+
}, (error: unknown) => {
|
|
258
|
+
assert.ok(error instanceof CLIError)
|
|
259
|
+
assert.equal(error.code, 'IMPORT_TASK_APP_ID_MISSING')
|
|
260
|
+
return true
|
|
261
|
+
})
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
test('row import-csv validates mapping, uploads local file, starts import, and watches task', async () => {
|
|
265
|
+
const csvFile = tempJSONFile('ID,客户名称\n12,Alice\n').replace(/payload\.json$/, 'records.csv')
|
|
266
|
+
fs.writeFileSync(csvFile, 'ID,客户名称\n12,Alice\n')
|
|
267
|
+
const mappingFile = tempJSONFile({
|
|
268
|
+
mode: 'addorupdate',
|
|
269
|
+
header: true,
|
|
270
|
+
dataStartRow: 2,
|
|
271
|
+
recordId: { header: 'ID' },
|
|
272
|
+
fields: [{ fieldId: 1001, header: '客户名称' }],
|
|
157
273
|
})
|
|
274
|
+
const calls: Array<{ url: string; method?: string; body?: string; isForm?: boolean }> = []
|
|
275
|
+
const out = await executeCLI(
|
|
276
|
+
'user',
|
|
277
|
+
['row', 'import-csv', '--app-id', 'app_1', '--table-id', 'table_1', '--file', csvFile, '--mapping-file', mappingFile, '--ttl-seconds', '1'],
|
|
278
|
+
{ ...env, ARCUBASE_IMPORT_TASK_WATCH_INTERVAL_MS: '1' } as any,
|
|
279
|
+
async (url, init) => {
|
|
280
|
+
calls.push({
|
|
281
|
+
url: String(url),
|
|
282
|
+
method: init?.method,
|
|
283
|
+
body: typeof init?.body === 'string' ? init.body : undefined,
|
|
284
|
+
isForm: init?.body instanceof FormData,
|
|
285
|
+
})
|
|
286
|
+
if (String(url).endsWith('/api/entity/app_1/table_1/import') && init?.method === 'POST') {
|
|
287
|
+
const body = JSON.parse(String(init.body))
|
|
288
|
+
if (body._import_action === 'init-token') {
|
|
289
|
+
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 })
|
|
290
|
+
}
|
|
291
|
+
if (body._import_action === 'uploaded') {
|
|
292
|
+
return new Response(JSON.stringify({ data: 'task-1' }), { status: 200 })
|
|
293
|
+
}
|
|
294
|
+
if (body._import_action === 'do-process') {
|
|
295
|
+
assert.deepEqual(body.config, {
|
|
296
|
+
fileFormat: 'csv',
|
|
297
|
+
importMode: 'addorupdate',
|
|
298
|
+
titleRowIndex: 0,
|
|
299
|
+
csvDelimiter: ',',
|
|
300
|
+
mapper: { '$id': 0, '1001': 1 },
|
|
301
|
+
})
|
|
302
|
+
return new Response(JSON.stringify({ data: { taskId: 'task-1', status: 'processing', state: {} } }), { status: 200 })
|
|
303
|
+
}
|
|
304
|
+
if (body._import_action === 'get-state') {
|
|
305
|
+
return new Response(JSON.stringify({ data: { taskId: 'task-1', status: 'finished', state: { state: { SuccessNum: 1 } } } }), { status: 200 })
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (String(url) === 'https://uploads.example.com') {
|
|
309
|
+
return new Response(null, { status: 204 })
|
|
310
|
+
}
|
|
311
|
+
throw new Error(`unexpected fetch ${url}`)
|
|
312
|
+
},
|
|
313
|
+
)
|
|
314
|
+
assert.equal(out.kind, 'result')
|
|
315
|
+
assert.equal(out.data.data.status, 'finished')
|
|
316
|
+
assert.ok(calls.some((call) => call.isForm), 'expected local file upload form request')
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
test('row import-csv missing header fails before fetch', async () => {
|
|
320
|
+
const csvFile = tempJSONFile('Name\nAlice\n').replace(/payload\.json$/, 'records.csv')
|
|
321
|
+
fs.writeFileSync(csvFile, 'Name\nAlice\n')
|
|
322
|
+
const mappingFile = tempJSONFile({
|
|
323
|
+
mode: 'addonly',
|
|
324
|
+
header: true,
|
|
325
|
+
dataStartRow: 2,
|
|
326
|
+
fields: [{ fieldId: 1001, header: 'Missing' }],
|
|
327
|
+
})
|
|
328
|
+
let called = false
|
|
329
|
+
await assert.rejects(async () => {
|
|
330
|
+
await executeCLI('user', ['row', 'import-csv', '--app-id', 'app_1', '--table-id', 'table_1', '--file', csvFile, '--mapping-file', mappingFile], env as any, async () => {
|
|
331
|
+
called = true
|
|
332
|
+
return new Response('{}')
|
|
333
|
+
})
|
|
334
|
+
}, (error: unknown) => {
|
|
335
|
+
assert.ok(error instanceof CLIError)
|
|
336
|
+
assert.equal(error.code, 'INVALID_IMPORT_MAPPING')
|
|
337
|
+
return true
|
|
338
|
+
})
|
|
339
|
+
assert.equal(called, false)
|
|
158
340
|
})
|
|
159
341
|
|
|
160
342
|
test('table create rejects empty schema body before request', async () => {
|
|
@@ -387,7 +569,7 @@ test('table update-schema rejects per-user permission patches', async () => {
|
|
|
387
569
|
layout: [[1001], [1002]],
|
|
388
570
|
fields: [
|
|
389
571
|
{ 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:
|
|
572
|
+
{ 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
573
|
],
|
|
392
574
|
options: { Buttons: [], CustomActions: [], FrontendEvents: [], TitleFormat: { Fields: [], Parts: [], Body: '' }, misc: {} },
|
|
393
575
|
workflow_enabled: false,
|
|
@@ -770,6 +952,45 @@ test('row query rejects camelCase viewMode and points to row crud doc', async ()
|
|
|
770
952
|
})
|
|
771
953
|
})
|
|
772
954
|
|
|
955
|
+
test('row query rejects invalid search shape through EntityQueryReqVO zod schema', async () => {
|
|
956
|
+
for (const search of [
|
|
957
|
+
{ conditions: [{ field_id: 1002, op: 'eq', value: 'member_1' }] },
|
|
958
|
+
{ field_filters: [{ field_id: 1002, operator: 'eq', value: 'member_1' }] },
|
|
959
|
+
{ q: ['SO-1001'] },
|
|
960
|
+
]) {
|
|
961
|
+
const bodyFile = tempJSONFile({ limit: 20, offset: 0, search })
|
|
962
|
+
let dataQueryFetchCount = 0
|
|
963
|
+
await assert.rejects(async () => {
|
|
964
|
+
await executeCLI(
|
|
965
|
+
'user',
|
|
966
|
+
['row', 'query', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile],
|
|
967
|
+
env as any,
|
|
968
|
+
withRowPolicyResolver('table_1', ['view'], async () => {
|
|
969
|
+
dataQueryFetchCount += 1
|
|
970
|
+
return new Response('{}', { status: 200 })
|
|
971
|
+
}),
|
|
972
|
+
)
|
|
973
|
+
}, (error: unknown) => {
|
|
974
|
+
assert.ok(error instanceof CLIError)
|
|
975
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
976
|
+
assert.equal(error.details?.requestType, 'EntityQueryReqVO')
|
|
977
|
+
assert.ok((error.details?.issues ?? []).some((item) => item.path.startsWith('body.search.')))
|
|
978
|
+
assert.ok(error.details?.tsHints?.some((item) => item.symbol === 'EntityQueryReqVO' && item.file === '/runtime/arcubase-sdk/types/user-action.ts'))
|
|
979
|
+
assert.deepEqual(error.details?.shapeHint, {
|
|
980
|
+
limit: 20,
|
|
981
|
+
offset: 0,
|
|
982
|
+
search: { q: 'SO-1001' },
|
|
983
|
+
sorts: [{ column: ':1001', order: 'asc' }],
|
|
984
|
+
})
|
|
985
|
+
assert.ok(error.details?.commonMistakes?.some((item) => item.includes('string route-query keys')))
|
|
986
|
+
assert.ok(error.details?.suggestions?.some((item) => item.includes('"search":{"q":"SO-1001"}')))
|
|
987
|
+
assert.ok(error.details?.suggestions?.some((item) => item.includes('"filter_:1002"')))
|
|
988
|
+
return true
|
|
989
|
+
})
|
|
990
|
+
assert.equal(dataQueryFetchCount, 0)
|
|
991
|
+
}
|
|
992
|
+
})
|
|
993
|
+
|
|
773
994
|
test('row query unknown limit flag gives body-json retry path', async () => {
|
|
774
995
|
await assert.rejects(async () => {
|
|
775
996
|
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 +998,12 @@ test('row query unknown limit flag gives body-json retry path', async () => {
|
|
|
777
998
|
assert.ok(error instanceof CLIError)
|
|
778
999
|
assert.equal(error.code, 'UNKNOWN_FLAG')
|
|
779
1000
|
assert.equal(error.details?.requestType, 'EntityQueryReqVO')
|
|
780
|
-
assert.deepEqual(error.details?.shapeHint, {
|
|
1001
|
+
assert.deepEqual(error.details?.shapeHint, {
|
|
1002
|
+
limit: 20,
|
|
1003
|
+
offset: 0,
|
|
1004
|
+
search: { q: 'SO-1001' },
|
|
1005
|
+
sorts: [{ column: ':1001', order: 'asc' }],
|
|
1006
|
+
})
|
|
781
1007
|
assert.ok(error.details?.suggestions?.some((item) => item.includes('--body-json')))
|
|
782
1008
|
return true
|
|
783
1009
|
})
|
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)
|