@carthooks/arcubase-cli 0.1.7 → 0.1.9
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 +1042 -5056
- package/bundle/arcubase.mjs +1042 -5056
- package/dist/generated/command_registry.generated.d.ts +693 -2812
- package/dist/generated/command_registry.generated.d.ts.map +1 -1
- package/dist/generated/command_registry.generated.js +812 -4100
- package/dist/generated/type_index.generated.d.ts +8 -397
- package/dist/generated/type_index.generated.d.ts.map +1 -1
- package/dist/generated/type_index.generated.js +8 -403
- package/dist/generated/zod_registry.generated.d.ts +33 -261
- package/dist/generated/zod_registry.generated.d.ts.map +1 -1
- package/dist/generated/zod_registry.generated.js +36 -365
- package/dist/runtime/body_loader.d.ts.map +1 -1
- package/dist/runtime/body_loader.js +12 -0
- package/dist/runtime/command_registry.d.ts +3 -21
- package/dist/runtime/command_registry.d.ts.map +1 -1
- package/dist/runtime/command_registry.js +17 -23
- package/dist/runtime/execute.d.ts +3 -3
- package/dist/runtime/execute.d.ts.map +1 -1
- package/dist/runtime/execute.js +53 -157
- package/dist/runtime/http.js +1 -1
- package/dist/runtime/upload.js +3 -3
- package/dist/runtime/zod_registry.d.ts.map +1 -1
- package/dist/runtime/zod_registry.js +7 -24
- package/dist/tests/command_registry.test.js +81 -28
- package/dist/tests/execute_validation.test.js +175 -510
- package/dist/tests/help.test.js +39 -62
- package/dist/tests/upload.test.js +4 -4
- package/dist/tests/zod_registry.test.js +0 -10
- package/package.json +1 -1
- package/sdk-dist/docs/runtime-reference/README.md +20 -30
- package/sdk-dist/docs/runtime-reference/entity-schema/README.md +4 -5
- package/sdk-dist/docs/runtime-reference/entity-schema/linkto.md +3 -3
- package/sdk-dist/docs/runtime-reference/entity-schema/query.md +3 -3
- package/sdk-dist/docs/runtime-reference/entity-schema/relation.md +3 -3
- package/sdk-dist/docs/runtime-reference/entity-schema/relationfield.md +3 -3
- package/sdk-dist/docs/runtime-reference/entity-schema/subform.md +2 -2
- package/sdk-dist/docs/runtime-reference/entity-schema.md +4 -4
- package/sdk-dist/docs/runtime-reference/examples/README.md +2 -2
- package/sdk-dist/docs/runtime-reference/examples/oms-01/README.md +12 -12
- package/sdk-dist/docs/runtime-reference/examples/oms-01/app-overview.md +1 -1
- package/sdk-dist/docs/runtime-reference/row-crud.md +11 -32
- package/sdk-dist/docs/runtime-reference/search-and-bulk-actions.md +16 -47
- package/sdk-dist/docs/runtime-reference/table-lifecycle.md +11 -27
- package/sdk-dist/docs/runtime-reference/uploads.md +1 -1
- package/sdk-dist/docs/runtime-reference/workflow/README.md +19 -0
- package/sdk-dist/generated/command_registry.generated.ts +814 -4102
- package/sdk-dist/generated/type_index.generated.ts +8 -403
- package/sdk-dist/generated/zod_registry.generated.ts +44 -500
- package/sdk-dist/types/app.ts +3 -10
- package/sdk-dist/types/common.ts +7 -0
- package/sdk-dist/types/global-action.ts +2 -1
- package/src/generated/command_registry.generated.ts +814 -4102
- package/src/generated/type_index.generated.ts +8 -403
- package/src/generated/zod_registry.generated.ts +44 -500
- package/src/runtime/body_loader.ts +11 -0
- package/src/runtime/command_registry.ts +22 -44
- package/src/runtime/execute.ts +157 -165
- package/src/runtime/http.ts +1 -1
- package/src/runtime/upload.ts +3 -3
- package/src/runtime/zod_registry.ts +7 -25
- package/src/tests/command_registry.test.ts +81 -31
- package/src/tests/execute_validation.test.ts +236 -603
- package/src/tests/help.test.ts +47 -66
- package/src/tests/upload.test.ts +4 -4
- package/src/tests/zod_registry.test.ts +0 -13
- package/sdk-dist/docs/runtime-reference/app-discovery.md +0 -68
|
@@ -22,242 +22,200 @@ function tempJSONFile(payload: unknown): string {
|
|
|
22
22
|
return filePath
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
function validEntitySavePayload(overrides: Record<string, unknown> = {}) {
|
|
26
|
+
return {
|
|
27
|
+
id: 2188889845,
|
|
28
|
+
app_id: 2188889844,
|
|
29
|
+
name: '订单',
|
|
30
|
+
schema_version: 1,
|
|
31
|
+
field_id_seq: 1002,
|
|
32
|
+
layout: [[1001]],
|
|
33
|
+
fields: [
|
|
34
|
+
{
|
|
35
|
+
id: 1001,
|
|
36
|
+
label: '订单号',
|
|
37
|
+
type: 'text',
|
|
38
|
+
required: true,
|
|
39
|
+
unique: false,
|
|
40
|
+
editable: true,
|
|
41
|
+
visible: true,
|
|
42
|
+
show_label: true,
|
|
43
|
+
scannable: false,
|
|
44
|
+
default_value_mode: 0,
|
|
45
|
+
description: '',
|
|
46
|
+
value: null,
|
|
47
|
+
number_decimal: 0,
|
|
48
|
+
depends: [],
|
|
49
|
+
key: 'order_id',
|
|
50
|
+
code_index: false,
|
|
51
|
+
options: {
|
|
52
|
+
placeholder: '',
|
|
53
|
+
type: 'text',
|
|
54
|
+
lengthLimit: false,
|
|
55
|
+
lengthMin: 0,
|
|
56
|
+
lengthMax: 255,
|
|
57
|
+
},
|
|
58
|
+
transfers: null,
|
|
59
|
+
children: null,
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
options: {
|
|
63
|
+
Buttons: [],
|
|
64
|
+
CustomActions: [],
|
|
65
|
+
FrontendEvents: [],
|
|
66
|
+
TitleFormat: {
|
|
67
|
+
Fields: [],
|
|
68
|
+
Parts: [],
|
|
69
|
+
Body: '',
|
|
70
|
+
},
|
|
71
|
+
misc: {},
|
|
72
|
+
},
|
|
73
|
+
workflow_enabled: false,
|
|
74
|
+
...overrides,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
test('table update-schema rejects invalid body file', async () => {
|
|
26
79
|
const invalidFile = tempJSONFile({ name: '订单', layout: 'bad-layout' })
|
|
27
80
|
await assert.rejects(async () => {
|
|
28
|
-
await executeCLI('admin', ['
|
|
81
|
+
await executeCLI('admin', ['table', 'update-schema', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', invalidFile], env as any, async () => new Response('{}'))
|
|
29
82
|
}, (error: unknown) => {
|
|
30
83
|
assert.ok(error instanceof CLIError)
|
|
31
84
|
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
32
85
|
assert.equal(error.details?.requestType, 'EntitySaveReqVO')
|
|
33
86
|
assert.ok((error.details?.issues ?? []).some((item) => item.path.startsWith('body.layout')))
|
|
34
|
-
assert.ok((error.details?.tsHints ?? []).some((item) => item.file.startsWith('/opt/arcubase-sdk/')))
|
|
35
|
-
assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/entity-schema.md')))
|
|
36
87
|
return true
|
|
37
88
|
})
|
|
38
89
|
})
|
|
39
90
|
|
|
40
|
-
test('
|
|
91
|
+
test('app create rejects name longer than 20 chars', async () => {
|
|
41
92
|
const bodyFile = tempJSONFile({ name: 'CLI Verify Happy Path' })
|
|
42
93
|
await assert.rejects(async () => {
|
|
43
|
-
await executeCLI(
|
|
44
|
-
'admin',
|
|
45
|
-
['app', 'create-app-by-tenants', '--body-file', bodyFile],
|
|
46
|
-
env as any,
|
|
47
|
-
async () => new Response('{}', { status: 200 }),
|
|
48
|
-
)
|
|
94
|
+
await executeCLI('admin', ['app', 'create', '--body-file', bodyFile], env as any, async () => new Response('{}', { status: 200 }))
|
|
49
95
|
}, (error: unknown) => {
|
|
50
96
|
assert.ok(error instanceof CLIError)
|
|
51
97
|
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
52
98
|
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.name'))
|
|
53
|
-
assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/table-lifecycle.md')))
|
|
54
99
|
return true
|
|
55
100
|
})
|
|
56
101
|
})
|
|
57
102
|
|
|
58
|
-
test('
|
|
103
|
+
test('table create executes with valid body file', async () => {
|
|
59
104
|
const bodyFile = tempJSONFile({ name: '订单', parent: 0 })
|
|
60
105
|
const out = await executeCLI(
|
|
61
106
|
'admin',
|
|
62
|
-
['
|
|
107
|
+
['table', 'create', '--app-id', 'app_1', '--body-file', bodyFile],
|
|
63
108
|
env as any,
|
|
64
|
-
async (url, init) =>
|
|
109
|
+
async (url, init) =>
|
|
110
|
+
new Response(JSON.stringify({
|
|
111
|
+
url,
|
|
112
|
+
method: init?.method,
|
|
113
|
+
hasTenantHeader: Object.prototype.hasOwnProperty.call((init?.headers as Record<string, string>) ?? {}, 'X-Tenant-Id'),
|
|
114
|
+
hasHostHeader: Object.prototype.hasOwnProperty.call((init?.headers as Record<string, string>) ?? {}, 'X-Host'),
|
|
115
|
+
}), { status: 200 }),
|
|
65
116
|
)
|
|
66
117
|
assert.equal(out.kind, 'result')
|
|
67
|
-
assert.equal(out.
|
|
68
|
-
assert.equal(out.data.
|
|
69
|
-
assert.equal(out.data.
|
|
70
|
-
assert.equal(out.data.headers['X-Host'], undefined)
|
|
71
|
-
assert.equal(out.data.headers['X-Tenant-Id'], undefined)
|
|
118
|
+
assert.equal(out.data.url, 'https://arcubase.example.com/api/apps/app_1/entities')
|
|
119
|
+
assert.equal(out.data.hasTenantHeader, false)
|
|
120
|
+
assert.equal(out.data.hasHostHeader, false)
|
|
72
121
|
})
|
|
73
122
|
|
|
74
|
-
test('
|
|
123
|
+
test('table create executes with valid body json', async () => {
|
|
75
124
|
const out = await executeCLI(
|
|
76
125
|
'admin',
|
|
77
|
-
['
|
|
126
|
+
['table', 'create', '--app-id', 'app_1', '--body-json', '{"name":"订单","parent":0}'],
|
|
78
127
|
env as any,
|
|
79
|
-
async (url, init) => new Response(JSON.stringify({
|
|
128
|
+
async (url, init) => new Response(JSON.stringify({
|
|
129
|
+
url,
|
|
130
|
+
method: init?.method,
|
|
131
|
+
body: init?.body ? JSON.parse(String(init.body)) : null,
|
|
132
|
+
}), { status: 200 }),
|
|
80
133
|
)
|
|
81
134
|
assert.equal(out.kind, 'result')
|
|
82
|
-
assert.equal(out.data.url, 'https://arcubase.example.com/apps/app_1/
|
|
135
|
+
assert.equal(out.data.url, 'https://arcubase.example.com/api/apps/app_1/entities')
|
|
136
|
+
assert.deepEqual(out.data.body, { name: '订单', parent: 0 })
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('body json conflicts with body file', async () => {
|
|
140
|
+
const bodyFile = tempJSONFile({ name: '订单', parent: 0 })
|
|
141
|
+
await assert.rejects(async () => {
|
|
142
|
+
await executeCLI('admin', ['table', 'create', '--app-id', 'app_1', '--body-file', bodyFile, '--body-json', '{"name":"订单"}'], env as any, async () => new Response('{}', { status: 200 }))
|
|
143
|
+
}, (error: unknown) => {
|
|
144
|
+
assert.ok(error instanceof CLIError)
|
|
145
|
+
assert.equal(error.code, 'BODY_JSON_CONFLICT')
|
|
146
|
+
return true
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test('body json is rejected for commands without request type', async () => {
|
|
151
|
+
await assert.rejects(async () => {
|
|
152
|
+
await executeCLI('admin', ['app', 'list', '--body-json', '{"name":"ignored"}'], env as any, async () => new Response('{}', { status: 200 }))
|
|
153
|
+
}, (error: unknown) => {
|
|
154
|
+
assert.ok(error instanceof CLIError)
|
|
155
|
+
assert.equal(error.code, 'BODY_JSON_NOT_SUPPORTED')
|
|
156
|
+
return true
|
|
157
|
+
})
|
|
83
158
|
})
|
|
84
159
|
|
|
85
|
-
test('
|
|
86
|
-
|
|
160
|
+
test('body json parse errors fail fast', async () => {
|
|
161
|
+
await assert.rejects(async () => {
|
|
162
|
+
await executeCLI('admin', ['table', 'create', '--app-id', 'app_1', '--body-json', '{"name":'], env as any, async () => new Response('{}', { status: 200 }))
|
|
163
|
+
}, (error: unknown) => {
|
|
164
|
+
assert.ok(error instanceof CLIError)
|
|
165
|
+
assert.equal(error.code, 'INVALID_BODY_JSON')
|
|
166
|
+
return true
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
test('access-rule list executes against external root path', async () => {
|
|
87
171
|
const out = await executeCLI(
|
|
88
172
|
'admin',
|
|
89
|
-
['app', '
|
|
173
|
+
['access-rule', 'list', '--app-id', 'app_1', '--table-id', 'table_1'],
|
|
90
174
|
env as any,
|
|
91
|
-
async (url, init) => {
|
|
92
|
-
calls.push(`${init?.method ?? 'GET'} ${String(url)}`)
|
|
93
|
-
if (String(url) === 'https://arcubase.example.com/apps') {
|
|
94
|
-
return new Response(JSON.stringify({
|
|
95
|
-
data: [
|
|
96
|
-
{ id: 101, name: 'Workspace' },
|
|
97
|
-
{ id: 102, name: 'CRM' },
|
|
98
|
-
],
|
|
99
|
-
}), { status: 200 })
|
|
100
|
-
}
|
|
101
|
-
if (String(url) === 'https://arcubase.example.com/apps/101/entities') {
|
|
102
|
-
return new Response(JSON.stringify({
|
|
103
|
-
data: [
|
|
104
|
-
{ id: 201, name: 'Orders' },
|
|
105
|
-
{ id: 202, name: 'Customers' },
|
|
106
|
-
],
|
|
107
|
-
}), { status: 200 })
|
|
108
|
-
}
|
|
109
|
-
if (String(url) === 'https://arcubase.example.com/apps/102/entities') {
|
|
110
|
-
return new Response(JSON.stringify({
|
|
111
|
-
data: [
|
|
112
|
-
{ id: 203, name: 'Leads' },
|
|
113
|
-
],
|
|
114
|
-
}), { status: 200 })
|
|
115
|
-
}
|
|
116
|
-
return new Response('{}', { status: 404 })
|
|
117
|
-
},
|
|
175
|
+
async (url, init) => new Response(JSON.stringify({ url, method: init?.method }), { status: 200 }),
|
|
118
176
|
)
|
|
119
177
|
assert.equal(out.kind, 'result')
|
|
120
|
-
assert.
|
|
121
|
-
apps: [
|
|
122
|
-
{
|
|
123
|
-
id: '101',
|
|
124
|
-
name: 'Workspace',
|
|
125
|
-
entities: [
|
|
126
|
-
{ id: '201', name: 'Orders' },
|
|
127
|
-
{ id: '202', name: 'Customers' },
|
|
128
|
-
],
|
|
129
|
-
},
|
|
130
|
-
{
|
|
131
|
-
id: '102',
|
|
132
|
-
name: 'CRM',
|
|
133
|
-
entities: [
|
|
134
|
-
{ id: '203', name: 'Leads' },
|
|
135
|
-
],
|
|
136
|
-
},
|
|
137
|
-
],
|
|
138
|
-
})
|
|
139
|
-
assert.deepEqual(calls, [
|
|
140
|
-
'GET https://arcubase.example.com/apps',
|
|
141
|
-
'POST https://arcubase.example.com/apps/101/entities',
|
|
142
|
-
'POST https://arcubase.example.com/apps/102/entities',
|
|
143
|
-
])
|
|
178
|
+
assert.equal(out.data.url, 'https://arcubase.example.com/api/apps/app_1/entity/table_1/ingress')
|
|
144
179
|
})
|
|
145
180
|
|
|
146
|
-
test('
|
|
147
|
-
const bodyFile = tempJSONFile(
|
|
148
|
-
id: 2188889845,
|
|
149
|
-
app_id: 2188889844,
|
|
150
|
-
name: '订单',
|
|
151
|
-
schema_version: 1,
|
|
152
|
-
field_id_seq: 1002,
|
|
153
|
-
layout: [[1001]],
|
|
154
|
-
fields: [
|
|
155
|
-
{
|
|
156
|
-
id: 1001,
|
|
157
|
-
label: '订单号',
|
|
158
|
-
type: 'text',
|
|
159
|
-
required: true,
|
|
160
|
-
unique: false,
|
|
161
|
-
editable: true,
|
|
162
|
-
visible: true,
|
|
163
|
-
show_label: true,
|
|
164
|
-
scannable: false,
|
|
165
|
-
default_value_mode: 0,
|
|
166
|
-
description: '',
|
|
167
|
-
value: null,
|
|
168
|
-
number_decimal: 0,
|
|
169
|
-
depends: [],
|
|
170
|
-
key: 'order_id',
|
|
171
|
-
code_index: false,
|
|
172
|
-
options: {},
|
|
173
|
-
transfers: null,
|
|
174
|
-
children: null,
|
|
175
|
-
},
|
|
176
|
-
],
|
|
177
|
-
workflow_enabled: false,
|
|
178
|
-
})
|
|
181
|
+
test('table update-schema treats top-level error body as failure even on http 200', async () => {
|
|
182
|
+
const bodyFile = tempJSONFile(validEntitySavePayload())
|
|
179
183
|
await assert.rejects(async () => {
|
|
180
184
|
await executeCLI(
|
|
181
185
|
'admin',
|
|
182
|
-
['
|
|
186
|
+
['table', 'update-schema', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile],
|
|
183
187
|
env as any,
|
|
184
|
-
async () =>
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
message: 'json: cannot unmarshal object into Go struct field .EntityVO.fields of type []models.Field',
|
|
189
|
-
type: 'system',
|
|
190
|
-
key: 'error.system',
|
|
191
|
-
},
|
|
192
|
-
trace_id: 'req_xxx',
|
|
193
|
-
}),
|
|
194
|
-
{ status: 200 },
|
|
195
|
-
),
|
|
188
|
+
async () => new Response(JSON.stringify({
|
|
189
|
+
error: { message: 'json: cannot unmarshal object into Go struct field .EntityVO.fields of type []models.Field', type: 'system', key: 'error.system' },
|
|
190
|
+
trace_id: 'req_xxx',
|
|
191
|
+
}), { status: 200 }),
|
|
196
192
|
)
|
|
197
193
|
}, (error: unknown) => {
|
|
198
194
|
assert.ok(error instanceof CLIError)
|
|
199
195
|
assert.equal(error.code, 'UPSTREAM_BODY_ERROR')
|
|
200
|
-
assert.equal(error.details?.endpoint, '/apps/app_1/entity/
|
|
196
|
+
assert.equal(error.details?.endpoint, '/api/apps/app_1/entity/table_1')
|
|
201
197
|
assert.equal(error.details?.method, 'PUT')
|
|
202
198
|
assert.equal(error.details?.traceId, 'req_xxx')
|
|
203
199
|
return true
|
|
204
200
|
})
|
|
205
201
|
})
|
|
206
202
|
|
|
207
|
-
test('
|
|
208
|
-
const bodyFile = tempJSONFile({
|
|
209
|
-
id: 2188889845,
|
|
210
|
-
app_id: 2188889844,
|
|
211
|
-
name: '订单',
|
|
203
|
+
test('table update-schema normalizes shell null placeholders before validation', async () => {
|
|
204
|
+
const bodyFile = tempJSONFile(validEntitySavePayload({
|
|
212
205
|
schema_version: 0,
|
|
213
|
-
field_id_seq: 1002,
|
|
214
206
|
layout: null,
|
|
215
207
|
current_serial: null,
|
|
216
|
-
fields: [
|
|
217
|
-
{
|
|
218
|
-
id: 1001,
|
|
219
|
-
label: '订单号',
|
|
220
|
-
type: 'text',
|
|
221
|
-
required: true,
|
|
222
|
-
unique: false,
|
|
223
|
-
editable: true,
|
|
224
|
-
visible: true,
|
|
225
|
-
show_label: true,
|
|
226
|
-
scannable: false,
|
|
227
|
-
default_value_mode: 0,
|
|
228
|
-
description: '',
|
|
229
|
-
value: null,
|
|
230
|
-
number_decimal: 0,
|
|
231
|
-
depends: [],
|
|
232
|
-
key: 'order_id',
|
|
233
|
-
code_index: false,
|
|
234
|
-
options: {
|
|
235
|
-
placeholder: '',
|
|
236
|
-
type: 'text',
|
|
237
|
-
lengthLimit: false,
|
|
238
|
-
lengthMin: 0,
|
|
239
|
-
lengthMax: 255,
|
|
240
|
-
},
|
|
241
|
-
transfers: null,
|
|
242
|
-
children: null,
|
|
243
|
-
},
|
|
244
|
-
],
|
|
245
208
|
options: {
|
|
246
209
|
Buttons: null,
|
|
247
210
|
CustomActions: null,
|
|
248
211
|
FrontendEvents: null,
|
|
249
|
-
TitleFormat: {
|
|
250
|
-
Fields: null,
|
|
251
|
-
Parts: null,
|
|
252
|
-
Body: '',
|
|
253
|
-
},
|
|
212
|
+
TitleFormat: { Fields: null, Parts: null, Body: '' },
|
|
254
213
|
misc: null,
|
|
255
214
|
},
|
|
256
|
-
|
|
257
|
-
})
|
|
215
|
+
}))
|
|
258
216
|
const out = await executeCLI(
|
|
259
217
|
'admin',
|
|
260
|
-
['
|
|
218
|
+
['table', 'update-schema', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile],
|
|
261
219
|
env as any,
|
|
262
220
|
async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
|
|
263
221
|
)
|
|
@@ -273,52 +231,26 @@ test('admin save entity normalizes shell null placeholders before validation', a
|
|
|
273
231
|
assert.deepEqual(requestBody.options.misc, {})
|
|
274
232
|
})
|
|
275
233
|
|
|
276
|
-
test('delete
|
|
277
|
-
const bodyFile = tempJSONFile({
|
|
278
|
-
selection: {
|
|
279
|
-
type: 'selection',
|
|
280
|
-
ids: [4000000003],
|
|
281
|
-
length: 1,
|
|
282
|
-
},
|
|
283
|
-
})
|
|
284
|
-
|
|
234
|
+
test('row delete rejects selection type outside ids', async () => {
|
|
235
|
+
const bodyFile = tempJSONFile({ selection: { type: 'selection', ids: [4000000003], length: 1 } })
|
|
285
236
|
await assert.rejects(async () => {
|
|
286
|
-
await executeCLI(
|
|
287
|
-
'user',
|
|
288
|
-
['rows', 'delete-entity-row', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile],
|
|
289
|
-
env as any,
|
|
290
|
-
async () => new Response('{}', { status: 200 }),
|
|
291
|
-
)
|
|
237
|
+
await executeCLI('user', ['row', 'delete', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile], env as any, async () => new Response('{}', { status: 200 }))
|
|
292
238
|
}, (error: unknown) => {
|
|
293
239
|
assert.ok(error instanceof CLIError)
|
|
294
240
|
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
295
241
|
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.selection'))
|
|
296
|
-
assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/search-and-bulk-actions.md')))
|
|
297
242
|
return true
|
|
298
243
|
})
|
|
299
244
|
})
|
|
300
245
|
|
|
301
|
-
test('bulk
|
|
246
|
+
test('row bulk-update accepts ids selection with number_add action', async () => {
|
|
302
247
|
const bodyFile = tempJSONFile({
|
|
303
|
-
selection: {
|
|
304
|
-
|
|
305
|
-
ids: [4000000003],
|
|
306
|
-
length: 1,
|
|
307
|
-
},
|
|
308
|
-
fields: [
|
|
309
|
-
{
|
|
310
|
-
id: 1002,
|
|
311
|
-
action: {
|
|
312
|
-
type: 'number_add',
|
|
313
|
-
number_add: 10,
|
|
314
|
-
},
|
|
315
|
-
},
|
|
316
|
-
],
|
|
248
|
+
selection: { type: 'ids', ids: [4000000003], length: 1 },
|
|
249
|
+
fields: [{ id: 1002, action: { type: 'number_add', number_add: 10 } }],
|
|
317
250
|
})
|
|
318
|
-
|
|
319
251
|
const out = await executeCLI(
|
|
320
252
|
'user',
|
|
321
|
-
['
|
|
253
|
+
['row', 'bulk-update', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile],
|
|
322
254
|
env as any,
|
|
323
255
|
async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
|
|
324
256
|
)
|
|
@@ -327,69 +259,18 @@ test('bulk update accepts ids selection with number_add action', async () => {
|
|
|
327
259
|
assert.equal(out.data.fields[0].action.type, 'number_add')
|
|
328
260
|
})
|
|
329
261
|
|
|
330
|
-
test('selection
|
|
331
|
-
const bodyFile = tempJSONFile({
|
|
332
|
-
selection: {
|
|
333
|
-
type: 'condition',
|
|
334
|
-
length: 1,
|
|
335
|
-
condition: {
|
|
336
|
-
q: 'A-100',
|
|
337
|
-
selectAll: true,
|
|
338
|
-
},
|
|
339
|
-
sorts: [
|
|
340
|
-
{ column: ':1002', order: 'desc' },
|
|
341
|
-
],
|
|
342
|
-
},
|
|
343
|
-
})
|
|
344
|
-
|
|
345
|
-
const out = await executeCLI(
|
|
346
|
-
'user',
|
|
347
|
-
['rows', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile],
|
|
348
|
-
env as any,
|
|
349
|
-
async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
|
|
350
|
-
)
|
|
351
|
-
assert.equal(out.kind, 'result')
|
|
352
|
-
assert.equal(out.data.selection.type, 'condition')
|
|
353
|
-
assert.equal(out.data.selection.condition.selectAll, true)
|
|
354
|
-
})
|
|
355
|
-
|
|
356
|
-
test('selection action accepts condition selection with filter_* keys', async () => {
|
|
357
|
-
const bodyFile = tempJSONFile({
|
|
358
|
-
selection: {
|
|
359
|
-
type: 'condition',
|
|
360
|
-
length: 1,
|
|
361
|
-
condition: {
|
|
362
|
-
'filter_:1001': '%7B%22is%22%3A%22A-100%22%7D',
|
|
363
|
-
selectAll: true,
|
|
364
|
-
},
|
|
365
|
-
},
|
|
366
|
-
})
|
|
367
|
-
|
|
368
|
-
const out = await executeCLI(
|
|
369
|
-
'user',
|
|
370
|
-
['rows', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile],
|
|
371
|
-
env as any,
|
|
372
|
-
async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
|
|
373
|
-
)
|
|
374
|
-
assert.equal(out.kind, 'result')
|
|
375
|
-
assert.equal(out.data.selection.type, 'condition')
|
|
376
|
-
assert.equal(out.data.selection.condition.selectAll, true)
|
|
377
|
-
})
|
|
378
|
-
|
|
379
|
-
test('selection action accepts condition selection with selectAll only for batch_print', async () => {
|
|
262
|
+
test('row selection-action accepts condition selection with selectAll', async () => {
|
|
380
263
|
const bodyFile = tempJSONFile({
|
|
381
264
|
selection: {
|
|
382
265
|
type: 'condition',
|
|
383
266
|
length: 1,
|
|
384
|
-
condition: {
|
|
385
|
-
|
|
386
|
-
},
|
|
267
|
+
condition: { q: 'A-100', selectAll: true },
|
|
268
|
+
sorts: [{ column: ':1002', order: 'desc' }],
|
|
387
269
|
},
|
|
388
270
|
})
|
|
389
|
-
|
|
390
271
|
const out = await executeCLI(
|
|
391
272
|
'user',
|
|
392
|
-
['
|
|
273
|
+
['row', 'selection-action', '--app-id', 'app_1', '--table-id', 'table_1', '--action', 'archive', '--body-file', bodyFile],
|
|
393
274
|
env as any,
|
|
394
275
|
async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
|
|
395
276
|
)
|
|
@@ -398,64 +279,10 @@ test('selection action accepts condition selection with selectAll only for batch
|
|
|
398
279
|
assert.equal(out.data.selection.condition.selectAll, true)
|
|
399
280
|
})
|
|
400
281
|
|
|
401
|
-
test('selection
|
|
402
|
-
const bodyFile = tempJSONFile({
|
|
403
|
-
selection: {
|
|
404
|
-
type: 'all',
|
|
405
|
-
},
|
|
406
|
-
})
|
|
407
|
-
|
|
408
|
-
const out = await executeCLI(
|
|
409
|
-
'user',
|
|
410
|
-
['rows', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'query', '--body-file', bodyFile],
|
|
411
|
-
env as any,
|
|
412
|
-
async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
|
|
413
|
-
)
|
|
414
|
-
assert.equal(out.kind, 'result')
|
|
415
|
-
assert.equal(out.data.selection.type, 'all')
|
|
416
|
-
})
|
|
417
|
-
|
|
418
|
-
test('selection action rejects condition selection for query action', async () => {
|
|
419
|
-
const bodyFile = tempJSONFile({
|
|
420
|
-
selection: {
|
|
421
|
-
type: 'condition',
|
|
422
|
-
length: 1,
|
|
423
|
-
condition: {
|
|
424
|
-
q: 'A-100',
|
|
425
|
-
selectAll: true,
|
|
426
|
-
},
|
|
427
|
-
},
|
|
428
|
-
})
|
|
429
|
-
|
|
430
|
-
await assert.rejects(async () => {
|
|
431
|
-
await executeCLI(
|
|
432
|
-
'user',
|
|
433
|
-
['rows', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'query', '--body-file', bodyFile],
|
|
434
|
-
env as any,
|
|
435
|
-
async () => new Response('{}', { status: 200 }),
|
|
436
|
-
)
|
|
437
|
-
}, (error: unknown) => {
|
|
438
|
-
assert.ok(error instanceof CLIError)
|
|
439
|
-
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
440
|
-
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.selection.type'))
|
|
441
|
-
return true
|
|
442
|
-
})
|
|
443
|
-
})
|
|
444
|
-
|
|
445
|
-
test('selection action rejects all selection for archive action', async () => {
|
|
446
|
-
const bodyFile = tempJSONFile({
|
|
447
|
-
selection: {
|
|
448
|
-
type: 'all',
|
|
449
|
-
},
|
|
450
|
-
})
|
|
451
|
-
|
|
282
|
+
test('row selection-action rejects condition selection for query action', async () => {
|
|
283
|
+
const bodyFile = tempJSONFile({ selection: { type: 'condition', length: 1, condition: { q: 'A-100', selectAll: true } } })
|
|
452
284
|
await assert.rejects(async () => {
|
|
453
|
-
await executeCLI(
|
|
454
|
-
'user',
|
|
455
|
-
['rows', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile],
|
|
456
|
-
env as any,
|
|
457
|
-
async () => new Response('{}', { status: 200 }),
|
|
458
|
-
)
|
|
285
|
+
await executeCLI('user', ['row', 'selection-action', '--app-id', 'app_1', '--table-id', 'table_1', '--action', 'query', '--body-file', bodyFile], env as any, async () => new Response('{}', { status: 200 }))
|
|
459
286
|
}, (error: unknown) => {
|
|
460
287
|
assert.ok(error instanceof CLIError)
|
|
461
288
|
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
@@ -464,59 +291,18 @@ test('selection action rejects all selection for archive action', async () => {
|
|
|
464
291
|
})
|
|
465
292
|
})
|
|
466
293
|
|
|
467
|
-
test('
|
|
468
|
-
const bodyFile = tempJSONFile({
|
|
469
|
-
selection: {
|
|
470
|
-
type: 'condition',
|
|
471
|
-
length: 1,
|
|
472
|
-
condition: {
|
|
473
|
-
selectAll: true,
|
|
474
|
-
},
|
|
475
|
-
},
|
|
476
|
-
})
|
|
477
|
-
|
|
478
|
-
await assert.rejects(async () => {
|
|
479
|
-
await executeCLI(
|
|
480
|
-
'user',
|
|
481
|
-
['rows', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile],
|
|
482
|
-
env as any,
|
|
483
|
-
async () => new Response('{}', { status: 200 }),
|
|
484
|
-
)
|
|
485
|
-
}, (error: unknown) => {
|
|
486
|
-
assert.ok(error instanceof CLIError)
|
|
487
|
-
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
488
|
-
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.selection.condition'))
|
|
489
|
-
return true
|
|
490
|
-
})
|
|
491
|
-
})
|
|
492
|
-
|
|
493
|
-
test('insert entity rejects local file path for file field and points to arcubase upload', async () => {
|
|
494
|
-
const bodyFile = tempJSONFile({
|
|
495
|
-
fields: {
|
|
496
|
-
1001: 'Lead A',
|
|
497
|
-
1007: './contract.pdf',
|
|
498
|
-
},
|
|
499
|
-
})
|
|
294
|
+
test('row create rejects local file path for file field and points to arcubase upload', async () => {
|
|
295
|
+
const bodyFile = tempJSONFile({ fields: { 1001: 'Lead A', 1007: './contract.pdf' } })
|
|
500
296
|
const calls: Array<{ url: string; method?: string }> = []
|
|
501
|
-
|
|
502
297
|
await assert.rejects(async () => {
|
|
503
298
|
await executeCLI(
|
|
504
299
|
'user',
|
|
505
|
-
['
|
|
300
|
+
['row', 'create', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile],
|
|
506
301
|
env as any,
|
|
507
302
|
async (url, init) => {
|
|
508
303
|
calls.push({ url: String(url), method: init?.method })
|
|
509
|
-
if (String(url) === 'https://arcubase.example.com/entity/app_1/
|
|
510
|
-
return new Response(JSON.stringify({
|
|
511
|
-
data: {
|
|
512
|
-
id: 1,
|
|
513
|
-
app_id: 1,
|
|
514
|
-
fields: [
|
|
515
|
-
{ id: 1001, type: 'text', label: 'Lead Name' },
|
|
516
|
-
{ id: 1007, type: 'file', label: 'Contract File' },
|
|
517
|
-
],
|
|
518
|
-
},
|
|
519
|
-
}), { status: 200 })
|
|
304
|
+
if (String(url) === 'https://arcubase.example.com/api/entity/app_1/table_1' && init?.method === 'GET') {
|
|
305
|
+
return new Response(JSON.stringify({ data: { id: 1, app_id: 1, fields: [{ id: 1001, type: 'text', label: 'Lead Name' }, { id: 1007, type: 'file', label: 'Contract File' }] } }), { status: 200 })
|
|
520
306
|
}
|
|
521
307
|
return new Response('{}', { status: 200 })
|
|
522
308
|
},
|
|
@@ -526,44 +312,21 @@ test('insert entity rejects local file path for file field and points to arcubas
|
|
|
526
312
|
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
527
313
|
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.fields.1007'))
|
|
528
314
|
assert.match(error.message, /arcubase upload \.\/contract\.pdf/)
|
|
529
|
-
assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/uploads.md')))
|
|
530
315
|
return true
|
|
531
316
|
})
|
|
532
|
-
|
|
533
|
-
assert.deepEqual(calls, [
|
|
534
|
-
{ url: 'https://arcubase.example.com/entity/app_1/entity_1', method: 'GET' },
|
|
535
|
-
])
|
|
317
|
+
assert.deepEqual(calls, [{ url: 'https://arcubase.example.com/api/entity/app_1/table_1', method: 'GET' }])
|
|
536
318
|
})
|
|
537
319
|
|
|
538
|
-
test('update
|
|
539
|
-
const bodyFile = tempJSONFile({
|
|
540
|
-
changed_fields: [1008],
|
|
541
|
-
data: {
|
|
542
|
-
1008: {
|
|
543
|
-
upload_id: 123456,
|
|
544
|
-
file: 'lead-photo.jpg',
|
|
545
|
-
file_name: 'lead-photo.jpg',
|
|
546
|
-
meta: {},
|
|
547
|
-
},
|
|
548
|
-
},
|
|
549
|
-
})
|
|
550
|
-
|
|
320
|
+
test('row update rejects object value for image field and requires upload array', async () => {
|
|
321
|
+
const bodyFile = tempJSONFile({ changed_fields: [1008], data: { 1008: { upload_id: 123456, file: 'lead-photo.jpg', file_name: 'lead-photo.jpg', meta: {} } } })
|
|
551
322
|
await assert.rejects(async () => {
|
|
552
323
|
await executeCLI(
|
|
553
324
|
'user',
|
|
554
|
-
['
|
|
325
|
+
['row', 'update', '--app-id', 'app_1', '--table-id', 'table_1', '--row-id', 'row_1', '--body-file', bodyFile],
|
|
555
326
|
env as any,
|
|
556
327
|
async (url, init) => {
|
|
557
|
-
if (String(url) === 'https://arcubase.example.com/entity/app_1/
|
|
558
|
-
return new Response(JSON.stringify({
|
|
559
|
-
data: {
|
|
560
|
-
id: 1,
|
|
561
|
-
app_id: 1,
|
|
562
|
-
fields: [
|
|
563
|
-
{ id: 1008, type: 'image', label: 'Lead Photo' },
|
|
564
|
-
],
|
|
565
|
-
},
|
|
566
|
-
}), { status: 200 })
|
|
328
|
+
if (String(url) === 'https://arcubase.example.com/api/entity/app_1/table_1' && init?.method === 'GET') {
|
|
329
|
+
return new Response(JSON.stringify({ data: { id: 1, app_id: 1, fields: [{ id: 1008, type: 'image', label: 'Lead Photo' }] } }), { status: 200 })
|
|
567
330
|
}
|
|
568
331
|
return new Response('{}', { status: 200 })
|
|
569
332
|
},
|
|
@@ -573,88 +336,47 @@ test('update row rejects object value for image field and requires upload array'
|
|
|
573
336
|
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
574
337
|
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.data.1008'))
|
|
575
338
|
assert.match(error.message, /must be an array/)
|
|
576
|
-
assert.match(error.message, /arcubase upload <local-file>/)
|
|
577
339
|
return true
|
|
578
340
|
})
|
|
579
341
|
})
|
|
580
342
|
|
|
581
|
-
test('
|
|
582
|
-
const bodyFile = tempJSONFile({
|
|
583
|
-
fields: {
|
|
584
|
-
1001: 'Lead A',
|
|
585
|
-
1007: [
|
|
586
|
-
{
|
|
587
|
-
upload_id: 123456,
|
|
588
|
-
file: 'contract.pdf',
|
|
589
|
-
file_name: 'contract.pdf',
|
|
590
|
-
meta: {},
|
|
591
|
-
},
|
|
592
|
-
],
|
|
593
|
-
},
|
|
594
|
-
})
|
|
343
|
+
test('row create accepts upload array for file field after schema preflight', async () => {
|
|
344
|
+
const bodyFile = tempJSONFile({ fields: { 1001: 'Lead A', 1007: [{ upload_id: 123456, file: 'contract.pdf', file_name: 'contract.pdf', meta: {} }] } })
|
|
595
345
|
const calls: Array<{ url: string; method?: string }> = []
|
|
596
|
-
|
|
597
346
|
const out = await executeCLI(
|
|
598
347
|
'user',
|
|
599
|
-
['
|
|
348
|
+
['row', 'create', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile],
|
|
600
349
|
env as any,
|
|
601
350
|
async (url, init) => {
|
|
602
351
|
calls.push({ url: String(url), method: init?.method })
|
|
603
|
-
if (String(url) === 'https://arcubase.example.com/entity/app_1/
|
|
604
|
-
return new Response(JSON.stringify({
|
|
605
|
-
data: {
|
|
606
|
-
id: 1,
|
|
607
|
-
app_id: 1,
|
|
608
|
-
fields: [
|
|
609
|
-
{ id: 1001, type: 'text', label: 'Lead Name' },
|
|
610
|
-
{ id: 1007, type: 'file', label: 'Contract File' },
|
|
611
|
-
],
|
|
612
|
-
},
|
|
613
|
-
}), { status: 200 })
|
|
352
|
+
if (String(url) === 'https://arcubase.example.com/api/entity/app_1/table_1' && init?.method === 'GET') {
|
|
353
|
+
return new Response(JSON.stringify({ data: { id: 1, app_id: 1, fields: [{ id: 1001, type: 'text', label: 'Lead Name' }, { id: 1007, type: 'file', label: 'Contract File' }] } }), { status: 200 })
|
|
614
354
|
}
|
|
615
355
|
return new Response(String(init?.body ?? '{}'), { status: 200 })
|
|
616
356
|
},
|
|
617
357
|
)
|
|
618
|
-
|
|
619
358
|
assert.equal(out.kind, 'result')
|
|
620
359
|
assert.deepEqual(calls, [
|
|
621
|
-
{ url: 'https://arcubase.example.com/entity/app_1/
|
|
622
|
-
{ url: 'https://arcubase.example.com/entity/app_1/
|
|
360
|
+
{ url: 'https://arcubase.example.com/api/entity/app_1/table_1', method: 'GET' },
|
|
361
|
+
{ url: 'https://arcubase.example.com/api/entity/app_1/table_1/insert', method: 'POST' },
|
|
623
362
|
])
|
|
624
363
|
assert.equal(out.data.fields['1007'][0].upload_id, 123456)
|
|
625
364
|
})
|
|
626
365
|
|
|
627
|
-
test('
|
|
628
|
-
const bodyFile = tempJSONFile({
|
|
629
|
-
fields: {
|
|
630
|
-
1001: 'Lead A',
|
|
631
|
-
1007: [
|
|
632
|
-
{
|
|
633
|
-
upload_id: 123456,
|
|
634
|
-
file: 'contract.pdf',
|
|
635
|
-
file_name: 'contract.pdf',
|
|
636
|
-
meta: {},
|
|
637
|
-
},
|
|
638
|
-
],
|
|
639
|
-
},
|
|
640
|
-
})
|
|
641
|
-
|
|
366
|
+
test('row create decorates upload bind failures with actionable guidance', async () => {
|
|
367
|
+
const bodyFile = tempJSONFile({ fields: { 1001: 'Lead A', 1007: [{ upload_id: 123456, file: 'contract.pdf', file_name: 'contract.pdf', meta: {} }] } })
|
|
642
368
|
await assert.rejects(async () => {
|
|
643
369
|
await executeCLI(
|
|
644
370
|
'user',
|
|
645
|
-
['
|
|
371
|
+
['row', 'create', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile],
|
|
646
372
|
env as any,
|
|
647
373
|
async (url, init) => {
|
|
648
|
-
if (String(url) === 'https://arcubase.example.com/entity/app_1/
|
|
649
|
-
return new Response(JSON.stringify({
|
|
650
|
-
data: {
|
|
651
|
-
fields: [{ id: 1007, type: 'file', label: 'Contract File' }],
|
|
652
|
-
},
|
|
653
|
-
}), { status: 200 })
|
|
374
|
+
if (String(url) === 'https://arcubase.example.com/api/entity/app_1/table_1' && init?.method === 'GET') {
|
|
375
|
+
return new Response(JSON.stringify({ data: { fields: [{ id: 1007, type: 'file', label: 'Contract File' }] } }), { status: 200 })
|
|
654
376
|
}
|
|
655
377
|
return new Response(JSON.stringify({
|
|
656
378
|
error: {
|
|
657
|
-
message: 'failed to bind upload to assets: Head
|
|
379
|
+
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',
|
|
658
380
|
type: 'system',
|
|
659
381
|
key: 'error.system',
|
|
660
382
|
},
|
|
@@ -668,239 +390,150 @@ test('insert entity decorates upload bind failures with actionable guidance', as
|
|
|
668
390
|
assert.equal(error.details?.traceId, 'req_upload_bind')
|
|
669
391
|
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')
|
|
670
392
|
assert.match(error.details?.hint ?? '', /rerun arcubase upload/)
|
|
671
|
-
assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/uploads.md')))
|
|
672
393
|
return true
|
|
673
394
|
})
|
|
674
395
|
})
|
|
675
396
|
|
|
676
|
-
test('
|
|
677
|
-
const bodyFile = tempJSONFile({
|
|
678
|
-
selection: {
|
|
679
|
-
type: 'condition',
|
|
680
|
-
length: 1,
|
|
681
|
-
condition: {
|
|
682
|
-
q: 'A-100',
|
|
683
|
-
},
|
|
684
|
-
},
|
|
685
|
-
})
|
|
686
|
-
|
|
397
|
+
test('row query rejects camelCase viewMode and points to row crud doc', async () => {
|
|
398
|
+
const bodyFile = tempJSONFile({ limit: 20, offset: 0, viewMode: 'grid' })
|
|
687
399
|
await assert.rejects(async () => {
|
|
688
|
-
await executeCLI(
|
|
689
|
-
'user',
|
|
690
|
-
['rows', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile],
|
|
691
|
-
env as any,
|
|
692
|
-
async () => new Response('{}', { status: 200 }),
|
|
693
|
-
)
|
|
400
|
+
await executeCLI('user', ['row', 'query', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile], env as any, async () => new Response('{}', { status: 200 }))
|
|
694
401
|
}, (error: unknown) => {
|
|
695
402
|
assert.ok(error instanceof CLIError)
|
|
696
403
|
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
697
|
-
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.
|
|
404
|
+
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.viewMode'))
|
|
698
405
|
return true
|
|
699
406
|
})
|
|
700
407
|
})
|
|
701
408
|
|
|
702
|
-
test('
|
|
703
|
-
const bodyFile = tempJSONFile({
|
|
704
|
-
selection: {
|
|
705
|
-
type: 'condition',
|
|
706
|
-
length: 1,
|
|
707
|
-
condition: {
|
|
708
|
-
text_search: 'A-100',
|
|
709
|
-
selectAll: true,
|
|
710
|
-
},
|
|
711
|
-
},
|
|
712
|
-
})
|
|
713
|
-
|
|
409
|
+
test('row update rejects fields key', async () => {
|
|
410
|
+
const bodyFile = tempJSONFile({ fields: { 1002: 99 }, data: { '1002': 99 }, changed_fields: [1002] })
|
|
714
411
|
await assert.rejects(async () => {
|
|
715
|
-
await executeCLI(
|
|
716
|
-
'user',
|
|
717
|
-
['rows', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile],
|
|
718
|
-
env as any,
|
|
719
|
-
async () => new Response('{}', { status: 200 }),
|
|
720
|
-
)
|
|
412
|
+
await executeCLI('user', ['row', 'update', '--app-id', 'app_1', '--table-id', 'table_1', '--row-id', 'row_1', '--body-file', bodyFile], env as any, async () => new Response('{}', { status: 200 }))
|
|
721
413
|
}, (error: unknown) => {
|
|
722
414
|
assert.ok(error instanceof CLIError)
|
|
723
415
|
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
724
|
-
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.
|
|
725
|
-
assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/search-and-bulk-actions.md')))
|
|
416
|
+
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.fields'))
|
|
726
417
|
return true
|
|
727
418
|
})
|
|
728
419
|
})
|
|
729
420
|
|
|
730
|
-
test('
|
|
731
|
-
const bodyFile = tempJSONFile({
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
sorts: [
|
|
740
|
-
{ column: ':1002', order: 'desc' },
|
|
741
|
-
],
|
|
742
|
-
},
|
|
743
|
-
fields: [
|
|
744
|
-
{
|
|
745
|
-
id: 1002,
|
|
746
|
-
action: {
|
|
747
|
-
type: 'number_add',
|
|
748
|
-
number_add: 2,
|
|
749
|
-
},
|
|
750
|
-
},
|
|
751
|
-
],
|
|
421
|
+
test('row update rejects changed_fields that are missing from data', async () => {
|
|
422
|
+
const bodyFile = tempJSONFile({ data: { '1002': 99 }, changed_fields: [1003] })
|
|
423
|
+
await assert.rejects(async () => {
|
|
424
|
+
await executeCLI('user', ['row', 'update', '--app-id', 'app_1', '--table-id', 'table_1', '--row-id', 'row_1', '--body-file', bodyFile], env as any, async () => new Response('{}', { status: 200 }))
|
|
425
|
+
}, (error: unknown) => {
|
|
426
|
+
assert.ok(error instanceof CLIError)
|
|
427
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
428
|
+
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.changed_fields.0'))
|
|
429
|
+
return true
|
|
752
430
|
})
|
|
753
|
-
|
|
754
|
-
const out = await executeCLI(
|
|
755
|
-
'user',
|
|
756
|
-
['entity', 'update-entity-bulk', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile],
|
|
757
|
-
env as any,
|
|
758
|
-
async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
|
|
759
|
-
)
|
|
760
|
-
assert.equal(out.kind, 'result')
|
|
761
|
-
assert.equal(out.data.selection.type, 'condition')
|
|
762
|
-
assert.equal(out.data.fields[0].action.type, 'number_add')
|
|
763
431
|
})
|
|
764
432
|
|
|
765
|
-
test('
|
|
766
|
-
const bodyFile = tempJSONFile({
|
|
767
|
-
policy_id: '',
|
|
768
|
-
selection: {
|
|
769
|
-
type: 'ids',
|
|
770
|
-
ids: [4000000003],
|
|
771
|
-
length: 1,
|
|
772
|
-
},
|
|
773
|
-
})
|
|
774
|
-
|
|
433
|
+
test('workflow approve maps to fixed action path', async () => {
|
|
434
|
+
const bodyFile = tempJSONFile({ task_id: 1001, fields: {}, changed: [] })
|
|
775
435
|
const out = await executeCLI(
|
|
776
|
-
'
|
|
777
|
-
['
|
|
436
|
+
'user',
|
|
437
|
+
['workflow', 'approve', '--app-id', 'app_1', '--table-id', 'table_1', '--row-id', 'row_1', '--body-file', bodyFile],
|
|
778
438
|
env as any,
|
|
779
|
-
async (
|
|
439
|
+
async (url, init) => new Response(JSON.stringify({ url, method: init?.method, body: init?.body ? JSON.parse(String(init.body)) : null }), { status: 200 }),
|
|
780
440
|
)
|
|
781
441
|
assert.equal(out.kind, 'result')
|
|
782
|
-
assert.equal(out.data.
|
|
442
|
+
assert.equal(out.data.url, 'https://arcubase.example.com/api/entity/app_1/table_1/row/row_1/action/2')
|
|
443
|
+
assert.equal(out.data.body.task_id, 1001)
|
|
783
444
|
})
|
|
784
445
|
|
|
785
|
-
test('
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
q: 'A-100',
|
|
793
|
-
selectAll: true,
|
|
794
|
-
},
|
|
795
|
-
},
|
|
796
|
-
tag_names: ['tag-a'],
|
|
446
|
+
test('workflow query-approve-users requires request body', async () => {
|
|
447
|
+
await assert.rejects(async () => {
|
|
448
|
+
await executeCLI('user', ['workflow', 'query-approve-users', '--app-id', 'app_1', '--table-id', 'table_1'], env as any, async () => new Response('{}', { status: 200 }))
|
|
449
|
+
}, (error: unknown) => {
|
|
450
|
+
assert.ok(error instanceof CLIError)
|
|
451
|
+
assert.equal(error.code, 'MISSING_BODY_FILE')
|
|
452
|
+
return true
|
|
797
453
|
})
|
|
798
|
-
|
|
799
|
-
const out = await executeCLI(
|
|
800
|
-
'admin',
|
|
801
|
-
['entity-tags', 'batch-add-entity-tags', '--appId', 'app_1', '--entityId', 'entity_1', '--body-file', bodyFile],
|
|
802
|
-
env as any,
|
|
803
|
-
async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
|
|
804
|
-
)
|
|
805
|
-
assert.equal(out.kind, 'result')
|
|
806
|
-
assert.equal(out.data.tag_names[0], 'tag-a')
|
|
807
454
|
})
|
|
808
455
|
|
|
809
|
-
test('
|
|
810
|
-
const bodyFile = tempJSONFile({
|
|
811
|
-
id: 'event_1',
|
|
812
|
-
data: {
|
|
813
|
-
action_id: 'action_1',
|
|
814
|
-
policy_id: '',
|
|
815
|
-
selection: {
|
|
816
|
-
type: 'all',
|
|
817
|
-
},
|
|
818
|
-
},
|
|
819
|
-
params: {},
|
|
820
|
-
})
|
|
821
|
-
|
|
456
|
+
test('entry passes include_app_tables query flag', async () => {
|
|
822
457
|
const out = await executeCLI(
|
|
823
458
|
'user',
|
|
824
|
-
['
|
|
459
|
+
['entry', '--include-app-tables', 'true'],
|
|
825
460
|
env as any,
|
|
826
|
-
async (
|
|
461
|
+
async (url, init) => new Response(JSON.stringify({ url, method: init?.method }), { status: 200 }),
|
|
827
462
|
)
|
|
828
463
|
assert.equal(out.kind, 'result')
|
|
829
|
-
assert.equal(out.data.
|
|
830
|
-
assert.equal(out.data.data.action_id, 'action_1')
|
|
831
|
-
assert.equal(out.data.data.selection.type, 'all')
|
|
464
|
+
assert.equal(out.data.url, 'https://arcubase.example.com/api/entry?include_app_tables=true')
|
|
832
465
|
})
|
|
833
466
|
|
|
834
|
-
test('
|
|
835
|
-
const bodyFile = tempJSONFile({
|
|
836
|
-
limit: 20,
|
|
837
|
-
offset: 0,
|
|
838
|
-
viewMode: 'grid',
|
|
839
|
-
})
|
|
840
|
-
|
|
467
|
+
test('access-rule update rejects enabled without update whitelist', async () => {
|
|
841
468
|
await assert.rejects(async () => {
|
|
842
469
|
await executeCLI(
|
|
843
|
-
'
|
|
844
|
-
['
|
|
470
|
+
'admin',
|
|
471
|
+
['access-rule', 'update', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"enabled":true}'],
|
|
845
472
|
env as any,
|
|
846
473
|
async () => new Response('{}', { status: 200 }),
|
|
847
474
|
)
|
|
848
475
|
}, (error: unknown) => {
|
|
849
476
|
assert.ok(error instanceof CLIError)
|
|
850
477
|
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
851
|
-
assert.
|
|
852
|
-
assert.
|
|
478
|
+
assert.match(error.message, /update.*enabled/)
|
|
479
|
+
assert.equal(error.details?.requestType, 'AppIngressUpdateReqVO')
|
|
480
|
+
assert.ok((error.details?.tsHints ?? []).some((item) => item.file === '/opt/arcubase-sdk/types/ingress.ts'))
|
|
853
481
|
return true
|
|
854
482
|
})
|
|
855
483
|
})
|
|
856
484
|
|
|
857
|
-
test('update
|
|
858
|
-
const
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
485
|
+
test('access-rule update accepts enabled with update whitelist', async () => {
|
|
486
|
+
const out = await executeCLI(
|
|
487
|
+
'admin',
|
|
488
|
+
['access-rule', 'update', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"update":["enabled"],"enabled":true}'],
|
|
489
|
+
env as any,
|
|
490
|
+
async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
|
|
491
|
+
)
|
|
492
|
+
assert.equal(out.kind, 'result')
|
|
493
|
+
assert.deepEqual(out.data, { update: ['enabled'], enabled: true })
|
|
494
|
+
})
|
|
867
495
|
|
|
496
|
+
test('access-rule assign-users rejects users shortcut', async () => {
|
|
868
497
|
await assert.rejects(async () => {
|
|
869
498
|
await executeCLI(
|
|
870
|
-
'
|
|
871
|
-
['
|
|
499
|
+
'admin',
|
|
500
|
+
['access-rule', 'assign-users', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"users":["tu_1"]}'],
|
|
872
501
|
env as any,
|
|
873
502
|
async () => new Response('{}', { status: 200 }),
|
|
874
503
|
)
|
|
875
504
|
}, (error: unknown) => {
|
|
876
505
|
assert.ok(error instanceof CLIError)
|
|
877
506
|
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
878
|
-
assert.
|
|
879
|
-
assert.
|
|
507
|
+
assert.match(error.message, /options.user_scope/)
|
|
508
|
+
assert.equal(error.details?.requestType, 'AppIngressUpdateReqVO')
|
|
880
509
|
return true
|
|
881
510
|
})
|
|
882
511
|
})
|
|
883
512
|
|
|
884
|
-
test('
|
|
885
|
-
const bodyFile = tempJSONFile({
|
|
886
|
-
data: {
|
|
887
|
-
'1002': 99,
|
|
888
|
-
},
|
|
889
|
-
changed_fields: [1003],
|
|
890
|
-
})
|
|
891
|
-
|
|
513
|
+
test('access-rule assign-users requires update user_scope and non-empty user_scope', async () => {
|
|
892
514
|
await assert.rejects(async () => {
|
|
893
515
|
await executeCLI(
|
|
894
|
-
'
|
|
895
|
-
['
|
|
516
|
+
'admin',
|
|
517
|
+
['access-rule', 'assign-users', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"update":["user_scope"],"options":{"user_scope":[]}}'],
|
|
896
518
|
env as any,
|
|
897
519
|
async () => new Response('{}', { status: 200 }),
|
|
898
520
|
)
|
|
899
521
|
}, (error: unknown) => {
|
|
900
522
|
assert.ok(error instanceof CLIError)
|
|
901
523
|
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
902
|
-
assert.
|
|
903
|
-
assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/row-crud.md')))
|
|
524
|
+
assert.match(error.message, /non-empty/)
|
|
904
525
|
return true
|
|
905
526
|
})
|
|
906
527
|
})
|
|
528
|
+
|
|
529
|
+
test('access-rule assign-users accepts canonical user_scope body', async () => {
|
|
530
|
+
const out = await executeCLI(
|
|
531
|
+
'admin',
|
|
532
|
+
['access-rule', 'assign-users', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"update":["user_scope"],"options":{"user_scope":[{"type":"user","id":123}]}}'],
|
|
533
|
+
env as any,
|
|
534
|
+
async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
|
|
535
|
+
)
|
|
536
|
+
assert.equal(out.kind, 'result')
|
|
537
|
+
assert.deepEqual(out.data.update, ['user_scope'])
|
|
538
|
+
assert.deepEqual(out.data.options.user_scope, [{ type: 'user', id: 123 }])
|
|
539
|
+
})
|