@carthooks/arcubase-cli 0.1.19 → 0.1.20
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 +403 -9
- package/bundle/arcubase.mjs +403 -9
- package/dist/generated/help_examples.generated.d.ts +205 -0
- package/dist/generated/help_examples.generated.d.ts.map +1 -0
- package/dist/generated/help_examples.generated.js +282 -0
- package/dist/runtime/execute.d.ts.map +1 -1
- package/dist/runtime/execute.js +66 -1
- package/dist/runtime/help_examples.d.ts +10 -0
- package/dist/runtime/help_examples.d.ts.map +1 -0
- package/dist/runtime/help_examples.js +48 -0
- package/package.json +4 -3
- package/sdk-dist/docs/runtime-reference/help-examples/admin/access-rule/assign-users.json +23 -0
- package/sdk-dist/docs/runtime-reference/help-examples/admin/access-rule/create.json +95 -0
- package/sdk-dist/docs/runtime-reference/help-examples/user/row/bulk-update.json +23 -0
- package/sdk-dist/docs/runtime-reference/help-examples/user/row/selection-action.json +25 -0
- package/sdk-dist/generated/help_examples.generated.ts +289 -0
- package/src/generated/help_examples.generated.ts +289 -0
- package/src/runtime/execute.ts +79 -1
- package/src/runtime/help_examples.ts +61 -0
- package/src/tests/execute_validation.test.ts +27 -0
- package/src/tests/help.test.ts +32 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// Generated by scripts/generate-help-examples.mjs. Do not edit by hand.
|
|
2
|
+
|
|
3
|
+
export type GeneratedHelpExample = {
|
|
4
|
+
title: string
|
|
5
|
+
body?: unknown
|
|
6
|
+
notes?: readonly string[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const helpExamplesIndex = {
|
|
10
|
+
"admin.access-rule.assign-users": {
|
|
11
|
+
"command": [
|
|
12
|
+
"access-rule",
|
|
13
|
+
"assign-users"
|
|
14
|
+
],
|
|
15
|
+
"examples": [
|
|
16
|
+
{
|
|
17
|
+
"title": "replace user_scope with user, department, and actor scopes",
|
|
18
|
+
"body": {
|
|
19
|
+
"update": [
|
|
20
|
+
"user_scope"
|
|
21
|
+
],
|
|
22
|
+
"options": {
|
|
23
|
+
"user_scope": [
|
|
24
|
+
{
|
|
25
|
+
"type": "user",
|
|
26
|
+
"id": 2188889901
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"type": "department",
|
|
30
|
+
"id": 2188881201
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"type": "actor",
|
|
34
|
+
"id": 2188883301
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"notes": [
|
|
40
|
+
"id must be numeric arcubaseUserId, department ID, or actor ID",
|
|
41
|
+
"assign-users updates user_scope only and preserves the existing policy"
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
"admin.access-rule.create": {
|
|
47
|
+
"command": [
|
|
48
|
+
"access-rule",
|
|
49
|
+
"create"
|
|
50
|
+
],
|
|
51
|
+
"examples": [
|
|
52
|
+
{
|
|
53
|
+
"title": "full access for a specific arcubaseUserId",
|
|
54
|
+
"body": {
|
|
55
|
+
"name": "Admin full access",
|
|
56
|
+
"enabled": true,
|
|
57
|
+
"type": "form",
|
|
58
|
+
"options": {
|
|
59
|
+
"user_scope": [
|
|
60
|
+
{
|
|
61
|
+
"type": "user",
|
|
62
|
+
"id": 2188889977
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
"policy": {
|
|
66
|
+
"key": "custom",
|
|
67
|
+
"actions": [
|
|
68
|
+
"view",
|
|
69
|
+
"add",
|
|
70
|
+
"edit",
|
|
71
|
+
"delete"
|
|
72
|
+
],
|
|
73
|
+
"all_fields_read": true,
|
|
74
|
+
"all_fields_write": true
|
|
75
|
+
},
|
|
76
|
+
"list_options": {}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"title": "department can view/add/edit only rows created by themselves",
|
|
82
|
+
"body": {
|
|
83
|
+
"name": "Sales own rows",
|
|
84
|
+
"enabled": true,
|
|
85
|
+
"type": "form",
|
|
86
|
+
"options": {
|
|
87
|
+
"user_scope": [
|
|
88
|
+
{
|
|
89
|
+
"type": "department",
|
|
90
|
+
"id": 2188881201
|
|
91
|
+
}
|
|
92
|
+
],
|
|
93
|
+
"policy": {
|
|
94
|
+
"key": "custom",
|
|
95
|
+
"actions": [
|
|
96
|
+
"view",
|
|
97
|
+
"add",
|
|
98
|
+
"edit"
|
|
99
|
+
],
|
|
100
|
+
"all_fields_read": true,
|
|
101
|
+
"all_fields_write": true,
|
|
102
|
+
"condition": {
|
|
103
|
+
"mode": "and",
|
|
104
|
+
"conditions": [
|
|
105
|
+
{
|
|
106
|
+
"column": "creator",
|
|
107
|
+
"operator": "$eq",
|
|
108
|
+
"value": "[[current_user]]",
|
|
109
|
+
"value_from": 0,
|
|
110
|
+
"value_relation": ""
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
"list_options": {}
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
"notes": [
|
|
119
|
+
"department scope includes child departments",
|
|
120
|
+
"row-owner isolation uses [[current_user]] on creator"
|
|
121
|
+
]
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"title": "role actor can view rows matching a complex AND/OR condition",
|
|
125
|
+
"body": {
|
|
126
|
+
"name": "Sales manager review queue",
|
|
127
|
+
"enabled": true,
|
|
128
|
+
"type": "form",
|
|
129
|
+
"options": {
|
|
130
|
+
"user_scope": [
|
|
131
|
+
{
|
|
132
|
+
"type": "actor",
|
|
133
|
+
"id": 2188883301
|
|
134
|
+
}
|
|
135
|
+
],
|
|
136
|
+
"policy": {
|
|
137
|
+
"key": "custom",
|
|
138
|
+
"actions": [
|
|
139
|
+
"view",
|
|
140
|
+
"edit"
|
|
141
|
+
],
|
|
142
|
+
"all_fields_read": true,
|
|
143
|
+
"all_fields_write": true,
|
|
144
|
+
"condition": {
|
|
145
|
+
"mode": "and",
|
|
146
|
+
"conditions": [
|
|
147
|
+
{
|
|
148
|
+
"column": ":1004",
|
|
149
|
+
"operator": "$eq",
|
|
150
|
+
"value": "pending",
|
|
151
|
+
"value_from": 0,
|
|
152
|
+
"value_relation": ""
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"column": "",
|
|
156
|
+
"operator": "",
|
|
157
|
+
"value": null,
|
|
158
|
+
"value_from": 0,
|
|
159
|
+
"value_relation": "",
|
|
160
|
+
"subset": {
|
|
161
|
+
"mode": "or",
|
|
162
|
+
"conditions": [
|
|
163
|
+
{
|
|
164
|
+
"column": "creator",
|
|
165
|
+
"operator": "$eq",
|
|
166
|
+
"value": "[[current_user]]",
|
|
167
|
+
"value_from": 0,
|
|
168
|
+
"value_relation": ""
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"column": ":1005",
|
|
172
|
+
"operator": "$eq",
|
|
173
|
+
"value": "high",
|
|
174
|
+
"value_from": 0,
|
|
175
|
+
"value_relation": ""
|
|
176
|
+
}
|
|
177
|
+
]
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
]
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
"list_options": {}
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
"notes": [
|
|
187
|
+
"actor means Arcubase role id",
|
|
188
|
+
"condition mode \"and\" and \"or\" are canonical; legacy \"all\" also reads as AND",
|
|
189
|
+
"nested OR uses subset and leaves the wrapper column/operator empty"
|
|
190
|
+
]
|
|
191
|
+
}
|
|
192
|
+
]
|
|
193
|
+
},
|
|
194
|
+
"user.row.bulk-update": {
|
|
195
|
+
"command": [
|
|
196
|
+
"row",
|
|
197
|
+
"bulk-update"
|
|
198
|
+
],
|
|
199
|
+
"examples": [
|
|
200
|
+
{
|
|
201
|
+
"title": "bulk update explicit row ids",
|
|
202
|
+
"body": {
|
|
203
|
+
"selection": {
|
|
204
|
+
"type": "ids",
|
|
205
|
+
"ids": [
|
|
206
|
+
2188890411,
|
|
207
|
+
2188890412
|
|
208
|
+
],
|
|
209
|
+
"length": 2
|
|
210
|
+
},
|
|
211
|
+
"fields": [
|
|
212
|
+
{
|
|
213
|
+
"id": 1003,
|
|
214
|
+
"action": {
|
|
215
|
+
"type": "set",
|
|
216
|
+
"value": 2
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
]
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
"title": "bulk update rows selected by a search condition",
|
|
224
|
+
"body": {
|
|
225
|
+
"selection": {
|
|
226
|
+
"type": "condition",
|
|
227
|
+
"condition": {
|
|
228
|
+
"selectAll": true,
|
|
229
|
+
"q": "SO-1001"
|
|
230
|
+
},
|
|
231
|
+
"length": 0
|
|
232
|
+
},
|
|
233
|
+
"fields": [
|
|
234
|
+
{
|
|
235
|
+
"id": 1003,
|
|
236
|
+
"action": {
|
|
237
|
+
"type": "set",
|
|
238
|
+
"value": 2
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
]
|
|
242
|
+
},
|
|
243
|
+
"notes": [
|
|
244
|
+
"condition selection uses row-query style keys such as q, tab, and filter_:1002"
|
|
245
|
+
]
|
|
246
|
+
}
|
|
247
|
+
]
|
|
248
|
+
},
|
|
249
|
+
"user.row.selection-action": {
|
|
250
|
+
"command": [
|
|
251
|
+
"row",
|
|
252
|
+
"selection-action"
|
|
253
|
+
],
|
|
254
|
+
"examples": [
|
|
255
|
+
{
|
|
256
|
+
"title": "run a custom action against explicit row ids",
|
|
257
|
+
"body": {
|
|
258
|
+
"selection": {
|
|
259
|
+
"type": "ids",
|
|
260
|
+
"ids": [
|
|
261
|
+
2188890411
|
|
262
|
+
],
|
|
263
|
+
"length": 1
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
"notes": [
|
|
267
|
+
"pass the backend action code with --action",
|
|
268
|
+
"for query action, use ids or all selection instead of condition selection"
|
|
269
|
+
]
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
"title": "run a custom action against a filtered selection",
|
|
273
|
+
"body": {
|
|
274
|
+
"selection": {
|
|
275
|
+
"type": "condition",
|
|
276
|
+
"condition": {
|
|
277
|
+
"selectAll": true,
|
|
278
|
+
"q": "SO-1001"
|
|
279
|
+
},
|
|
280
|
+
"length": 0
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
"notes": [
|
|
284
|
+
"condition selection uses row-query style keys such as q, tab, and filter_:1002"
|
|
285
|
+
]
|
|
286
|
+
}
|
|
287
|
+
]
|
|
288
|
+
}
|
|
289
|
+
} as const satisfies Record<string, { command: readonly string[]; examples: readonly GeneratedHelpExample[] }>
|
package/src/runtime/execute.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { getBodySchema, getCommandDocHints, getDocHints, getTypeHints, getUnsupp
|
|
|
10
10
|
import { resolveArcubaseSDKPath } from './paths.js'
|
|
11
11
|
import { compileCSVImportMapping, compileExcelImportMapping, type CompiledEntityImportConfig } from './entity_import_mapping.js'
|
|
12
12
|
import { uploadLocalFileWithToken } from './local_upload.js'
|
|
13
|
+
import { renderCommandHelpExamples } from './help_examples.js'
|
|
13
14
|
|
|
14
15
|
export type CommandExecutionSummary = {
|
|
15
16
|
scope: CommandScope
|
|
@@ -64,12 +65,39 @@ function buildUnknownCommandDetails(scope: CommandScope, moduleName: string, com
|
|
|
64
65
|
return suggestions.length > 0 ? { suggestions } : undefined
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
function throwHelpExamplesRequiresCommand(scope: CommandScope, moduleName?: string): never {
|
|
69
|
+
const binary = binaryForScope(scope)
|
|
70
|
+
const suggestions = scope === 'admin'
|
|
71
|
+
? [
|
|
72
|
+
`${binary} access-rule create --help-examples`,
|
|
73
|
+
`${binary} access-rule assign-users --help-examples`,
|
|
74
|
+
]
|
|
75
|
+
: [
|
|
76
|
+
`${binary} row bulk-update --help-examples`,
|
|
77
|
+
`${binary} row selection-action --help-examples`,
|
|
78
|
+
]
|
|
79
|
+
throw new CLIError(
|
|
80
|
+
'HELP_EXAMPLES_REQUIRES_COMMAND',
|
|
81
|
+
'--help-examples must follow a final command',
|
|
82
|
+
2,
|
|
83
|
+
{
|
|
84
|
+
operation: moduleName ? `${binary} ${moduleName} --help-examples` : `${binary} --help-examples`,
|
|
85
|
+
hint: `retry with a final command, for example: ${suggestions[0]}`,
|
|
86
|
+
suggestions,
|
|
87
|
+
},
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
67
91
|
type EnvInput = Record<string, string | undefined>
|
|
68
92
|
|
|
69
93
|
function sdkPath(file: string, env: EnvInput = process.env): string {
|
|
70
94
|
return resolveArcubaseSDKPath(file, env)
|
|
71
95
|
}
|
|
72
96
|
|
|
97
|
+
function binaryForScope(scope: CommandScope): string {
|
|
98
|
+
return scope === 'admin' ? 'arcubase-admin' : 'arcubase'
|
|
99
|
+
}
|
|
100
|
+
|
|
73
101
|
export function renderCommandHelp(scope: CommandScope, moduleName: string, commandName?: string, env: EnvInput = process.env): string {
|
|
74
102
|
const command = findCommand(scope, moduleName, commandName)
|
|
75
103
|
if (!command) {
|
|
@@ -92,6 +120,7 @@ export function renderCommandHelp(scope: CommandScope, moduleName: string, comma
|
|
|
92
120
|
command.requestType ? `body: ${bodyTypeText} via --body-json <json-string> | --body-file <file>.json` : 'body: none',
|
|
93
121
|
queryFlags.length ? `query flags: ${queryFlags.join(', ')}, --query-file` : 'query flags: --query-file',
|
|
94
122
|
pathFlags.length ? `path flags: ${pathFlags.join(', ')}` : 'path flags: none',
|
|
123
|
+
`copyable examples: ${scope === 'admin' ? 'arcubase-admin' : 'arcubase'} ${command.commandPath.join(' ')} --help-examples`,
|
|
95
124
|
...(isTableCreate
|
|
96
125
|
? [
|
|
97
126
|
'body-json example:',
|
|
@@ -248,7 +277,7 @@ function buildScalarQuery(command: ReturnType<typeof findCommand>, flags: Record
|
|
|
248
277
|
}
|
|
249
278
|
|
|
250
279
|
function validateKnownFlags(command: NonNullable<ReturnType<typeof findCommand>>, flags: Record<string, string | boolean>, env: EnvInput = process.env) {
|
|
251
|
-
const allowed = new Set<string>(['help', 'body-file', 'body-json', 'query-file'])
|
|
280
|
+
const allowed = new Set<string>(['help', 'help-examples', 'body-file', 'body-json', 'query-file'])
|
|
252
281
|
for (const mapping of command.pathParams) {
|
|
253
282
|
if (!getFixedValue(mapping)) {
|
|
254
283
|
allowed.add(mapping.flag)
|
|
@@ -1051,6 +1080,38 @@ function validateAccessRuleUserScope(
|
|
|
1051
1080
|
}
|
|
1052
1081
|
}
|
|
1053
1082
|
|
|
1083
|
+
function normalizeNumericAccessRuleUserScopeIDs(
|
|
1084
|
+
scope: CommandScope,
|
|
1085
|
+
command: NonNullable<ReturnType<typeof findCommand>>,
|
|
1086
|
+
body: unknown,
|
|
1087
|
+
): unknown {
|
|
1088
|
+
if (!isAccessRuleCommand(scope, command) || !isRecord(body) || !isRecord(body.options) || !Array.isArray(body.options.user_scope)) {
|
|
1089
|
+
return body
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
let changed = false
|
|
1093
|
+
const userScope = body.options.user_scope.map((item) => {
|
|
1094
|
+
if (!isRecord(item) || typeof item.id !== 'string' || !/^\d+$/.test(item.id)) {
|
|
1095
|
+
return item
|
|
1096
|
+
}
|
|
1097
|
+
const id = Number(item.id)
|
|
1098
|
+
if (!Number.isFinite(id) || !Number.isInteger(id)) {
|
|
1099
|
+
return item
|
|
1100
|
+
}
|
|
1101
|
+
changed = true
|
|
1102
|
+
return { ...item, id }
|
|
1103
|
+
})
|
|
1104
|
+
|
|
1105
|
+
if (!changed) return body
|
|
1106
|
+
return {
|
|
1107
|
+
...body,
|
|
1108
|
+
options: {
|
|
1109
|
+
...body.options,
|
|
1110
|
+
user_scope: userScope,
|
|
1111
|
+
},
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1054
1115
|
function requireUpdateContains(
|
|
1055
1116
|
scope: CommandScope,
|
|
1056
1117
|
command: NonNullable<ReturnType<typeof findCommand>>,
|
|
@@ -2005,16 +2066,32 @@ export async function executeCLI(scope: CommandScope, argv: string[], env?: Runt
|
|
|
2005
2066
|
const parsed = parseCLIArgs(argv)
|
|
2006
2067
|
const envInput = env ?? process.env
|
|
2007
2068
|
if (parsed.commandTokens.length === 0) {
|
|
2069
|
+
if (hasFlag(parsed.flags, 'help-examples')) {
|
|
2070
|
+
throwHelpExamplesRequiresCommand(scope)
|
|
2071
|
+
}
|
|
2008
2072
|
return { kind: 'help', text: renderRootHelp(scope) }
|
|
2009
2073
|
}
|
|
2010
2074
|
const [moduleName, commandName] = parsed.commandTokens
|
|
2011
2075
|
if (!moduleName) {
|
|
2076
|
+
if (hasFlag(parsed.flags, 'help-examples')) {
|
|
2077
|
+
throwHelpExamplesRequiresCommand(scope)
|
|
2078
|
+
}
|
|
2012
2079
|
return { kind: 'help', text: renderRootHelp(scope) }
|
|
2013
2080
|
}
|
|
2014
2081
|
const singleTokenCommand = !commandName ? findCommand(scope, moduleName) : null
|
|
2015
2082
|
if (!commandName && !singleTokenCommand) {
|
|
2083
|
+
if (hasFlag(parsed.flags, 'help-examples')) {
|
|
2084
|
+
throwHelpExamplesRequiresCommand(scope, moduleName)
|
|
2085
|
+
}
|
|
2016
2086
|
return { kind: 'help', text: renderModuleHelp(scope, moduleName, envInput) }
|
|
2017
2087
|
}
|
|
2088
|
+
if (hasFlag(parsed.flags, 'help-examples')) {
|
|
2089
|
+
const command = commandName ? findCommand(scope, moduleName, commandName) : singleTokenCommand
|
|
2090
|
+
if (!command) {
|
|
2091
|
+
throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName}${commandName ? ` ${commandName}` : ''}`, 2, buildUnknownCommandDetails(scope, moduleName, commandName ?? ''))
|
|
2092
|
+
}
|
|
2093
|
+
return { kind: 'help', text: renderCommandHelpExamples(scope, command) }
|
|
2094
|
+
}
|
|
2018
2095
|
if (hasFlag(parsed.flags, 'help')) {
|
|
2019
2096
|
return { kind: 'help', text: renderCommandHelp(scope, moduleName, commandName, envInput) }
|
|
2020
2097
|
}
|
|
@@ -2065,6 +2142,7 @@ export async function executeCLI(scope: CommandScope, argv: string[], env?: Runt
|
|
|
2065
2142
|
if (body === undefined) {
|
|
2066
2143
|
throw new CLIError('MISSING_BODY_FILE', `command requires --body-json or --body-file for ${command.requestType}; prefer --body-json to avoid file creation`, 2)
|
|
2067
2144
|
}
|
|
2145
|
+
body = normalizeNumericAccessRuleUserScopeIDs(scope, command, body)
|
|
2068
2146
|
if (isTableCreateCommand(scope, command)) {
|
|
2069
2147
|
requireTableCreateSchemaBody(scope, command, body, runtimeEnv)
|
|
2070
2148
|
validateActionSpecificRawBody(scope, command, body, runtimeEnv)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { helpExamplesIndex } from '../generated/help_examples.generated.js'
|
|
2
|
+
import type { CommandDef, CommandScope } from './command_registry.js'
|
|
3
|
+
|
|
4
|
+
type HelpExample = {
|
|
5
|
+
title: string
|
|
6
|
+
body?: unknown
|
|
7
|
+
notes?: readonly string[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function binaryForScope(scope: CommandScope): string {
|
|
11
|
+
return scope === 'admin' ? 'arcubase-admin' : 'arcubase'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getFixedValue(value: object): string | undefined {
|
|
15
|
+
if (!('fixedValue' in value)) {
|
|
16
|
+
return undefined
|
|
17
|
+
}
|
|
18
|
+
const fixedValue = (value as { fixedValue?: unknown }).fixedValue
|
|
19
|
+
return typeof fixedValue === 'string' ? fixedValue : undefined
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function pathFlagPlaceholders(command: CommandDef): string {
|
|
23
|
+
const parts = command.pathParams
|
|
24
|
+
.filter((item) => !getFixedValue(item))
|
|
25
|
+
.map((item) => `--${item.flag} <${item.flag.replace(/-/g, '_')}>`)
|
|
26
|
+
return parts.length > 0 ? `${parts.join(' ')} ` : ''
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function helpExampleKey(scope: CommandScope, command: CommandDef): string {
|
|
30
|
+
return `${scope}.${command.commandPath.join('.')}`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getCommandHelpExamples(scope: CommandScope, command: CommandDef): readonly HelpExample[] {
|
|
34
|
+
return (helpExamplesIndex as Record<string, { examples?: readonly HelpExample[] } | undefined>)[helpExampleKey(scope, command)]?.examples ?? []
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function renderCommandHelpExamples(scope: CommandScope, command: CommandDef): string {
|
|
38
|
+
const binary = binaryForScope(scope)
|
|
39
|
+
const examples = getCommandHelpExamples(scope, command)
|
|
40
|
+
const header = `${binary} ${command.commandPath.join(' ')} --help-examples`
|
|
41
|
+
if (examples.length === 0) {
|
|
42
|
+
return [
|
|
43
|
+
header,
|
|
44
|
+
'',
|
|
45
|
+
'No dedicated examples for this final command yet.',
|
|
46
|
+
`Use ${binary} ${command.commandPath.join(' ')} --help for flags, docs, and type paths.`,
|
|
47
|
+
].join('\n')
|
|
48
|
+
}
|
|
49
|
+
return [
|
|
50
|
+
header,
|
|
51
|
+
'',
|
|
52
|
+
...examples.flatMap((example) => [
|
|
53
|
+
`${example.title}:`,
|
|
54
|
+
...(example.body === undefined
|
|
55
|
+
? []
|
|
56
|
+
: [` ${binary} ${command.commandPath.join(' ')} ${pathFlagPlaceholders(command)}--body-json '${JSON.stringify(example.body)}'`.replace(/\s+/g, ' ').trim()]),
|
|
57
|
+
...(example.notes ?? []).map((note) => ` - ${note}`),
|
|
58
|
+
'',
|
|
59
|
+
]),
|
|
60
|
+
].join('\n').trimEnd()
|
|
61
|
+
}
|
|
@@ -1344,6 +1344,33 @@ test('access-rule assign-users rejects non-numeric user scope id', async () => {
|
|
|
1344
1344
|
}, (error: unknown) => assertAssignUsersGuidance(error, /numeric arcubaseUserId, department ID, or actor ID/))
|
|
1345
1345
|
})
|
|
1346
1346
|
|
|
1347
|
+
test('access-rule create normalizes numeric string user_scope ids before validation and request', async () => {
|
|
1348
|
+
const out = await executeCLI(
|
|
1349
|
+
'admin',
|
|
1350
|
+
['access-rule', 'create', '--app-id', 'app_1', '--table-id', 'table_1', '--body-json', '{"name":"Role read","enabled":true,"type":"form","options":{"user_scope":[{"type":"actor","id":"2188883301"}],"policy":{"key":"custom","actions":["view"],"all_fields_read":true,"all_fields_write":false},"list_options":{}}}'],
|
|
1351
|
+
env as any,
|
|
1352
|
+
async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
|
|
1353
|
+
)
|
|
1354
|
+
assert.equal(out.kind, 'result')
|
|
1355
|
+
assert.deepEqual(out.data.options.user_scope, [{ type: 'actor', id: 2188883301 }])
|
|
1356
|
+
})
|
|
1357
|
+
|
|
1358
|
+
test('access-rule assign-users normalizes numeric string user_scope ids before validation and request', async () => {
|
|
1359
|
+
const out = await executeCLI(
|
|
1360
|
+
'admin',
|
|
1361
|
+
['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":"2188889901"},{"type":"department","id":"2188881201"},{"type":"actor","id":"2188883301"}]}}'],
|
|
1362
|
+
env as any,
|
|
1363
|
+
async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
|
|
1364
|
+
)
|
|
1365
|
+
assert.equal(out.kind, 'result')
|
|
1366
|
+
assert.deepEqual(out.data.update, ['user_scope'])
|
|
1367
|
+
assert.deepEqual(out.data.options.user_scope, [
|
|
1368
|
+
{ type: 'user', id: 2188889901 },
|
|
1369
|
+
{ type: 'department', id: 2188881201 },
|
|
1370
|
+
{ type: 'actor', id: 2188883301 },
|
|
1371
|
+
])
|
|
1372
|
+
})
|
|
1373
|
+
|
|
1347
1374
|
test('access-rule assign-users accepts canonical mixed user_scope body', async () => {
|
|
1348
1375
|
const out = await executeCLI(
|
|
1349
1376
|
'admin',
|
package/src/tests/help.test.ts
CHANGED
|
@@ -130,6 +130,38 @@ test('admin access-rule help gives update whitelist and assign-users body-json e
|
|
|
130
130
|
assert.doesNotMatch(assign.text, /"allowedUsers"/)
|
|
131
131
|
})
|
|
132
132
|
|
|
133
|
+
test('help-examples requires a final command', async () => {
|
|
134
|
+
await assert.rejects(
|
|
135
|
+
() => executeCLI('admin', ['--help-examples'], env as any, async () => new Response('{}')),
|
|
136
|
+
{ code: 'HELP_EXAMPLES_REQUIRES_COMMAND' },
|
|
137
|
+
)
|
|
138
|
+
await assert.rejects(
|
|
139
|
+
() => executeCLI('admin', ['access-rule', '--help-examples'], env as any, async () => new Response('{}')),
|
|
140
|
+
{ code: 'HELP_EXAMPLES_REQUIRES_COMMAND' },
|
|
141
|
+
)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
test('admin access-rule create help-examples show canonical ingress scopes and conditions', async () => {
|
|
145
|
+
const out = await executeCLI('admin', ['access-rule', 'create', '--help-examples'], env as any, async () => new Response('{}'))
|
|
146
|
+
assert.equal(out.kind, 'help')
|
|
147
|
+
assert.match(out.text, /arcubase-admin access-rule create --help-examples/)
|
|
148
|
+
assert.match(out.text, /"type":"department"/)
|
|
149
|
+
assert.match(out.text, /"type":"actor"/)
|
|
150
|
+
assert.match(out.text, /"column":"creator","operator":"\$eq","value":"\[\[current_user\]\]"/)
|
|
151
|
+
assert.match(out.text, /"subset":\{"mode":"or"/)
|
|
152
|
+
assert.doesNotMatch(out.text, forbiddenArcubaseUserTerms())
|
|
153
|
+
assert.doesNotMatch(out.text, /"type":"member"/)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test('user row bulk-update help-examples show ids and condition selection bodies', async () => {
|
|
157
|
+
const out = await executeCLI('user', ['row', 'bulk-update', '--help-examples'], env as any, async () => new Response('{}'))
|
|
158
|
+
assert.equal(out.kind, 'help')
|
|
159
|
+
assert.match(out.text, /arcubase row bulk-update --help-examples/)
|
|
160
|
+
assert.match(out.text, /"selection":\{"type":"ids"/)
|
|
161
|
+
assert.match(out.text, /"selection":\{"type":"condition"/)
|
|
162
|
+
assert.match(out.text, /"action":\{"type":"set"/)
|
|
163
|
+
})
|
|
164
|
+
|
|
133
165
|
test('admin assign-users help uses arcubaseUserId terminology', async () => {
|
|
134
166
|
const help = await executeCLI('admin', ['access-rule', 'assign-users', '--help'], env as any, async () => new Response('{}'))
|
|
135
167
|
assert.equal(help.kind, 'help')
|