@carthooks/arcubase-cli 0.1.3 → 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.
Files changed (33) hide show
  1. package/bundle/arcubase-admin.mjs +366 -41
  2. package/bundle/arcubase.mjs +366 -41
  3. package/dist/bin/arcubase-admin.js +0 -0
  4. package/dist/bin/arcubase.js +0 -0
  5. package/dist/runtime/errors.d.ts +1 -0
  6. package/dist/runtime/errors.d.ts.map +1 -1
  7. package/dist/runtime/execute.d.ts.map +1 -1
  8. package/dist/runtime/execute.js +209 -6
  9. package/dist/runtime/upload.d.ts +13 -0
  10. package/dist/runtime/upload.d.ts.map +1 -0
  11. package/dist/runtime/upload.js +148 -0
  12. package/dist/runtime/zod_registry.d.ts.map +1 -1
  13. package/dist/runtime/zod_registry.js +27 -24
  14. package/dist/tests/execute_validation.test.js +153 -0
  15. package/dist/tests/help.test.js +18 -2
  16. package/dist/tests/upload.test.d.ts +2 -0
  17. package/dist/tests/upload.test.d.ts.map +1 -0
  18. package/dist/tests/upload.test.js +107 -0
  19. package/package.json +1 -1
  20. package/sdk-dist/docs/runtime-reference/README.md +11 -1
  21. package/sdk-dist/docs/runtime-reference/entity-schema/file.md +9 -0
  22. package/sdk-dist/docs/runtime-reference/entity-schema/image.md +9 -0
  23. package/sdk-dist/docs/runtime-reference/examples/oms-01/README.md +19 -0
  24. package/sdk-dist/docs/runtime-reference/row-crud.md +2 -0
  25. package/sdk-dist/docs/runtime-reference/table-lifecycle.md +12 -0
  26. package/sdk-dist/docs/runtime-reference/uploads.md +106 -0
  27. package/src/runtime/errors.ts +1 -0
  28. package/src/runtime/execute.ts +248 -6
  29. package/src/runtime/upload.ts +196 -0
  30. package/src/runtime/zod_registry.ts +27 -25
  31. package/src/tests/execute_validation.test.ts +183 -0
  32. package/src/tests/help.test.ts +23 -2
  33. package/src/tests/upload.test.ts +133 -0
@@ -328,6 +328,159 @@ test('selection action rejects selectAll-only condition for archive action', asy
328
328
  return true;
329
329
  });
330
330
  });
