@carthooks/arcubase-cli 0.1.7 → 0.1.8
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 +58 -168
- 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 +199 -640
- package/src/tests/help.test.ts +36 -69
- 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
|
+
})
|
|
83
148
|
})
|
|
84
149
|
|
|
85
|
-
test('
|
|
86
|
-
|
|
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
|
+
})
|
|
158
|
+
})
|
|
159
|
+
|
|
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,24 +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
|
|
262
|
+
test('row selection-action accepts condition selection with selectAll', async () => {
|
|
331
263
|
const bodyFile = tempJSONFile({
|
|
332
264
|
selection: {
|
|
333
265
|
type: 'condition',
|
|
334
266
|
length: 1,
|
|
335
|
-
condition: {
|
|
336
|
-
|
|
337
|
-
selectAll: true,
|
|
338
|
-
},
|
|
339
|
-
sorts: [
|
|
340
|
-
{ column: ':1002', order: 'desc' },
|
|
341
|
-
],
|
|
267
|
+
condition: { q: 'A-100', selectAll: true },
|
|
268
|
+
sorts: [{ column: ':1002', order: 'desc' }],
|
|
342
269
|
},
|
|
343
270
|
})
|
|
344
|
-
|
|
345
271
|
const out = await executeCLI(
|
|
346
272
|
'user',
|
|
347
|
-
['
|
|
273
|
+
['row', 'selection-action', '--app-id', 'app_1', '--table-id', 'table_1', '--action', 'archive', '--body-file', bodyFile],
|
|
348
274
|
env as any,
|
|
349
275
|
async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
|
|
350
276
|
)
|
|
@@ -353,87 +279,10 @@ test('selection action accepts condition selection with selectAll', async () =>
|
|
|
353
279
|
assert.equal(out.data.selection.condition.selectAll, true)
|
|
354
280
|
})
|
|
355
281
|
|
|
356
|
-
test('selection
|
|
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 () => {
|
|
380
|
-
const bodyFile = tempJSONFile({
|
|
381
|
-
selection: {
|
|
382
|
-
type: 'condition',
|
|
383
|
-
length: 1,
|
|
384
|
-
condition: {
|
|
385
|
-
selectAll: true,
|
|
386
|
-
},
|
|
387
|
-
},
|
|
388
|
-
})
|
|
389
|
-
|
|
390
|
-
const out = await executeCLI(
|
|
391
|
-
'user',
|
|
392
|
-
['rows', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'batch_print', '--body-file', bodyFile],
|
|
393
|
-
env as any,
|
|
394
|
-
async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
|
|
395
|
-
)
|
|
396
|
-
assert.equal(out.kind, 'result')
|
|
397
|
-
assert.equal(out.data.selection.type, 'condition')
|
|
398
|
-
assert.equal(out.data.selection.condition.selectAll, true)
|
|
399
|
-
})
|
|
400
|
-
|
|
401
|
-
test('selection action accepts all selection for query-style actions', async () => {
|
|
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
|
-
|
|
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 } } })
|
|
430
284
|
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
|
-
)
|
|
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 }))
|
|
437
286
|
}, (error: unknown) => {
|
|
438
287
|
assert.ok(error instanceof CLIError)
|
|
439
288
|
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
@@ -442,81 +291,18 @@ test('selection action rejects condition selection for query action', async () =
|
|
|
442
291
|
})
|
|
443
292
|
})
|
|
444
293
|
|
|
445
|
-
test('
|
|
446
|
-
const bodyFile = tempJSONFile({
|
|
447
|
-
selection: {
|
|
448
|
-
type: 'all',
|
|
449
|
-
},
|
|
450
|
-
})
|
|
451
|
-
|
|
452
|
-
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
|
-
)
|
|
459
|
-
}, (error: unknown) => {
|
|
460
|
-
assert.ok(error instanceof CLIError)
|
|
461
|
-
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
462
|
-
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.selection.type'))
|
|
463
|
-
return true
|
|
464
|
-
})
|
|
465
|
-
})
|
|
466
|
-
|
|
467
|
-
test('selection action rejects selectAll-only condition for archive action', async () => {
|
|
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,76 @@ 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
|
-
],
|
|
752
|
-
})
|
|
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
|
-
})
|
|
764
|
-
|
|
765
|
-
test('entity tags get-selection-tags accepts ids selection', async () => {
|
|
766
|
-
const bodyFile = tempJSONFile({
|
|
767
|
-
policy_id: '',
|
|
768
|
-
selection: {
|
|
769
|
-
type: 'ids',
|
|
770
|
-
ids: [4000000003],
|
|
771
|
-
length: 1,
|
|
772
|
-
},
|
|
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
|
|
773
430
|
})
|
|
774
|
-
|
|
775
|
-
const out = await executeCLI(
|
|
776
|
-
'admin',
|
|
777
|
-
['entity-tags', 'get-selection-tags', '--appId', 'app_1', '--entityId', 'entity_1', '--body-file', bodyFile],
|
|
778
|
-
env as any,
|
|
779
|
-
async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
|
|
780
|
-
)
|
|
781
|
-
assert.equal(out.kind, 'result')
|
|
782
|
-
assert.equal(out.data.selection.type, 'ids')
|
|
783
431
|
})
|
|
784
432
|
|
|
785
|
-
test('
|
|
786
|
-
const bodyFile = tempJSONFile({
|
|
787
|
-
policy_id: '',
|
|
788
|
-
selection: {
|
|
789
|
-
type: 'condition',
|
|
790
|
-
length: 1,
|
|
791
|
-
condition: {
|
|
792
|
-
q: 'A-100',
|
|
793
|
-
selectAll: true,
|
|
794
|
-
},
|
|
795
|
-
},
|
|
796
|
-
tag_names: ['tag-a'],
|
|
797
|
-
})
|
|
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
|
-
})
|
|
808
|
-
|
|
809
|
-
test('invoke batch operator accepts backend request shape', async () => {
|
|
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
|
-
|
|
433
|
+
test('workflow approve maps to fixed action path', async () => {
|
|
434
|
+
const bodyFile = tempJSONFile({ task_id: 1001, fields: {}, changed: [] })
|
|
822
435
|
const out = await executeCLI(
|
|
823
436
|
'user',
|
|
824
|
-
['
|
|
437
|
+
['workflow', 'approve', '--app-id', 'app_1', '--table-id', 'table_1', '--row-id', 'row_1', '--body-file', bodyFile],
|
|
825
438
|
env as any,
|
|
826
|
-
async (
|
|
439
|
+
async (url, init) => new Response(JSON.stringify({ url, method: init?.method, body: init?.body ? JSON.parse(String(init.body)) : null }), { status: 200 }),
|
|
827
440
|
)
|
|
828
441
|
assert.equal(out.kind, 'result')
|
|
829
|
-
assert.equal(out.data.
|
|
830
|
-
assert.equal(out.data.
|
|
831
|
-
assert.equal(out.data.data.selection.type, 'all')
|
|
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)
|
|
832
444
|
})
|
|
833
445
|
|
|
834
|
-
test('query
|
|
835
|
-
const bodyFile = tempJSONFile({
|
|
836
|
-
limit: 20,
|
|
837
|
-
offset: 0,
|
|
838
|
-
viewMode: 'grid',
|
|
839
|
-
})
|
|
840
|
-
|
|
841
|
-
await assert.rejects(async () => {
|
|
842
|
-
await executeCLI(
|
|
843
|
-
'user',
|
|
844
|
-
['rows', 'query-entity', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile],
|
|
845
|
-
env as any,
|
|
846
|
-
async () => new Response('{}', { status: 200 }),
|
|
847
|
-
)
|
|
848
|
-
}, (error: unknown) => {
|
|
849
|
-
assert.ok(error instanceof CLIError)
|
|
850
|
-
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
851
|
-
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.viewMode'))
|
|
852
|
-
assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/row-crud.md')))
|
|
853
|
-
return true
|
|
854
|
-
})
|
|
855
|
-
})
|
|
856
|
-
|
|
857
|
-
test('update entity row rejects fields key', async () => {
|
|
858
|
-
const bodyFile = tempJSONFile({
|
|
859
|
-
fields: {
|
|
860
|
-
1002: 99,
|
|
861
|
-
},
|
|
862
|
-
data: {
|
|
863
|
-
'1002': 99,
|
|
864
|
-
},
|
|
865
|
-
changed_fields: [1002],
|
|
866
|
-
})
|
|
867
|
-
|
|
446
|
+
test('workflow query-approve-users requires request body', async () => {
|
|
868
447
|
await assert.rejects(async () => {
|
|
869
|
-
await executeCLI(
|
|
870
|
-
'user',
|
|
871
|
-
['rows', 'update-entity-row', '--app-id', 'app_1', '--entity-id', 'entity_1', '--row-id', 'row_1', '--body-file', bodyFile],
|
|
872
|
-
env as any,
|
|
873
|
-
async () => new Response('{}', { status: 200 }),
|
|
874
|
-
)
|
|
448
|
+
await executeCLI('user', ['workflow', 'query-approve-users', '--app-id', 'app_1', '--table-id', 'table_1'], env as any, async () => new Response('{}', { status: 200 }))
|
|
875
449
|
}, (error: unknown) => {
|
|
876
450
|
assert.ok(error instanceof CLIError)
|
|
877
|
-
assert.equal(error.code, '
|
|
878
|
-
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.fields'))
|
|
879
|
-
assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/row-crud.md')))
|
|
451
|
+
assert.equal(error.code, 'MISSING_BODY_FILE')
|
|
880
452
|
return true
|
|
881
453
|
})
|
|
882
454
|
})
|
|
883
455
|
|
|
884
|
-
test('
|
|
885
|
-
const
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
await executeCLI(
|
|
894
|
-
'user',
|
|
895
|
-
['rows', 'update-entity-row', '--app-id', 'app_1', '--entity-id', 'entity_1', '--row-id', 'row_1', '--body-file', bodyFile],
|
|
896
|
-
env as any,
|
|
897
|
-
async () => new Response('{}', { status: 200 }),
|
|
898
|
-
)
|
|
899
|
-
}, (error: unknown) => {
|
|
900
|
-
assert.ok(error instanceof CLIError)
|
|
901
|
-
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
902
|
-
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.changed_fields.0'))
|
|
903
|
-
assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/row-crud.md')))
|
|
904
|
-
return true
|
|
905
|
-
})
|
|
456
|
+
test('entry passes include_app_tables query flag', async () => {
|
|
457
|
+
const out = await executeCLI(
|
|
458
|
+
'user',
|
|
459
|
+
['entry', '--include-app-tables', 'true'],
|
|
460
|
+
env as any,
|
|
461
|
+
async (url, init) => new Response(JSON.stringify({ url, method: init?.method }), { status: 200 }),
|
|
462
|
+
)
|
|
463
|
+
assert.equal(out.kind, 'result')
|
|
464
|
+
assert.equal(out.data.url, 'https://arcubase.example.com/api/entry?include_app_tables=true')
|
|
906
465
|
})
|