@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.
- package/bundle/arcubase-admin.mjs +366 -41
- package/bundle/arcubase.mjs +366 -41
- package/dist/bin/arcubase-admin.js +0 -0
- package/dist/bin/arcubase.js +0 -0
- package/dist/runtime/errors.d.ts +1 -0
- package/dist/runtime/errors.d.ts.map +1 -1
- package/dist/runtime/execute.d.ts.map +1 -1
- package/dist/runtime/execute.js +209 -6
- 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.map +1 -1
- package/dist/runtime/zod_registry.js +27 -24
- package/dist/tests/execute_validation.test.js +153 -0
- package/dist/tests/help.test.js +18 -2
- 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 +11 -1
- package/sdk-dist/docs/runtime-reference/entity-schema/file.md +9 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/image.md +9 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/README.md +19 -0
- package/sdk-dist/docs/runtime-reference/row-crud.md +2 -0
- package/sdk-dist/docs/runtime-reference/table-lifecycle.md +12 -0
- package/sdk-dist/docs/runtime-reference/uploads.md +106 -0
- package/src/runtime/errors.ts +1 -0
- package/src/runtime/execute.ts +248 -6
- package/src/runtime/upload.ts +196 -0
- package/src/runtime/zod_registry.ts +27 -25
- package/src/tests/execute_validation.test.ts +183 -0
- package/src/tests/help.test.ts +23 -2
- 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: {
|
package/dist/tests/help.test.js
CHANGED
|
@@ -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\/
|
|
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\/
|
|
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 @@
|
|
|
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
|
@@ -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
|
|
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.
|