331
+ test('insert entity rejects local file path for file field and points to arcubase upload', async () => {
332
+ const bodyFile = tempJSONFile({
333
+ fields: {
334
+ 1001: 'Lead A',
335
+ 1007: './contract.pdf',
336
+ },
337
+ });
338
+ const calls = [];
339
+ await assert.rejects(async () => {
340
+ await executeCLI('user', ['workflow', 'insert-entity', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile], env, async (url, init) => {
341
+ calls.push({ url: String(url), method: init?.method });
342
+ if (String(url) === 'https://arcubase.example.com/entity/app_1/entity_1' && init?.method === 'GET') {
343
+ return new Response(JSON.stringify({
344
+ data: {
345
+ id: 1,
346
+ app_id: 1,
347
+ fields: [
348
+ { id: 1001, type: 'text', label: 'Lead Name' },
349
+ { id: 1007, type: 'file', label: 'Contract File' },
350
+ ],
351
+ },
352
+ }), { status: 200 });
353
+ }
354
+ return new Response('{}', { status: 200 });
355
+ });
356
+ }, (error) => {
357
+ assert.ok(error instanceof CLIError);
358
+ assert.equal(error.code, 'BODY_VALIDATION_FAILED');
359
+ assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.fields.1007'));
360
+ assert.match(error.message, /arcubase upload \.\/contract\.pdf/);
361
+ assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/uploads.md')));
362
+ return true;
363
+ });
364
+ assert.deepEqual(calls, [
365
+ { url: 'https://arcubase.example.com/entity/app_1/entity_1', method: 'GET' },
366
+ ]);
367
+ });
368
+ test('update row rejects object value for image field and requires upload array', async () => {
369
+ const bodyFile = tempJSONFile({
370
+ changed_fields: [1008],
371
+ data: {
372
+ 1008: {
373
+ upload_id: 123456,
374
+ file: 'lead-photo.jpg',
375
+ file_name: 'lead-photo.jpg',
376
+ meta: {},
377
+ },
378
+ },
379
+ });
380
+ await assert.rejects(async () => {
381
+ await executeCLI('user', ['workflow', 'update-entity-row', '--app-id', 'app_1', '--entity-id', 'entity_1', '--row-id', 'row_1', '--body-file', bodyFile], env, async (url, init) => {
382
+ if (String(url) === 'https://arcubase.example.com/entity/app_1/entity_1' && init?.method === 'GET') {
383
+ return new Response(JSON.stringify({
384
+ data: {
385
+ id: 1,
386
+ app_id: 1,
387
+ fields: [
388
+ { id: 1008, type: 'image', label: 'Lead Photo' },
389
+ ],
390
+ },
391
+ }), { status: 200 });
392
+ }
393
+ return new Response('{}', { status: 200 });
394
+ });
395
+ }, (error) => {
396
+ assert.ok(error instanceof CLIError);
397
+ assert.equal(error.code, 'BODY_VALIDATION_FAILED');
398
+ assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.data.1008'));
399
+ assert.match(error.message, /must be an array/);
400
+ assert.match(error.message, /arcubase upload <local-file>/);
401
+ return true;
402
+ });
403
+ });
404
+ test('insert entity accepts upload array for file field after schema preflight', async () => {
405
+ const bodyFile = tempJSONFile({
406
+ fields: {
407
+ 1001: 'Lead A',
408
+ 1007: [
409
+ {
410
+ upload_id: 123456,
411
+ file: 'contract.pdf',
412
+ file_name: 'contract.pdf',
413
+ meta: {},
414
+ },
415
+ ],
416
+ },
417
+ });
418
+ const calls = [];
419
+ const out = await executeCLI('user', ['workflow', 'insert-entity', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile], env, async (url, init) => {
420
+ calls.push({ url: String(url), method: init?.method });
421
+ if (String(url) === 'https://arcubase.example.com/entity/app_1/entity_1' && init?.method === 'GET') {
422
+ return new Response(JSON.stringify({
423
+ data: {
424
+ id: 1,
425
+ app_id: 1,
426
+ fields: [
427
+ { id: 1001, type: 'text', label: 'Lead Name' },
428
+ { id: 1007, type: 'file', label: 'Contract File' },
429
+ ],
430
+ },
431
+ }), { status: 200 });
432
+ }
433
+ return new Response(String(init?.body ?? '{}'), { status: 200 });
434
+ });
435
+ assert.equal(out.kind, 'result');
436
+ assert.deepEqual(calls, [
437
+ { url: 'https://arcubase.example.com/entity/app_1/entity_1', method: 'GET' },
438
+ { url: 'https://arcubase.example.com/entity/app_1/entity_1/insert', method: 'POST' },
439
+ ]);
440
+ assert.equal(out.data.fields['1007'][0].upload_id, 123456);
441
+ });
442
+ test('insert entity decorates upload bind failures with actionable guidance', async () => {
443
+ const bodyFile = tempJSONFile({
444
+ fields: {
445
+ 1001: 'Lead A',
446
+ 1007: [
447
+ {
448
+ upload_id: 123456,
449
+ file: 'contract.pdf',
450
+ file_name: 'contract.pdf',
451
+ meta: {},
452
+ },
453
+ ],
454
+ },
455
+ });
456
+ await assert.rejects(async () => {
457
+ await executeCLI('user', ['workflow', 'insert-entity', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile], env, async (url, init) => {
458
+ if (String(url) === 'https://arcubase.example.com/entity/app_1/entity_1' && init?.method === 'GET') {
459
+ return new Response(JSON.stringify({
460
+ data: {
461
+ fields: [{ id: 1007, type: 'file', label: 'Contract File' }],
462
+ },
463
+ }), { status: 200 });
464
+ }
465
+ return new Response(JSON.stringify({
466
+ error: {
467
+ 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',
468
+ type: 'system',
469
+ key: 'error.system',
470
+ },
471
+ trace_id: 'req_upload_bind',
472
+ }), { status: 200 });
473
+ });
474
+ }, (error) => {
475
+ assert.ok(error instanceof CLIError);
476
+ assert.equal(error.code, 'UPSTREAM_BODY_ERROR');
477
+ assert.equal(error.details?.traceId, 'req_upload_bind');
478
+ 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');
479
+ assert.match(error.details?.hint ?? '', /rerun arcubase upload/);
480
+ assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/uploads.md')));
481
+ return true;
482
+ });
483
+ });
331
484
  test('selection action rejects condition selection without selectAll', async () => {
332
485
  const bodyFile = tempJSONFile({
333
486
  selection: {
@@ -46,7 +46,7 @@ test('command help prints runtime doc hints for request body commands', async ()
46
46
  assert.equal(out.kind, 'help');
47
47
  assert.match(out.text, /docs:/);
48
48
  assert.match(out.text, /\/opt\/arcubase-sdk\/docs\/runtime-reference\/row-crud\.md/);
49
- assert.match(out.text, /\/opt\/arcubase-sdk\/docs\/runtime-reference\/examples\/oms-01\/README\.md/);
49
+ assert.match(out.text, /\/opt\/arcubase-sdk\/docs\/runtime-reference\/examples\/README\.md/);
50
50
  });
51
51
  test('command help prints query-file support', async () => {
52
52
  const out = await executeCLI('user', ['workflow', 'query-entity', '--help'], undefined, async () => new Response('{}'));
@@ -57,5 +57,21 @@ test('command help without request body can still point to lifecycle and example
57
57
  const out = await executeCLI('admin', ['entity', 'admin-get-entity-info', '--help'], undefined, async () => new Response('{}'));
58
58
  assert.equal(out.kind, 'help');
59
59
  assert.match(out.text, /\/opt\/arcubase-sdk\/docs\/runtime-reference\/table-lifecycle\.md/);
60
- assert.match(out.text, /\/opt\/arcubase-sdk\/docs\/runtime-reference\/examples\/oms-01\/README\.md/);
60
+ assert.match(out.text, /\/opt\/arcubase-sdk\/docs\/runtime-reference\/examples\/README\.md/);
61
+ });
62
+ test('admin-save-entity help prints row-operation next step', async () => {
63
+ const out = await executeCLI('admin', ['entity', 'admin-save-entity', '--help'], undefined, async () => new Response('{}'));
64
+ assert.equal(out.kind, 'help');
65
+ assert.match(out.text, /switch to arcubase for row operations/);
66
+ assert.match(out.text, /arcubase workflow insert-entity/);
67
+ assert.match(out.text, /arcubase workflow query-entity/);
68
+ });
69
+ test('wrong admin workflow command points to arcubase row command', async () => {
70
+ await assert.rejects(() => executeCLI('admin', ['workflow', 'insert-entity'], env, async () => new Response('{}')), (error) => {
71
+ assert.equal(error.code, 'UNKNOWN_COMMAND');
72
+ assert.equal(error.details?.reason, 'workflow insert-entity is a row operation, not an arcubase-admin command');
73
+ assert.equal(error.details?.hint, 'use: arcubase workflow insert-entity');
74
+ assert.ok((error.details?.suggestions ?? []).includes('arcubase workflow insert-entity'));
75
+ return true;
76
+ });
61
77
  });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=upload.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upload.test.d.ts","sourceRoot":"","sources":["../../src/tests/upload.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,107 @@
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 { executeCLI } from '../runtime/execute.js';
7
+ const env = {
8
+ ARCUBASE_BASE_URL: 'https://arcubase.example.com',
9
+ ARCUBASE_TENANT_ID: 'tenant_1',
10
+ ARCUBASE_ACCESS_TOKEN: 'tok',
11
+ ARCUBASE_TRACE_ID: '',
12
+ ARCUBASE_SUBJECT_TYPE: '',
13
+ ARCUBASE_SUBJECT_ID: '',
14
+ ARCUBASE_TOKEN_EXPIRES_AT: '',
15
+ };
16
+ test('upload help works without runtime env', async () => {
17
+ const out = await executeCLI('user', ['upload', '--help'], undefined, async () => new Response('{}'));
18
+ assert.equal(out.kind, 'help');
19
+ assert.match(out.text, /arcubase upload <local-file>/);
20
+ assert.match(out.text, /uploads\.md/);
21
+ });
22
+ test('upload returns file field value array for row payloads', async () => {
23
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'arcubase-upload-'));
24
+ const sourceFile = path.join(tempDir, 'contract.pdf');
25
+ fs.writeFileSync(sourceFile, 'fake pdf bytes');
26
+ const calls = [];
27
+ const result = await executeCLI('user', ['upload', sourceFile], env, async (input, init) => {
28
+ const url = String(input);
29
+ calls.push({ url, method: init?.method });
30
+ if (url === 'https://arcubase.example.com/upload/token') {
31
+ return new Response(JSON.stringify({
32
+ data: {
33
+ id: 123456,
34
+ mode: 'aliyun-oss',
35
+ token_data: {
36
+ dir: 'upload/123456/',
37
+ policy: 'policy',
38
+ accessid: 'accessid',
39
+ signature: 'signature',
40
+ host: 'https://uploads.example.com',
41
+ },
42
+ },
43
+ }), { status: 200, headers: { 'Content-Type': 'application/json' } });
44
+ }
45
+ if (url === 'https://uploads.example.com') {
46
+ return new Response('', { status: 200 });
47
+ }
48
+ throw new Error(`unexpected fetch url: ${url}`);
49
+ });
50
+ assert.equal(result.kind, 'result');
51
+ assert.deepEqual(calls, [
52
+ { url: 'https://arcubase.example.com/upload/token', method: 'POST' },
53
+ { url: 'https://uploads.example.com', method: 'POST' },
54
+ ]);
55
+ assert.deepEqual(result.data, [
56
+ {
57
+ upload_id: 123456,
58
+ file: 'contract.pdf',
59
+ file_name: 'contract.pdf',
60
+ meta: {},
61
+ },
62
+ ]);
63
+ });
64
+ test('upload supports s3-post-policy tokens', async () => {
65
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'arcubase-upload-s3-'));
66
+ const sourceFile = path.join(tempDir, 'contract.pdf');
67
+ fs.writeFileSync(sourceFile, 'fake pdf bytes');
68
+ const calls = [];
69
+ const result = await executeCLI('user', ['upload', sourceFile], env, async (input, init) => {
70
+ const url = String(input);
71
+ calls.push({ url, method: init?.method });
72
+ if (url === 'https://arcubase.example.com/upload/token') {
73
+ return new Response(JSON.stringify({
74
+ data: {
75
+ id: 654321,
76
+ mode: 's3-post-policy',
77
+ token_data: {
78
+ dir: 'upload/654321/',
79
+ policy: 'policy',
80
+ host: 'https://uploads.example.com/private-bucket',
81
+ x_amz_algorithm: 'AWS4-HMAC-SHA256',
82
+ x_amz_credential: 'test/20260504/us-east-1/s3/aws4_request',
83
+ x_amz_date: '20260504T010203Z',
84
+ x_amz_signature: 'abc123',
85
+ },
86
+ },
87
+ }), { status: 200, headers: { 'Content-Type': 'application/json' } });
88
+ }
89
+ if (url === 'https://uploads.example.com/private-bucket') {
90
+ return new Response('', { status: 200 });
91
+ }
92
+ throw new Error(`unexpected fetch url: ${url}`);
93
+ });
94
+ assert.equal(result.kind, 'result');
95
+ assert.deepEqual(calls, [
96
+ { url: 'https://arcubase.example.com/upload/token', method: 'POST' },
97
+ { url: 'https://uploads.example.com/private-bucket', method: 'POST' },
98
+ ]);
99
+ assert.deepEqual(result.data, [
100
+ {
101
+ upload_id: 654321,
102
+ file: 'contract.pdf',
103
+ file_name: 'contract.pdf',
104
+ meta: {},
105
+ },
106
+ ]);
107
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carthooks/arcubase-cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Arcubase runtime CLI for admin and user command execution",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -64,6 +64,13 @@ arcubase entity update-entity-bulk --app-id <app_id> --entity-id <entity_id> --b
64
64
  arcubase entity invoke-entity-batch-operator --app-id <app_id> --entity-id <entity_id> --body-file invoke-batch-operator.json
65
65
  ```
66
66
 
67
+ Hard rule:
68
+
69
+ - `arcubase-admin` stops at schema save
70
+ - all row operations must switch to `arcubase`
71
+ - do not use `arcubase-admin workflow insert-entity`
72
+ - do not use `arcubase-admin workflow query-entity`
73
+
67
74
  ## Required file inputs
68
75
 
69
76
  Most write commands use:
@@ -141,7 +148,8 @@ If you need to create a table:
141
148
  If you need row CRUD:
142
149
 
143
150
  1. read `row-crud.md`
144
- 2. if a field is complex, open the exact field-type page
151
+ 2. if the row contains a file or image field, read `uploads.md`
152
+ 3. if a field is complex, open the exact field-type page
145
153
 
146
154
  If you need a business-shaped template:
147
155
 
@@ -166,6 +174,8 @@ If you need search, selection, tags, bulk update, or batch operator:
166
174
  - one field cheatsheet per file
167
175
  - `row-crud.md`
168
176
  - insert, query, update, delete, restore
177
+ - `uploads.md`
178
+ - upload one local file and reuse the returned field value in row payloads
169
179
  - `search-and-bulk-actions.md`
170
180
  - search, selection, tags, bulk update, batch operator
171
181
  - `examples/`
@@ -42,3 +42,12 @@ File upload field.
42
42
  - `count_range: false`
43
43
  - `count_min: 0`
44
44
  - `count_max: 9`
45
+
46
+ ## Row write rule
47
+
48
+ When inserting or updating row data for a `file` field:
49
+
50
+ - do not construct upload payload objects by hand
51
+ - run `arcubase upload <local-file>` first
52
+ - use the returned `data` array directly as the row field value
53
+ - read `../uploads.md`
@@ -41,3 +41,12 @@ Image upload field.
41
41
  - `count_range: false`
42
42
  - `count_min: 0`
43
43
  - `count_max: 9`
44
+
45
+ ## Row write rule
46
+
47
+ When inserting or updating row data for an `image` field:
48
+
49
+ - do not construct upload payload objects by hand
50
+ - run `arcubase upload <local-file>` first
51
+ - use the returned `data` array directly as the row field value
52
+ - read `../uploads.md`
@@ -85,6 +85,25 @@ For every `*.schema.json` file:
85
85
 
86
86
  These schema files are templates, not live shell exports.
87
87
 
88
+ ## CLI phases
89
+
90
+ Phase 1 uses `arcubase-admin`:
91
+
92
+ - create the app
93
+ - create entity shells
94
+ - fetch shells
95
+ - save schemas
96
+
97
+ Phase 2 uses `arcubase`:
98
+
99
+ - insert rows
100
+ - query rows
101
+ - update rows
102
+ - delete or restore rows
103
+ - search, selection, and bulk actions
104
+
105
+ Do not use `arcubase-admin` for row operations.
106
+
88
107
  ## Minimal command path
89
108
 
90
109
  Create the app:
@@ -29,6 +29,7 @@ Rules:
29
29
 
30
30
  - use numeric field ids as JSON object keys
31
31
  - values must match the saved schema
32
+ - if a field is `file` or `image`, run `arcubase upload <local-file>` first and use the returned `data` array as the field value
32
33
 
33
34
  Success result:
34
35
 
@@ -121,6 +122,7 @@ Rules:
121
122
  - `changed_fields` is required
122
123
  - every id in `changed_fields` must also appear in `data`
123
124
  - use numeric field ids as JSON object keys
125
+ - if a field is `file` or `image`, run `arcubase upload <local-file>` first and use the returned `data` array as the field value
124
126
 
125
127
  Success result:
126
128
 
@@ -17,6 +17,13 @@ The successful path is command-first:
17
17
 
18
18
  Do not stop at shell creation.
19
19
 
20
+ CLI phase split:
21
+
22
+ - schema phase uses `arcubase-admin`
23
+ - row phase uses `arcubase`
24
+ - the switch happens immediately after `admin-save-entity`
25
+ - do not use `arcubase-admin` for row insert or row query
26
+
20
27
  ## Step 1: create the app
21
28
 
22
29
  Command:
@@ -97,6 +104,11 @@ Read before writing `save-entity.json`:
97
104
  2. `entity-schema/README.md`
98
105
  3. the exact field-type pages you need
99
106
 
107
+ After `admin-save-entity`, switch binaries:
108
+
109
+ - use `arcubase workflow insert-entity` for row insertion
110
+ - use `arcubase workflow query-entity` for readiness and row reads
111
+
100
112
  ## Step 5: probe readiness
101
113
 
102
114
  Command:
@@ -0,0 +1,106 @@
1
+ # Uploads
2
+
3
+ Use this page when a row contains a `file` or `image` field.
4
+
5
+ The successful path is:
6
+
7
+ 1. run `arcubase upload <local-file>`
8
+ 2. use the returned JSON array directly as the field value
9
+ 3. insert or update the row with `arcubase workflow insert-entity` or `arcubase workflow update-entity-row`
10
+
11
+ Do not construct `upload_id`, `assets_id`, or OSS form fields by hand.
12
+
13
+ ## Upload one local file
14
+
15
+ Command:
16
+
17
+ ```bash
18
+ arcubase upload ./contract.pdf
19
+ ```
20
+
21
+ Optional flags:
22
+
23
+ - `--filename <name>`
24
+ - `--global`
25
+
26
+ Example:
27
+
28
+ ```bash
29
+ arcubase upload ./lead-photo.jpg --filename lead-photo.jpg
30
+ ```
31
+
32
+ ## Upload result shape
33
+
34
+ The command returns a JSON array.
35
+
36
+ Example:
37
+
38
+ ```json
39
+ {
40
+ "data": [
41
+ {
42
+ "upload_id": 123456,
43
+ "file": "contract.pdf",
44
+ "file_name": "contract.pdf",
45
+ "meta": {}
46
+ }
47
+ ]
48
+ }
49
+ ```
50
+
51
+ Use the `data` array directly as the field value for a `file` or `image` field.
52
+
53
+ ## Insert a row with a file field
54
+
55
+ ```json
56
+ {
57
+ "fields": {
58
+ "1001": "Lead A",
59
+ "1007": [
60
+ {
61
+ "upload_id": 123456,
62
+ "file": "contract.pdf",
63
+ "file_name": "contract.pdf",
64
+ "meta": {}
65
+ }
66
+ ]
67
+ }
68
+ }
69
+ ```
70
+
71
+ ## Update a row with an image field
72
+
73
+ ```json
74
+ {
75
+ "data": {
76
+ "1008": [
77
+ {
78
+ "upload_id": 123457,
79
+ "file": "lead-photo.jpg",
80
+ "file_name": "lead-photo.jpg",
81
+ "meta": {}
82
+ }
83
+ ]
84
+ },
85
+ "changed_fields": [1008]
86
+ }
87
+ ```
88
+
89
+ ## Preview an uploaded file
90
+
91
+ Command:
92
+
93
+ ```bash
94
+ arcubase common upload-preview --body-file preview.json
95
+ ```
96
+
97
+ Example `preview.json`:
98
+
99
+ ```json
100
+ {
101
+ "upload_id": 123456,
102
+ "fname": "contract.pdf"
103
+ }
104
+ ```
105
+
106
+ This returns a preview URL for the upload token entry. It does not insert or update a row.
@@ -20,6 +20,7 @@ export type CLIErrorDetails = {
20
20
  tsHints?: CLITSTypeHint[]
21
21
  docHints?: CLIDocHint[]
22
22
  reason?: string
23
+ hint?: string
23
24
  endpoint?: string
24
25
  method?: string
25
26
  traceId?: string