@carthooks/arcubase-cli 0.1.20 → 0.1.22
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 +1669 -163
- package/bundle/arcubase.mjs +1669 -163
- package/dist/generated/command_registry.generated.d.ts +300 -0
- package/dist/generated/command_registry.generated.d.ts.map +1 -1
- package/dist/generated/command_registry.generated.js +416 -0
- package/dist/generated/help_examples.generated.d.ts +191 -0
- package/dist/generated/help_examples.generated.d.ts.map +1 -1
- package/dist/generated/help_examples.generated.js +277 -0
- package/dist/generated/type_index.generated.d.ts +44 -1
- package/dist/generated/type_index.generated.d.ts.map +1 -1
- package/dist/generated/type_index.generated.js +46 -1
- package/dist/generated/zod_registry.generated.d.ts +32 -1
- package/dist/generated/zod_registry.generated.d.ts.map +1 -1
- package/dist/generated/zod_registry.generated.js +41 -0
- package/dist/runtime/dev_sdk_gen.d.ts +9 -0
- package/dist/runtime/dev_sdk_gen.d.ts.map +1 -0
- package/dist/runtime/dev_sdk_gen.js +319 -0
- package/dist/runtime/execute.d.ts.map +1 -1
- package/dist/runtime/execute.js +384 -32
- package/dist/runtime/zod_registry.d.ts.map +1 -1
- package/dist/runtime/zod_registry.js +66 -0
- package/package.json +1 -1
- package/sdk-dist/api/admin/index.ts +1 -0
- package/sdk-dist/api/admin/ingress.ts +11 -0
- package/sdk-dist/api/admin/public-link.ts +48 -0
- package/sdk-dist/api/shared-link/form.ts +42 -1
- package/sdk-dist/api/user/index.ts +0 -1
- package/sdk-dist/docs/runtime-reference/access-rule.md +19 -0
- package/sdk-dist/docs/runtime-reference/dashboard.md +15 -0
- package/sdk-dist/docs/runtime-reference/dev-sdk-gen.md +41 -0
- package/sdk-dist/docs/runtime-reference/help-examples/admin/access-rule/bulk-apply.json +105 -0
- package/sdk-dist/docs/runtime-reference/help-examples/admin/dashboard/update-layout.json +16 -0
- package/sdk-dist/docs/runtime-reference/help-examples/admin/widget/create.json +33 -0
- package/sdk-dist/docs/runtime-reference/help-examples/admin/widget/preview-data.json +42 -0
- package/sdk-dist/docs/runtime-reference/widgets.md +40 -0
- package/sdk-dist/generated/command_registry.generated.ts +416 -0
- package/sdk-dist/generated/help_examples.generated.ts +277 -0
- package/sdk-dist/generated/type_index.generated.ts +46 -1
- package/sdk-dist/generated/zod_registry.generated.ts +62 -0
- package/sdk-dist/types/app.ts +37 -1
- package/sdk-dist/types/common.ts +77 -35
- package/sdk-dist/types/index.ts +1 -1
- package/sdk-dist/types/ingress.ts +31 -0
- package/sdk-dist/types/public-link.ts +10 -0
- package/sdk-dist/types/shared-link.ts +16 -1
- package/sdk-dist/types/user-action.ts +1 -43
- package/src/generated/command_registry.generated.ts +416 -0
- package/src/generated/help_examples.generated.ts +277 -0
- package/src/generated/type_index.generated.ts +46 -1
- package/src/generated/zod_registry.generated.ts +62 -0
- package/src/runtime/dev_sdk_gen.ts +360 -0
- package/src/runtime/execute.ts +484 -52
- package/src/runtime/zod_registry.ts +66 -0
- package/src/tests/command_registry.test.ts +21 -2
- package/src/tests/dev_sdk_gen.test.ts +226 -0
- package/src/tests/docs_readability.test.ts +15 -0
- package/src/tests/execute_validation.test.ts +215 -0
- package/src/tests/help.test.ts +83 -0
- package/sdk-dist/api/user/copilot.ts +0 -20
- package/sdk-dist/types/copilot.ts +0 -34
|
@@ -89,12 +89,32 @@ const conditionDocHint: RuntimeDocHint = {
|
|
|
89
89
|
title: 'Condition',
|
|
90
90
|
file: 'docs/runtime-reference/condition.md',
|
|
91
91
|
}
|
|
92
|
+
const dashboardDocHint: RuntimeDocHint = {
|
|
93
|
+
title: 'Dashboard',
|
|
94
|
+
file: 'docs/runtime-reference/dashboard.md',
|
|
95
|
+
}
|
|
96
|
+
const widgetsDocHint: RuntimeDocHint = {
|
|
97
|
+
title: 'Widgets',
|
|
98
|
+
file: 'docs/runtime-reference/widgets.md',
|
|
99
|
+
}
|
|
92
100
|
|
|
93
101
|
const docHintIndex: Record<string, RuntimeDocHint[]> = {
|
|
94
102
|
AppCreateByTenantsReqVO: [
|
|
95
103
|
{ title: 'Table lifecycle', file: 'docs/runtime-reference/table-lifecycle.md' },
|
|
96
104
|
examplesIndexDocHint,
|
|
97
105
|
],
|
|
106
|
+
AppNewWidgetsReqVO: [
|
|
107
|
+
widgetsDocHint,
|
|
108
|
+
],
|
|
109
|
+
AppPreviewWidgetsDataReqVO: [
|
|
110
|
+
widgetsDocHint,
|
|
111
|
+
],
|
|
112
|
+
AppUpdateWidgetsIngressOptionsReqVO: [
|
|
113
|
+
widgetsDocHint,
|
|
114
|
+
],
|
|
115
|
+
AppUpdateWidgetsReqVO: [
|
|
116
|
+
widgetsDocHint,
|
|
117
|
+
],
|
|
98
118
|
AppCreateEntityReqVO: [
|
|
99
119
|
{ title: 'Table lifecycle', file: 'docs/runtime-reference/table-lifecycle.md' },
|
|
100
120
|
{ title: 'Table schema cheatsheet', file: 'docs/runtime-reference/entity-schema.md' },
|
|
@@ -109,6 +129,23 @@ const docHintIndex: Record<string, RuntimeDocHint[]> = {
|
|
|
109
129
|
{ title: 'Access rule', file: 'docs/runtime-reference/access-rule.md' },
|
|
110
130
|
examplesIndexDocHint,
|
|
111
131
|
],
|
|
132
|
+
AppIngressBulkApplyReqVO: [
|
|
133
|
+
{ title: 'Access rule', file: 'docs/runtime-reference/access-rule.md' },
|
|
134
|
+
examplesIndexDocHint,
|
|
135
|
+
],
|
|
136
|
+
DashboardCreateReqVO: [
|
|
137
|
+
dashboardDocHint,
|
|
138
|
+
],
|
|
139
|
+
DashboardUpdateLayoutReqVO: [
|
|
140
|
+
dashboardDocHint,
|
|
141
|
+
widgetsDocHint,
|
|
142
|
+
],
|
|
143
|
+
DashboardUpdateOptionsReqVO: [
|
|
144
|
+
dashboardDocHint,
|
|
145
|
+
],
|
|
146
|
+
DashboardUpdateTitleReqVO: [
|
|
147
|
+
dashboardDocHint,
|
|
148
|
+
],
|
|
112
149
|
EntitySaveReqVO: [
|
|
113
150
|
{ title: 'Table schema cheatsheet', file: 'docs/runtime-reference/entity-schema.md' },
|
|
114
151
|
{ title: 'Field type index', file: 'docs/runtime-reference/entity-schema/README.md' },
|
|
@@ -148,6 +185,9 @@ const docHintIndex: Record<string, RuntimeDocHint[]> = {
|
|
|
148
185
|
{ title: 'Search and bulk actions', file: 'docs/runtime-reference/search-and-bulk-actions.md' },
|
|
149
186
|
examplesIndexDocHint,
|
|
150
187
|
],
|
|
188
|
+
FetchWidgetsDataReqVO: [
|
|
189
|
+
widgetsDocHint,
|
|
190
|
+
],
|
|
151
191
|
'EntityInvokeBatchOperatorReqVO & InvokeRequestVO': [
|
|
152
192
|
selectionDocHint,
|
|
153
193
|
conditionDocHint,
|
|
@@ -202,6 +242,21 @@ const commandDocHintIndex: Record<string, RuntimeDocHint[]> = {
|
|
|
202
242
|
{ title: 'Table lifecycle', file: 'docs/runtime-reference/table-lifecycle.md' },
|
|
203
243
|
examplesIndexDocHint,
|
|
204
244
|
],
|
|
245
|
+
'admin.dashboard.getDashboardList': [
|
|
246
|
+
dashboardDocHint,
|
|
247
|
+
],
|
|
248
|
+
'admin.dashboard.getDashboardOptions': [
|
|
249
|
+
dashboardDocHint,
|
|
250
|
+
],
|
|
251
|
+
'admin.widget.widgetsList': [
|
|
252
|
+
widgetsDocHint,
|
|
253
|
+
],
|
|
254
|
+
'admin.widget.getWidgets': [
|
|
255
|
+
widgetsDocHint,
|
|
256
|
+
],
|
|
257
|
+
'admin.widget.deleteWidgets': [
|
|
258
|
+
widgetsDocHint,
|
|
259
|
+
],
|
|
205
260
|
'admin.app.createEntity': [
|
|
206
261
|
{ title: 'Table lifecycle', file: 'docs/runtime-reference/table-lifecycle.md' },
|
|
207
262
|
examplesIndexDocHint,
|
|
@@ -225,6 +280,10 @@ const commandDocHintIndex: Record<string, RuntimeDocHint[]> = {
|
|
|
225
280
|
{ title: 'Access rule', file: 'docs/runtime-reference/access-rule.md' },
|
|
226
281
|
examplesIndexDocHint,
|
|
227
282
|
],
|
|
283
|
+
'admin.access-rule.bulkApplyEntityIngress': [
|
|
284
|
+
{ title: 'Access rule', file: 'docs/runtime-reference/access-rule.md' },
|
|
285
|
+
examplesIndexDocHint,
|
|
286
|
+
],
|
|
228
287
|
'user.row.insertEntity': [
|
|
229
288
|
{ title: 'Row CRUD', file: 'docs/runtime-reference/row-crud.md' },
|
|
230
289
|
{ title: 'Uploads', file: 'docs/runtime-reference/uploads.md' },
|
|
@@ -265,6 +324,13 @@ const commandDocHintIndex: Record<string, RuntimeDocHint[]> = {
|
|
|
265
324
|
{ title: 'Search and bulk actions', file: 'docs/runtime-reference/search-and-bulk-actions.md' },
|
|
266
325
|
examplesIndexDocHint,
|
|
267
326
|
],
|
|
327
|
+
'user.dashboard.getDashboard': [
|
|
328
|
+
dashboardDocHint,
|
|
329
|
+
],
|
|
330
|
+
'user.dashboard.getDashboardWidgets': [
|
|
331
|
+
dashboardDocHint,
|
|
332
|
+
widgetsDocHint,
|
|
333
|
+
],
|
|
268
334
|
}
|
|
269
335
|
|
|
270
336
|
export function getCommandDocHints(operation: string, env: EnvInput = process.env): RuntimeDocHint[] {
|
|
@@ -17,6 +17,12 @@ test('admin surface is restricted to reset white list', () => {
|
|
|
17
17
|
'app list',
|
|
18
18
|
'app update',
|
|
19
19
|
'app watch-import-task',
|
|
20
|
+
'dashboard create',
|
|
21
|
+
'dashboard get-options',
|
|
22
|
+
'dashboard list',
|
|
23
|
+
'dashboard rename',
|
|
24
|
+
'dashboard update-layout',
|
|
25
|
+
'dashboard update-options',
|
|
20
26
|
'table create',
|
|
21
27
|
'table delete',
|
|
22
28
|
'table get',
|
|
@@ -24,11 +30,19 @@ test('admin surface is restricted to reset white list', () => {
|
|
|
24
30
|
'table rename',
|
|
25
31
|
'table update-schema',
|
|
26
32
|
'access-rule assign-users',
|
|
33
|
+
'access-rule bulk-apply',
|
|
27
34
|
'access-rule create',
|
|
28
35
|
'access-rule delete',
|
|
29
36
|
'access-rule get',
|
|
30
37
|
'access-rule list',
|
|
31
38
|
'access-rule update',
|
|
39
|
+
'widget create',
|
|
40
|
+
'widget delete',
|
|
41
|
+
'widget get',
|
|
42
|
+
'widget list',
|
|
43
|
+
'widget preview-data',
|
|
44
|
+
'widget update',
|
|
45
|
+
'widget update-ingress-options',
|
|
32
46
|
'workflow create-version',
|
|
33
47
|
'workflow deploy',
|
|
34
48
|
'workflow get',
|
|
@@ -40,6 +54,8 @@ test('admin surface is restricted to reset white list', () => {
|
|
|
40
54
|
test('user surface is restricted to reset white list', () => {
|
|
41
55
|
const actual = commandPaths(userCommands)
|
|
42
56
|
const expected = [
|
|
57
|
+
'dashboard get',
|
|
58
|
+
'dashboard widgets',
|
|
43
59
|
'entry',
|
|
44
60
|
'profile get',
|
|
45
61
|
'profile update',
|
|
@@ -59,6 +75,7 @@ test('user surface is restricted to reset white list', () => {
|
|
|
59
75
|
'upload mobile',
|
|
60
76
|
'upload preview',
|
|
61
77
|
'upload token',
|
|
78
|
+
'widget data',
|
|
62
79
|
'workflow approve',
|
|
63
80
|
'workflow countersign',
|
|
64
81
|
'workflow forward',
|
|
@@ -75,8 +92,8 @@ test('user surface is restricted to reset white list', () => {
|
|
|
75
92
|
test('generated modules use CLI namespaces only', () => {
|
|
76
93
|
const adminModules = [...new Set(adminCommands.map((item) => item.module))].sort()
|
|
77
94
|
const userModules = [...new Set(userCommands.map((item) => item.module))].sort()
|
|
78
|
-
assert.deepEqual(adminModules, ['access-rule', 'app', 'table', 'workflow'])
|
|
79
|
-
assert.deepEqual(userModules, ['entry', 'profile', 'row', 'table', 'upload', 'workflow'])
|
|
95
|
+
assert.deepEqual(adminModules, ['access-rule', 'app', 'dashboard', 'table', 'widget', 'workflow'])
|
|
96
|
+
assert.deepEqual(userModules, ['dashboard', 'entry', 'profile', 'row', 'table', 'upload', 'widget', 'workflow'])
|
|
80
97
|
})
|
|
81
98
|
|
|
82
99
|
test('black list commands are removed from registry', () => {
|
|
@@ -98,6 +115,8 @@ test('black list commands are removed from registry', () => {
|
|
|
98
115
|
'workflow query-entity-selection',
|
|
99
116
|
'profile change-password',
|
|
100
117
|
'profile auth-channels',
|
|
118
|
+
'dashboard delete',
|
|
119
|
+
'dashboard clone',
|
|
101
120
|
]
|
|
102
121
|
for (const item of forbidden) {
|
|
103
122
|
assert.equal(all.has(item), false, `${item} should not exist`)
|
|
@@ -0,0 +1,226 @@
|
|
|
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
|
+
|
|
7
|
+
import { generateArcubaseProjectSDK } from '../runtime/dev_sdk_gen.js'
|
|
8
|
+
import { executeCLI } from '../runtime/execute.js'
|
|
9
|
+
import { CLIError } from '../runtime/errors.js'
|
|
10
|
+
|
|
11
|
+
const env = {
|
|
12
|
+
ARCUBASE_BASE_URL: 'https://arcubase.example.com',
|
|
13
|
+
ARCUBASE_ACCESS_TOKEN: 'tok',
|
|
14
|
+
ARCUBASE_SDK_ROOT: '/runtime/arcubase-sdk',
|
|
15
|
+
ARCUBASE_TRACE_ID: '',
|
|
16
|
+
ARCUBASE_SUBJECT_TYPE: '',
|
|
17
|
+
ARCUBASE_SUBJECT_ID: '',
|
|
18
|
+
ARCUBASE_TOKEN_EXPIRES_AT: '',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function appDetail() {
|
|
22
|
+
return {
|
|
23
|
+
id: 3882856425,
|
|
24
|
+
name: '故事应用',
|
|
25
|
+
entities: {},
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function entityList() {
|
|
30
|
+
return [
|
|
31
|
+
{
|
|
32
|
+
id: 3883547182,
|
|
33
|
+
name: '编故事',
|
|
34
|
+
},
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function appDetailWithEntityList() {
|
|
39
|
+
return {
|
|
40
|
+
id: 3882856425,
|
|
41
|
+
name: '故事应用',
|
|
42
|
+
entities: [
|
|
43
|
+
{
|
|
44
|
+
id: 3883547182,
|
|
45
|
+
name: '编故事',
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function entityDetail() {
|
|
52
|
+
return {
|
|
53
|
+
id: 3883547182,
|
|
54
|
+
app_id: 3882856425,
|
|
55
|
+
name: '编故事',
|
|
56
|
+
key: 'story',
|
|
57
|
+
fields: [
|
|
58
|
+
{ id: 1006, label: '时代', key: 'era', type: 'select', required: false, options: { options: { items: [{ key: 1, value: '现代' }] } } },
|
|
59
|
+
{ id: 1013, label: '你刚刚', key: 'recentEvent', type: 'text', required: false, options: {} },
|
|
60
|
+
{ id: 1011, label: '故事', key: 'story', type: 'textarea', required: true, options: {} },
|
|
61
|
+
],
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function entityDetailWithoutFieldKeys() {
|
|
66
|
+
const detail = entityDetail()
|
|
67
|
+
detail.fields = detail.fields.map((field) => ({ ...field, key: '' }))
|
|
68
|
+
return detail
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function sdkSchemaPayload() {
|
|
72
|
+
return {
|
|
73
|
+
...appDetailWithEntityList(),
|
|
74
|
+
entities: [
|
|
75
|
+
{
|
|
76
|
+
...entityDetail(),
|
|
77
|
+
key: 'story',
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
test('generateArcubaseProjectSDK emits typed key based entity sdk', () => {
|
|
84
|
+
const result = generateArcubaseProjectSDK(sdkSchemaPayload())
|
|
85
|
+
|
|
86
|
+
assert.deepEqual(result.files.map((file) => file.path).sort(), [
|
|
87
|
+
'client.ts',
|
|
88
|
+
'entities/story.ts',
|
|
89
|
+
'index.ts',
|
|
90
|
+
'runtime.ts',
|
|
91
|
+
'schema.ts',
|
|
92
|
+
])
|
|
93
|
+
|
|
94
|
+
const client = result.files.find((file) => file.path === 'client.ts')?.contents ?? ''
|
|
95
|
+
assert.match(client, /createArcubaseSdk/)
|
|
96
|
+
assert.match(client, /story: createStorySDK\(runtime\)/)
|
|
97
|
+
|
|
98
|
+
const runtime = result.files.find((file) => file.path === 'runtime.ts')?.contents ?? ''
|
|
99
|
+
assert.match(runtime, /accessToken/)
|
|
100
|
+
assert.match(runtime, /refreshToken/)
|
|
101
|
+
|
|
102
|
+
const story = result.files.find((file) => file.path === 'entities/story.ts')?.contents ?? ''
|
|
103
|
+
assert.match(story, /export type StoryFields =/)
|
|
104
|
+
assert.match(story, /era\?: number \| string/)
|
|
105
|
+
assert.match(story, /recentEvent\?: string/)
|
|
106
|
+
assert.match(story, /story: string/)
|
|
107
|
+
assert.match(story, /create\(fields: StoryCreateInput\)/)
|
|
108
|
+
assert.match(story, /update\(rowId: string \| number, fields: StoryUpdateInput\)/)
|
|
109
|
+
assert.match(story, /toArcubaseFields\(fields\)/)
|
|
110
|
+
assert.match(story, /changed_fields: Object.keys\(data\)\.map\(\(fieldId\) => Number\(fieldId\)\)/)
|
|
111
|
+
assert.match(story, /\/entity\/3882856425\/\$\{entityId\}\/insert/)
|
|
112
|
+
|
|
113
|
+
const schema = result.files.find((file) => file.path === 'schema.ts')?.contents ?? ''
|
|
114
|
+
assert.match(schema, /"story"/)
|
|
115
|
+
assert.match(schema, /"fieldId": 1011/)
|
|
116
|
+
assert.match(schema, /"entityId": 3883547182/)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('generateArcubaseProjectSDK rejects missing field keys', () => {
|
|
120
|
+
const payload = sdkSchemaPayload()
|
|
121
|
+
payload.entities[0].fields[1].key = ''
|
|
122
|
+
|
|
123
|
+
assert.throws(() => generateArcubaseProjectSDK(payload), (error: unknown) => {
|
|
124
|
+
assert.ok(error instanceof CLIError)
|
|
125
|
+
assert.equal(error.code, 'SDK_GEN_FIELD_KEY_REQUIRED')
|
|
126
|
+
assert.match(error.message, /你刚刚/)
|
|
127
|
+
return true
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test('generateArcubaseProjectSDK rejects duplicate field keys within an entity', () => {
|
|
132
|
+
const payload = sdkSchemaPayload()
|
|
133
|
+
payload.entities[0].fields[1].key = 'story'
|
|
134
|
+
|
|
135
|
+
assert.throws(() => generateArcubaseProjectSDK(payload), (error: unknown) => {
|
|
136
|
+
assert.ok(error instanceof CLIError)
|
|
137
|
+
assert.equal(error.code, 'SDK_GEN_DUPLICATE_FIELD_KEY')
|
|
138
|
+
assert.match(error.message, /story/)
|
|
139
|
+
return true
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test('arcubase-admin dev sdk-gen fetches app schema and writes typed sdk files', async () => {
|
|
144
|
+
const outDir = fs.mkdtempSync(path.join(os.tmpdir(), 'arcubase-sdk-gen-'))
|
|
145
|
+
const calls: Array<{ url: string; method?: string }> = []
|
|
146
|
+
|
|
147
|
+
const result = await executeCLI(
|
|
148
|
+
'admin',
|
|
149
|
+
['dev', 'sdk-gen', '--app-id', '3882856425', '--out', outDir],
|
|
150
|
+
env as any,
|
|
151
|
+
async (url, init) => {
|
|
152
|
+
calls.push({ url: String(url), method: init?.method })
|
|
153
|
+
if (String(url).endsWith('/api/apps/3882856425')) {
|
|
154
|
+
return new Response(JSON.stringify({ data: appDetail() }), { status: 200 })
|
|
155
|
+
}
|
|
156
|
+
if (String(url).endsWith('/api/apps/3882856425/entities')) {
|
|
157
|
+
return new Response(JSON.stringify({ data: entityList() }), { status: 200 })
|
|
158
|
+
}
|
|
159
|
+
if (String(url).endsWith('/api/apps/3882856425/entity/3883547182')) {
|
|
160
|
+
return new Response(JSON.stringify({ data: entityDetail() }), { status: 200 })
|
|
161
|
+
}
|
|
162
|
+
return new Response(JSON.stringify({ error: { message: `unexpected url ${url}` } }), { status: 404 })
|
|
163
|
+
},
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
assert.equal(result.kind, 'result')
|
|
167
|
+
assert.deepEqual(result.commandPath, ['dev', 'sdk-gen'])
|
|
168
|
+
assert.equal(calls[0].method, 'GET')
|
|
169
|
+
assert.match(calls[0].url, /\/api\/apps\/3882856425$/)
|
|
170
|
+
assert.equal(calls[1].method, 'GET')
|
|
171
|
+
assert.match(calls[1].url, /\/api\/apps\/3882856425\/entities$/)
|
|
172
|
+
assert.equal(calls[2].method, 'GET')
|
|
173
|
+
assert.match(calls[2].url, /\/api\/apps\/3882856425\/entity\/3883547182$/)
|
|
174
|
+
assert.ok(fs.existsSync(path.join(outDir, 'index.ts')))
|
|
175
|
+
assert.ok(fs.existsSync(path.join(outDir, 'client.ts')))
|
|
176
|
+
assert.ok(fs.existsSync(path.join(outDir, 'entities/story.ts')))
|
|
177
|
+
assert.match(fs.readFileSync(path.join(outDir, 'client.ts'), 'utf8'), /createArcubaseSdk/)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
test('arcubase-admin dev sdk-gen initializes missing field keys through admin auth before generation', async () => {
|
|
181
|
+
const outDir = fs.mkdtempSync(path.join(os.tmpdir(), 'arcubase-sdk-gen-'))
|
|
182
|
+
const calls: Array<{ url: string; method?: string; authorization?: string; body?: any }> = []
|
|
183
|
+
|
|
184
|
+
const result = await executeCLI(
|
|
185
|
+
'admin',
|
|
186
|
+
['dev', 'sdk-gen', '--app-id', '3882856425', '--out', outDir],
|
|
187
|
+
env as any,
|
|
188
|
+
async (url, init) => {
|
|
189
|
+
const bodyText = typeof init?.body === 'string' ? init.body : undefined
|
|
190
|
+
calls.push({
|
|
191
|
+
url: String(url),
|
|
192
|
+
method: init?.method,
|
|
193
|
+
authorization: init?.headers instanceof Headers
|
|
194
|
+
? init.headers.get('Authorization') ?? undefined
|
|
195
|
+
: (init?.headers as Record<string, string> | undefined)?.Authorization,
|
|
196
|
+
body: bodyText ? JSON.parse(bodyText) : undefined,
|
|
197
|
+
})
|
|
198
|
+
if (String(url).endsWith('/api/apps/3882856425')) {
|
|
199
|
+
return new Response(JSON.stringify({ data: appDetail() }), { status: 200 })
|
|
200
|
+
}
|
|
201
|
+
if (String(url).endsWith('/api/apps/3882856425/entities')) {
|
|
202
|
+
return new Response(JSON.stringify({ data: entityList() }), { status: 200 })
|
|
203
|
+
}
|
|
204
|
+
if (String(url).endsWith('/api/apps/3882856425/entity/3883547182')) {
|
|
205
|
+
return new Response(JSON.stringify({ data: entityDetailWithoutFieldKeys() }), { status: 200 })
|
|
206
|
+
}
|
|
207
|
+
if (String(url).endsWith('/api/apps/3882856425/entity/3883547182/custom-keys')) {
|
|
208
|
+
return new Response(JSON.stringify({ data: entityDetail() }), { status: 200 })
|
|
209
|
+
}
|
|
210
|
+
return new Response(JSON.stringify({ error: { message: `unexpected url ${url}` } }), { status: 404 })
|
|
211
|
+
},
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
assert.equal(result.kind, 'result')
|
|
215
|
+
const customKeysCall = calls.find((call) => call.url.endsWith('/api/apps/3882856425/entity/3883547182/custom-keys'))
|
|
216
|
+
assert.ok(customKeysCall)
|
|
217
|
+
assert.equal(customKeysCall.method, 'PUT')
|
|
218
|
+
assert.equal(customKeysCall.authorization, 'Bearer tok')
|
|
219
|
+
assert.deepEqual(customKeysCall.body, [
|
|
220
|
+
{ id: 1006, key: 'field1006' },
|
|
221
|
+
{ id: 1013, key: 'field1013' },
|
|
222
|
+
{ id: 1011, key: 'field1011' },
|
|
223
|
+
])
|
|
224
|
+
assert.match(fs.readFileSync(path.join(outDir, 'entities/story.ts'), 'utf8'), /field1011: string/)
|
|
225
|
+
assert.deepEqual(result.data.initializedFieldKeys, [{ entityId: 3883547182, fields: ['field1006', 'field1013', 'field1011'] }])
|
|
226
|
+
})
|
|
@@ -68,6 +68,21 @@ test('runtime docs do not use retired table create and row probe names', () => {
|
|
|
68
68
|
}
|
|
69
69
|
})
|
|
70
70
|
|
|
71
|
+
test('runtime docs document dashboard and widget single success paths', () => {
|
|
72
|
+
const dashboard = fs.readFileSync(path.join(root, 'dashboard.md'), 'utf8')
|
|
73
|
+
const widgets = fs.readFileSync(path.join(root, 'widgets.md'), 'utf8')
|
|
74
|
+
assert.match(dashboard, /arcubase dashboard get/)
|
|
75
|
+
assert.match(dashboard, /arcubase-admin dashboard list/)
|
|
76
|
+
assert.match(dashboard, /not exposed/)
|
|
77
|
+
assert.match(widgets, /arcubase-admin widget list/)
|
|
78
|
+
assert.match(widgets, /--widget-id/)
|
|
79
|
+
for (const aggregate of ['sum', 'avg', 'max', 'min', 'count', 'distinct_count']) {
|
|
80
|
+
assert.match(widgets, new RegExp(`\\\`${aggregate}\\\``))
|
|
81
|
+
}
|
|
82
|
+
assert.match(widgets, /unsupported aggregate names are invalid request bodies/)
|
|
83
|
+
assert.doesNotMatch(widgets, /widgets_id/)
|
|
84
|
+
})
|
|
85
|
+
|
|
71
86
|
test('scenario examples exist and are readable', () => {
|
|
72
87
|
for (const dir of ['oms-01', 'wms-01', 'crm-01']) {
|
|
73
88
|
const readme = path.join(root, 'examples', dir, 'README.md')
|
|
@@ -1094,6 +1094,135 @@ test('entry fixed app tables query cannot be disabled by query file', async () =
|
|
|
1094
1094
|
assert.equal(out.data.url, 'https://arcubase.example.com/api/entry?include_app_tables=true')
|
|
1095
1095
|
})
|
|
1096
1096
|
|
|
1097
|
+
test('dashboard and widget commands map to approved endpoints', async () => {
|
|
1098
|
+
const adminWidgetList = await executeCLI(
|
|
1099
|
+
'admin',
|
|
1100
|
+
['widget', 'list', '--app-id', 'app_1', '--type', 'dashboard', '--dashboard-id', '100'],
|
|
1101
|
+
env as any,
|
|
1102
|
+
async (url, init) => new Response(JSON.stringify({ url, method: init?.method }), { status: 200 }),
|
|
1103
|
+
)
|
|
1104
|
+
assert.equal(adminWidgetList.kind, 'result')
|
|
1105
|
+
assert.equal(adminWidgetList.data.url, 'https://arcubase.example.com/api/apps/app_1/widgets?type=dashboard&dashboard_id=100')
|
|
1106
|
+
assert.equal(adminWidgetList.data.method, 'GET')
|
|
1107
|
+
|
|
1108
|
+
const userDashboardWidgets = await executeCLI(
|
|
1109
|
+
'user',
|
|
1110
|
+
['dashboard', 'widgets', '--app-id', 'app_1', '--dashboard-id', 'dash_1'],
|
|
1111
|
+
env as any,
|
|
1112
|
+
async (url, init) => new Response(JSON.stringify({ url, method: init?.method }), { status: 200 }),
|
|
1113
|
+
)
|
|
1114
|
+
assert.equal(userDashboardWidgets.kind, 'result')
|
|
1115
|
+
assert.equal(userDashboardWidgets.data.url, 'https://arcubase.example.com/api/dashboard/app_1/dash_1/widgets')
|
|
1116
|
+
assert.equal(userDashboardWidgets.data.method, 'GET')
|
|
1117
|
+
|
|
1118
|
+
const userWidgetData = await executeCLI(
|
|
1119
|
+
'user',
|
|
1120
|
+
['widget', 'data', '--app-id', 'app_1', '--body-json', '{"dashboard_id":100,"type":1}'],
|
|
1121
|
+
env as any,
|
|
1122
|
+
async (url, init) => new Response(JSON.stringify({
|
|
1123
|
+
url,
|
|
1124
|
+
method: init?.method,
|
|
1125
|
+
body: init?.body ? JSON.parse(String(init.body)) : null,
|
|
1126
|
+
}), { status: 200 }),
|
|
1127
|
+
)
|
|
1128
|
+
assert.equal(userWidgetData.kind, 'result')
|
|
1129
|
+
assert.equal(userWidgetData.data.url, 'https://arcubase.example.com/api/apps/app_1/widgets-data')
|
|
1130
|
+
assert.equal(userWidgetData.data.method, 'POST')
|
|
1131
|
+
assert.deepEqual(userWidgetData.data.body, { dashboard_id: 100, type: 1 })
|
|
1132
|
+
})
|
|
1133
|
+
|
|
1134
|
+
test('widget preview-data rejects unsupported aggregate before request', async () => {
|
|
1135
|
+
let called = false
|
|
1136
|
+
await assert.rejects(async () => {
|
|
1137
|
+
await executeCLI(
|
|
1138
|
+
'admin',
|
|
1139
|
+
[
|
|
1140
|
+
'widget',
|
|
1141
|
+
'preview-data',
|
|
1142
|
+
'--app-id',
|
|
1143
|
+
'app_1',
|
|
1144
|
+
'--body-json',
|
|
1145
|
+
'{"dataSource":2188891001,"dataSourceType":"entity","measures":[{"Name":"median_amount","FieldID":1004,"Aggregate":"median"}]}',
|
|
1146
|
+
],
|
|
1147
|
+
env as any,
|
|
1148
|
+
async () => {
|
|
1149
|
+
called = true
|
|
1150
|
+
return new Response('{}', { status: 200 })
|
|
1151
|
+
},
|
|
1152
|
+
)
|
|
1153
|
+
}, (error: unknown) => {
|
|
1154
|
+
assert.ok(error instanceof CLIError)
|
|
1155
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
1156
|
+
assert.equal(error.details?.requestType, 'AppPreviewWidgetsDataReqVO')
|
|
1157
|
+
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.measures.0.Aggregate'))
|
|
1158
|
+
assert.ok((error.details?.docHints ?? []).some((item) => item.file === '/runtime/arcubase-sdk/docs/runtime-reference/widgets.md'))
|
|
1159
|
+
return true
|
|
1160
|
+
})
|
|
1161
|
+
assert.equal(called, false)
|
|
1162
|
+
})
|
|
1163
|
+
|
|
1164
|
+
test('widget preview-data rejects field dimensions without IsField true before request', async () => {
|
|
1165
|
+
let called = false
|
|
1166
|
+
await assert.rejects(async () => {
|
|
1167
|
+
await executeCLI(
|
|
1168
|
+
'admin',
|
|
1169
|
+
[
|
|
1170
|
+
'widget',
|
|
1171
|
+
'preview-data',
|
|
1172
|
+
'--app-id',
|
|
1173
|
+
'app_1',
|
|
1174
|
+
'--body-json',
|
|
1175
|
+
'{"dataSource":2188891001,"dataSourceType":"entity","dimensions":[{"FieldID":1004,"Name":"Source"}],"measures":[{"Name":"Total Amount","FieldID":1005,"Aggregate":"sum"}]}',
|
|
1176
|
+
],
|
|
1177
|
+
env as any,
|
|
1178
|
+
async () => {
|
|
1179
|
+
called = true
|
|
1180
|
+
return new Response('{}', { status: 200 })
|
|
1181
|
+
},
|
|
1182
|
+
)
|
|
1183
|
+
}, (error: unknown) => {
|
|
1184
|
+
assert.ok(error instanceof CLIError)
|
|
1185
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
1186
|
+
assert.equal(error.details?.requestType, 'AppPreviewWidgetsDataReqVO')
|
|
1187
|
+
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.dimensions.0.IsField'))
|
|
1188
|
+
assert.ok((error.details?.commonMistakes ?? []).includes('field dimensions with FieldID must set IsField:true'))
|
|
1189
|
+
return true
|
|
1190
|
+
})
|
|
1191
|
+
assert.equal(called, false)
|
|
1192
|
+
})
|
|
1193
|
+
|
|
1194
|
+
test('widget update rejects chart field dimensions without IsField true before request', async () => {
|
|
1195
|
+
let called = false
|
|
1196
|
+
await assert.rejects(async () => {
|
|
1197
|
+
await executeCLI(
|
|
1198
|
+
'admin',
|
|
1199
|
+
[
|
|
1200
|
+
'widget',
|
|
1201
|
+
'update',
|
|
1202
|
+
'--app-id',
|
|
1203
|
+
'app_1',
|
|
1204
|
+
'--widget-id',
|
|
1205
|
+
'widget_1',
|
|
1206
|
+
'--body-json',
|
|
1207
|
+
'{"entity_id":2188891001,"options":{"charts":{"entity_id":2188891001,"dimensions":[{"FieldID":1004,"IsField":false,"Name":"Source"}],"measures":[{"Name":"Total Amount","FieldID":1005,"Aggregate":"sum"}]}}}',
|
|
1208
|
+
],
|
|
1209
|
+
env as any,
|
|
1210
|
+
async () => {
|
|
1211
|
+
called = true
|
|
1212
|
+
return new Response('{}', { status: 200 })
|
|
1213
|
+
},
|
|
1214
|
+
)
|
|
1215
|
+
}, (error: unknown) => {
|
|
1216
|
+
assert.ok(error instanceof CLIError)
|
|
1217
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
1218
|
+
assert.equal(error.details?.requestType, 'AppUpdateWidgetsReqVO')
|
|
1219
|
+
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.options.charts.dimensions.0.IsField'))
|
|
1220
|
+
assert.ok((error.details?.commonMistakes ?? []).includes('field dimensions with FieldID must set IsField:true'))
|
|
1221
|
+
return true
|
|
1222
|
+
})
|
|
1223
|
+
assert.equal(called, false)
|
|
1224
|
+
})
|
|
1225
|
+
|
|
1097
1226
|
test('upstream http failures include endpoint status and body details', async () => {
|
|
1098
1227
|
await assert.rejects(async () => {
|
|
1099
1228
|
await executeCLI('user', ['entry'], env as any, async () => new Response('', { status: 500 }))
|
|
@@ -1387,6 +1516,92 @@ test('access-rule assign-users accepts canonical mixed user_scope body', async (
|
|
|
1387
1516
|
])
|
|
1388
1517
|
})
|
|
1389
1518
|
|
|
1519
|
+
test('access-rule bulk-apply normalizes numeric string ids before validation and request', async () => {
|
|
1520
|
+
const body = JSON.stringify({
|
|
1521
|
+
mode: 'update_existing',
|
|
1522
|
+
items: [
|
|
1523
|
+
{
|
|
1524
|
+
table_id: '2188893443',
|
|
1525
|
+
rule_id: '2188893557',
|
|
1526
|
+
enabled: true,
|
|
1527
|
+
options: {
|
|
1528
|
+
user_scope: [{ type: 'department', id: '2188881201' }],
|
|
1529
|
+
policy: { key: 'custom', actions: ['view'], all_fields_read: true, all_fields_write: false },
|
|
1530
|
+
list_options: {},
|
|
1531
|
+
},
|
|
1532
|
+
},
|
|
1533
|
+
],
|
|
1534
|
+
})
|
|
1535
|
+
let requestedURL = ''
|
|
1536
|
+
const out = await executeCLI(
|
|
1537
|
+
'admin',
|
|
1538
|
+
['access-rule', 'bulk-apply', '--app-id', 'app_1', '--body-json', body],
|
|
1539
|
+
env as any,
|
|
1540
|
+
async (url, init) => {
|
|
1541
|
+
requestedURL = String(url)
|
|
1542
|
+
return new Response(String(init?.body ?? '{}'), { status: 200 })
|
|
1543
|
+
},
|
|
1544
|
+
)
|
|
1545
|
+
assert.equal(out.kind, 'result')
|
|
1546
|
+
assert.match(requestedURL, /\/api\/apps\/app_1\/access-rules\/bulk-apply$/)
|
|
1547
|
+
assert.equal(out.data.items[0].table_id, 2188893443)
|
|
1548
|
+
assert.equal(out.data.items[0].rule_id, 2188893557)
|
|
1549
|
+
assert.deepEqual(out.data.items[0].options.user_scope, [{ type: 'department', id: 2188881201 }])
|
|
1550
|
+
})
|
|
1551
|
+
|
|
1552
|
+
test('access-rule bulk-apply rejects empty items before request', async () => {
|
|
1553
|
+
await assert.rejects(async () => {
|
|
1554
|
+
await executeCLI(
|
|
1555
|
+
'admin',
|
|
1556
|
+
['access-rule', 'bulk-apply', '--app-id', 'app_1', '--body-json', '{"mode":"upsert_by_name","items":[]}'],
|
|
1557
|
+
env as any,
|
|
1558
|
+
async () => new Response('{}', { status: 200 }),
|
|
1559
|
+
)
|
|
1560
|
+
}, (error: unknown) => {
|
|
1561
|
+
assert.ok(error instanceof CLIError)
|
|
1562
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
1563
|
+
assert.equal(error.details?.requestType, 'AppIngressBulkApplyReqVO')
|
|
1564
|
+
assert.match(error.message, /non-empty body.items/)
|
|
1565
|
+
return true
|
|
1566
|
+
})
|
|
1567
|
+
})
|
|
1568
|
+
|
|
1569
|
+
test('access-rule bulk-apply rejects non-canonical policy actions before request', async () => {
|
|
1570
|
+
const body = JSON.stringify({
|
|
1571
|
+
mode: 'upsert_by_name',
|
|
1572
|
+
items: [
|
|
1573
|
+
{
|
|
1574
|
+
table_id: 2188893443,
|
|
1575
|
+
name: 'Bad read alias',
|
|
1576
|
+
enabled: true,
|
|
1577
|
+
options: {
|
|
1578
|
+
user_scope: [{ type: 'user', id: 2188889901 }],
|
|
1579
|
+
policy: { key: 'custom', actions: ['read'], all_fields_read: true, all_fields_write: false },
|
|
1580
|
+
list_options: {},
|
|
1581
|
+
},
|
|
1582
|
+
},
|
|
1583
|
+
],
|
|
1584
|
+
})
|
|
1585
|
+
await assert.rejects(async () => {
|
|
1586
|
+
await executeCLI(
|
|
1587
|
+
'admin',
|
|
1588
|
+
['access-rule', 'bulk-apply', '--app-id', 'app_1', '--body-json', body],
|
|
1589
|
+
env as any,
|
|
1590
|
+
async () => new Response('{}', { status: 200 }),
|
|
1591
|
+
)
|
|
1592
|
+
}, (error: unknown) => {
|
|
1593
|
+
assert.ok(error instanceof CLIError)
|
|
1594
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
1595
|
+
assert.equal(error.details?.requestType, 'AppIngressBulkApplyReqVO')
|
|
1596
|
+
assert.match(error.message, /actions must use Arcubase action codes/)
|
|
1597
|
+
assert.deepEqual(error.details?.issues?.[0], {
|
|
1598
|
+
path: 'body.items.0.options.policy.actions',
|
|
1599
|
+
message: 'access-rule policy.actions must use Arcubase action codes: use "view" for read and "add","edit" for write; do not use "read", "write", or "insert"',
|
|
1600
|
+
})
|
|
1601
|
+
return true
|
|
1602
|
+
})
|
|
1603
|
+
})
|
|
1604
|
+
|
|
1390
1605
|
test('access-rule create accepts nested custom policy condition with field columns', async () => {
|
|
1391
1606
|
const body = JSON.stringify({
|
|
1392
1607
|
name: 'Sales own or west region',
|