@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
@@ -429,6 +429,189 @@ test('selection action rejects selectAll-only condition for archive action', asy
429
429
  })
430
430
  })
431
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
+
432
615
  test('selection action rejects condition selection without selectAll', async () => {
433
616
  const bodyFile = tempJSONFile({
434
617
  selection: {
@@ -54,7 +54,7 @@ test('command help prints runtime doc hints for request body commands', async ()
54
54
  assert.equal(out.kind, 'help')
55
55
  assert.match(out.text, /docs:/)
56
56
  assert.match(out.text, /\/opt\/arcubase-sdk\/docs\/runtime-reference\/row-crud\.md/)
57
- assert.match(out.text, /\/opt\/arcubase-sdk\/docs\/runtime-reference\/examples\/oms-01\/README\.md/)
57
+ assert.match(out.text, /\/opt\/arcubase-sdk\/docs\/runtime-reference\/examples\/README\.md/)
58
58
  })
59
59
 
60
60
  test('command help prints query-file support', async () => {
@@ -67,5 +67,26 @@ test('command help without request body can still point to lifecycle and example
67
67
  const out = await executeCLI('admin', ['entity', 'admin-get-entity-info', '--help'], undefined, async () => new Response('{}'))
68
68
  assert.equal(out.kind, 'help')
69
69
  assert.match(out.text, /\/opt\/arcubase-sdk\/docs\/runtime-reference\/table-lifecycle\.md/)
70
- assert.match(out.text, /\/opt\/arcubase-sdk\/docs\/runtime-reference\/examples\/oms-01\/README\.md/)
70
+ assert.match(out.text, /\/opt\/arcubase-sdk\/docs\/runtime-reference\/examples\/README\.md/)
71
+ })
72
+
73
+ test('admin-save-entity help prints row-operation next step', async () => {
74
+ const out = await executeCLI('admin', ['entity', 'admin-save-entity', '--help'], undefined, async () => new Response('{}'))
75
+ assert.equal(out.kind, 'help')
76
+ assert.match(out.text, /switch to arcubase for row operations/)
77
+ assert.match(out.text, /arcubase workflow insert-entity/)
78
+ assert.match(out.text, /arcubase workflow query-entity/)
79
+ })
80
+
81
+ test('wrong admin workflow command points to arcubase row command', async () => {
82
+ await assert.rejects(
83
+ () => executeCLI('admin', ['workflow', 'insert-entity'], env as any, async () => new Response('{}')),
84
+ (error: any) => {
85
+ assert.equal(error.code, 'UNKNOWN_COMMAND')
86
+ assert.equal(error.details?.reason, 'workflow insert-entity is a row operation, not an arcubase-admin command')
87
+ assert.equal(error.details?.hint, 'use: arcubase workflow insert-entity')
88
+ assert.ok((error.details?.suggestions ?? []).includes('arcubase workflow insert-entity'))
89
+ return true
90
+ }
91
+ )
71
92
  })
@@ -0,0 +1,133 @@
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
+
8
+ const env = {
9
+ ARCUBASE_BASE_URL: 'https://arcubase.example.com',
10
+ ARCUBASE_TENANT_ID: 'tenant_1',
11
+ ARCUBASE_ACCESS_TOKEN: 'tok',
12
+ ARCUBASE_TRACE_ID: '',
13
+ ARCUBASE_SUBJECT_TYPE: '',
14
+ ARCUBASE_SUBJECT_ID: '',
15
+ ARCUBASE_TOKEN_EXPIRES_AT: '',
16
+ }
17
+
18
+ test('upload help works without runtime env', async () => {
19
+ const out = await executeCLI('user', ['upload', '--help'], undefined, async () => new Response('{}'))
20
+ assert.equal(out.kind, 'help')
21
+ assert.match(out.text, /arcubase upload <local-file>/)
22
+ assert.match(out.text, /uploads\.md/)
23
+ })
24
+
25
+ test('upload returns file field value array for row payloads', async () => {
26
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'arcubase-upload-'))
27
+ const sourceFile = path.join(tempDir, 'contract.pdf')
28
+ fs.writeFileSync(sourceFile, 'fake pdf bytes')
29
+
30
+ const calls: Array<{ url: string; method?: string }> = []
31
+
32
+ const result = await executeCLI(
33
+ 'user',
34
+ ['upload', sourceFile],
35
+ env as any,
36
+ async (input, init) => {
37
+ const url = String(input)
38
+ calls.push({ url, method: init?.method })
39
+ if (url === 'https://arcubase.example.com/upload/token') {
40
+ return new Response(
41
+ JSON.stringify({
42
+ data: {
43
+ id: 123456,
44
+ mode: 'aliyun-oss',
45
+ token_data: {
46
+ dir: 'upload/123456/',
47
+ policy: 'policy',
48
+ accessid: 'accessid',
49
+ signature: 'signature',
50
+ host: 'https://uploads.example.com',
51
+ },
52
+ },
53
+ }),
54
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
55
+ )
56
+ }
57
+ if (url === 'https://uploads.example.com') {
58
+ return new Response('', { status: 200 })
59
+ }
60
+ throw new Error(`unexpected fetch url: ${url}`)
61
+ },
62
+ )
63
+
64
+ assert.equal(result.kind, 'result')
65
+ assert.deepEqual(calls, [
66
+ { url: 'https://arcubase.example.com/upload/token', method: 'POST' },
67
+ { url: 'https://uploads.example.com', method: 'POST' },
68
+ ])
69
+ assert.deepEqual(result.data, [
70
+ {
71
+ upload_id: 123456,
72
+ file: 'contract.pdf',
73
+ file_name: 'contract.pdf',
74
+ meta: {},
75
+ },
76
+ ])
77
+ })
78
+
79
+ test('upload supports s3-post-policy tokens', async () => {
80
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'arcubase-upload-s3-'))
81
+ const sourceFile = path.join(tempDir, 'contract.pdf')
82
+ fs.writeFileSync(sourceFile, 'fake pdf bytes')
83
+
84
+ const calls: Array<{ url: string; method?: string }> = []
85
+
86
+ const result = await executeCLI(
87
+ 'user',
88
+ ['upload', sourceFile],
89
+ env as any,
90
+ async (input, init) => {
91
+ const url = String(input)
92
+ calls.push({ url, method: init?.method })
93
+ if (url === 'https://arcubase.example.com/upload/token') {
94
+ return new Response(
95
+ JSON.stringify({
96
+ data: {
97
+ id: 654321,
98
+ mode: 's3-post-policy',
99
+ token_data: {
100
+ dir: 'upload/654321/',
101
+ policy: 'policy',
102
+ host: 'https://uploads.example.com/private-bucket',
103
+ x_amz_algorithm: 'AWS4-HMAC-SHA256',
104
+ x_amz_credential: 'test/20260504/us-east-1/s3/aws4_request',
105
+ x_amz_date: '20260504T010203Z',
106
+ x_amz_signature: 'abc123',
107
+ },
108
+ },
109
+ }),
110
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
111
+ )
112
+ }
113
+ if (url === 'https://uploads.example.com/private-bucket') {
114
+ return new Response('', { status: 200 })
115
+ }
116
+ throw new Error(`unexpected fetch url: ${url}`)
117
+ },
118
+ )
119
+
120
+ assert.equal(result.kind, 'result')
121
+ assert.deepEqual(calls, [
122
+ { url: 'https://arcubase.example.com/upload/token', method: 'POST' },
123
+ { url: 'https://uploads.example.com/private-bucket', method: 'POST' },
124
+ ])
125
+ assert.deepEqual(result.data, [
126
+ {
127
+ upload_id: 654321,
128
+ file: 'contract.pdf',
129
+ file_name: 'contract.pdf',
130
+ meta: {},
131
+ },
132
+ ])
133
+ })