@carthooks/arcubase-cli 0.1.18 → 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 +423 -34
- package/bundle/arcubase.mjs +423 -34
- 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/generated/zod_registry.generated.d.ts.map +1 -1
- package/dist/generated/zod_registry.generated.js +1 -1
- package/dist/runtime/execute.d.ts.map +1 -1
- package/dist/runtime/execute.js +84 -17
- 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/access-rule.md +12 -10
- 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/sdk-dist/generated/zod_registry.generated.ts +1 -1
- package/sdk-dist/types/common.ts +1 -0
- package/sdk-dist/types/entity.ts +1 -1
- package/sdk-dist/types/kiosk-ui.ts +1 -1
- package/sdk-dist/types/tenant.ts +1 -1
- package/src/generated/help_examples.generated.ts +289 -0
- package/src/generated/zod_registry.generated.ts +1 -1
- package/src/runtime/execute.ts +98 -25
- package/src/runtime/help_examples.ts +61 -0
- package/src/tests/execute_validation.test.ts +88 -10
- package/src/tests/help.test.ts +35 -1
- package/src/tests/zod_registry.test.ts +38 -0
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:',
|
|
@@ -130,19 +159,19 @@ export function renderCommandHelp(scope: CommandScope, moduleName: string, comma
|
|
|
130
159
|
` - hide field: ${JSON.stringify(buildHiddenFieldAccessRuleBody())}`,
|
|
131
160
|
'success requires:',
|
|
132
161
|
' - result.enabled must be true',
|
|
133
|
-
' - options.user_scope[].type is "user"
|
|
162
|
+
' - options.user_scope[].type is "user", "department", or "actor"',
|
|
134
163
|
' - hidden fields use colon field keys such as ":1002"',
|
|
135
164
|
]
|
|
136
165
|
: []),
|
|
137
166
|
...(scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'assign-users'
|
|
138
167
|
? [
|
|
139
168
|
'body-json example:',
|
|
140
|
-
' - {"update":["user_scope"],"options":{"user_scope":[{"type":"user","id":2188889901}]}}',
|
|
169
|
+
' - {"update":["user_scope"],"options":{"user_scope":[{"type":"user","id":2188889901},{"type":"department","id":2188881201},{"type":"actor","id":2188883301}]}}',
|
|
141
170
|
'success requires:',
|
|
142
171
|
' - result.enabled must be true',
|
|
143
|
-
' - result.options.user_scope must contain
|
|
144
|
-
' - PermitUserScope.id must be the numeric arcubaseUserId',
|
|
145
|
-
' - Do not copy a non-numeric
|
|
172
|
+
' - result.options.user_scope must contain every assigned user, department, or actor scope',
|
|
173
|
+
' - PermitUserScope.id must be the numeric arcubaseUserId, department ID, or actor ID',
|
|
174
|
+
' - Do not copy a non-numeric BotWorks userId into PermitUserScope.id',
|
|
146
175
|
' - If result.enabled is false, enable the rule first and retry assign-users',
|
|
147
176
|
]
|
|
148
177
|
: []),
|
|
@@ -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)
|
|
@@ -697,14 +726,18 @@ function buildAssignUsersValidationDetails(
|
|
|
697
726
|
shapeHint: {
|
|
698
727
|
update: ['user_scope'],
|
|
699
728
|
options: {
|
|
700
|
-
user_scope: [
|
|
729
|
+
user_scope: [
|
|
730
|
+
{ type: 'user', id: 2188889901 },
|
|
731
|
+
{ type: 'department', id: 2188881201 },
|
|
732
|
+
{ type: 'actor', id: 2188883301 },
|
|
733
|
+
],
|
|
701
734
|
},
|
|
702
735
|
},
|
|
703
736
|
commonMistakes: [
|
|
704
737
|
'do not use user_scope at top level',
|
|
705
738
|
'do not use users or allowedUsers',
|
|
706
|
-
'do not use non-numeric
|
|
707
|
-
'id must be the numeric arcubaseUserId',
|
|
739
|
+
'do not use non-numeric BotWorks userId as id',
|
|
740
|
+
'id must be the numeric arcubaseUserId, department ID, or actor ID',
|
|
708
741
|
],
|
|
709
742
|
}
|
|
710
743
|
}
|
|
@@ -1004,7 +1037,7 @@ function validateAccessRuleUserScope(
|
|
|
1004
1037
|
if (userScope === undefined) return
|
|
1005
1038
|
if (!Array.isArray(userScope)) {
|
|
1006
1039
|
throwBodyValidationFailure(
|
|
1007
|
-
|
|
1040
|
+
'access-rule user_scope must be an array of {"type":"user","id":2188889901}, {"type":"department","id":2188881201}, or {"type":"actor","id":2188883301}',
|
|
1008
1041
|
requestType,
|
|
1009
1042
|
pathPrefix,
|
|
1010
1043
|
scope,
|
|
@@ -1047,6 +1080,38 @@ function validateAccessRuleUserScope(
|
|
|
1047
1080
|
}
|
|
1048
1081
|
}
|
|
1049
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
|
+
|
|
1050
1115
|
function requireUpdateContains(
|
|
1051
1116
|
scope: CommandScope,
|
|
1052
1117
|
command: NonNullable<ReturnType<typeof findCommand>>,
|
|
@@ -1081,7 +1146,7 @@ function validateActionSpecificRawBody(
|
|
|
1081
1146
|
if (isAssignUsersCommand(scope, command)) {
|
|
1082
1147
|
if ('users' in body || 'allowedUsers' in body || 'user_scope' in body) {
|
|
1083
1148
|
throwBodyValidationFailure(
|
|
1084
|
-
'access-rule assign-users only accepts body.options.user_scope with {"update":["user_scope"],"options":{"user_scope":[{"type":"user","id":2188889901}]}}',
|
|
1149
|
+
'access-rule assign-users only accepts body.options.user_scope with {"update":["user_scope"],"options":{"user_scope":[{"type":"user","id":2188889901},{"type":"department","id":2188881201},{"type":"actor","id":2188883301}]}}',
|
|
1085
1150
|
command.requestType,
|
|
1086
1151
|
'body.options.user_scope',
|
|
1087
1152
|
scope,
|
|
@@ -1213,7 +1278,7 @@ function validateActionSpecificBody(
|
|
|
1213
1278
|
const userScope = options && Array.isArray(options.user_scope) ? options.user_scope : undefined
|
|
1214
1279
|
if ('users' in body || 'allowedUsers' in body) {
|
|
1215
1280
|
throwBodyValidationFailure(
|
|
1216
|
-
'access-rule assign-users only accepts body.options.user_scope with {"update":["user_scope"],"options":{"user_scope":[{"type":"user","id":2188889901}]}}',
|
|
1281
|
+
'access-rule assign-users only accepts body.options.user_scope with {"update":["user_scope"],"options":{"user_scope":[{"type":"user","id":2188889901},{"type":"department","id":2188881201},{"type":"actor","id":2188883301}]}}',
|
|
1217
1282
|
command.requestType,
|
|
1218
1283
|
'body.options.user_scope',
|
|
1219
1284
|
scope,
|
|
@@ -1254,19 +1319,9 @@ function validateActionSpecificBody(
|
|
|
1254
1319
|
env,
|
|
1255
1320
|
)
|
|
1256
1321
|
}
|
|
1257
|
-
if (item.type !== 'user') {
|
|
1258
|
-
throwBodyValidationFailure(
|
|
1259
|
-
'access-rule assign-users requires body.options.user_scope[].type to be "user"',
|
|
1260
|
-
command.requestType,
|
|
1261
|
-
`body.options.user_scope.${index}.type`,
|
|
1262
|
-
scope,
|
|
1263
|
-
command,
|
|
1264
|
-
env,
|
|
1265
|
-
)
|
|
1266
|
-
}
|
|
1267
1322
|
if (typeof item.id !== 'number' || !Number.isInteger(item.id) || item.id <= 0) {
|
|
1268
1323
|
throwBodyValidationFailure(
|
|
1269
|
-
'access-rule assign-users requires body.options.user_scope[].id to be numeric arcubaseUserId',
|
|
1324
|
+
'access-rule assign-users requires body.options.user_scope[].id to be numeric arcubaseUserId, department ID, or actor ID',
|
|
1270
1325
|
command.requestType,
|
|
1271
1326
|
`body.options.user_scope.${index}.id`,
|
|
1272
1327
|
scope,
|
|
@@ -2011,16 +2066,32 @@ export async function executeCLI(scope: CommandScope, argv: string[], env?: Runt
|
|
|
2011
2066
|
const parsed = parseCLIArgs(argv)
|
|
2012
2067
|
const envInput = env ?? process.env
|
|
2013
2068
|
if (parsed.commandTokens.length === 0) {
|
|
2069
|
+
if (hasFlag(parsed.flags, 'help-examples')) {
|
|
2070
|
+
throwHelpExamplesRequiresCommand(scope)
|
|
2071
|
+
}
|
|
2014
2072
|
return { kind: 'help', text: renderRootHelp(scope) }
|
|
2015
2073
|
}
|
|
2016
2074
|
const [moduleName, commandName] = parsed.commandTokens
|
|
2017
2075
|
if (!moduleName) {
|
|
2076
|
+
if (hasFlag(parsed.flags, 'help-examples')) {
|
|
2077
|
+
throwHelpExamplesRequiresCommand(scope)
|
|
2078
|
+
}
|
|
2018
2079
|
return { kind: 'help', text: renderRootHelp(scope) }
|
|
2019
2080
|
}
|
|
2020
2081
|
const singleTokenCommand = !commandName ? findCommand(scope, moduleName) : null
|
|
2021
2082
|
if (!commandName && !singleTokenCommand) {
|
|
2083
|
+
if (hasFlag(parsed.flags, 'help-examples')) {
|
|
2084
|
+
throwHelpExamplesRequiresCommand(scope, moduleName)
|
|
2085
|
+
}
|
|
2022
2086
|
return { kind: 'help', text: renderModuleHelp(scope, moduleName, envInput) }
|
|
2023
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
|
+
}
|
|
2024
2095
|
if (hasFlag(parsed.flags, 'help')) {
|
|
2025
2096
|
return { kind: 'help', text: renderCommandHelp(scope, moduleName, commandName, envInput) }
|
|
2026
2097
|
}
|
|
@@ -2071,6 +2142,7 @@ export async function executeCLI(scope: CommandScope, argv: string[], env?: Runt
|
|
|
2071
2142
|
if (body === undefined) {
|
|
2072
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)
|
|
2073
2144
|
}
|
|
2145
|
+
body = normalizeNumericAccessRuleUserScopeIDs(scope, command, body)
|
|
2074
2146
|
if (isTableCreateCommand(scope, command)) {
|
|
2075
2147
|
requireTableCreateSchemaBody(scope, command, body, runtimeEnv)
|
|
2076
2148
|
validateActionSpecificRawBody(scope, command, body, runtimeEnv)
|
|
@@ -2108,16 +2180,17 @@ export async function executeCLI(scope: CommandScope, argv: string[], env?: Runt
|
|
|
2108
2180
|
const message = parsedBody.error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`).join('; ')
|
|
2109
2181
|
if (isAssignUsersCommand(scope, command)) {
|
|
2110
2182
|
const hasUserScopeIDIssue = parsedBody.error.issues.some((issue) => issue.path.join('.') === 'options.user_scope.0.id' || issue.path.join('.').startsWith('options.user_scope.'))
|
|
2183
|
+
const userScopeIDMessage = 'access-rule assign-users requires body.options.user_scope[].id to be numeric arcubaseUserId, department ID, or actor ID'
|
|
2111
2184
|
throw new CLIError(
|
|
2112
2185
|
'BODY_VALIDATION_FAILED',
|
|
2113
|
-
hasUserScopeIDIssue ?
|
|
2186
|
+
hasUserScopeIDIssue ? userScopeIDMessage : message,
|
|
2114
2187
|
2,
|
|
2115
2188
|
buildAssignUsersValidationDetails(
|
|
2116
2189
|
scope,
|
|
2117
2190
|
command,
|
|
2118
2191
|
command.requestType,
|
|
2119
2192
|
hasUserScopeIDIssue ? 'body.options.user_scope' : 'body',
|
|
2120
|
-
hasUserScopeIDIssue ?
|
|
2193
|
+
hasUserScopeIDIssue ? userScopeIDMessage : message,
|
|
2121
2194
|
runtimeEnv,
|
|
2122
2195
|
)
|
|
2123
2196
|
)
|
|
@@ -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
|
+
}
|
|
@@ -55,14 +55,18 @@ function assertAssignUsersGuidance(error: unknown, messagePattern?: RegExp): boo
|
|
|
55
55
|
assert.deepEqual(error.details?.shapeHint, {
|
|
56
56
|
update: ['user_scope'],
|
|
57
57
|
options: {
|
|
58
|
-
user_scope: [
|
|
58
|
+
user_scope: [
|
|
59
|
+
{ type: 'user', id: 2188889901 },
|
|
60
|
+
{ type: 'department', id: 2188881201 },
|
|
61
|
+
{ type: 'actor', id: 2188883301 },
|
|
62
|
+
],
|
|
59
63
|
},
|
|
60
64
|
})
|
|
61
65
|
assert.deepEqual(error.details?.commonMistakes, [
|
|
62
66
|
'do not use user_scope at top level',
|
|
63
67
|
'do not use users or allowedUsers',
|
|
64
|
-
'do not use non-numeric
|
|
65
|
-
'id must be the numeric arcubaseUserId',
|
|
68
|
+
'do not use non-numeric BotWorks userId as id',
|
|
69
|
+
'id must be the numeric arcubaseUserId, department ID, or actor ID',
|
|
66
70
|
])
|
|
67
71
|
assert.ok(error.details?.tsHints?.some((item) => item.symbol === 'AppIngressUpdateReqVO' && item.file === '/runtime/arcubase-sdk/types/ingress.ts'))
|
|
68
72
|
assert.ok(error.details?.tsHints?.some((item) => item.symbol === 'EntityIngressOptions' && item.file === '/runtime/arcubase-sdk/types/ingress.ts'))
|
|
@@ -1318,7 +1322,7 @@ test('access-rule assign-users rejects top-level user_scope', async () => {
|
|
|
1318
1322
|
}, (error: unknown) => assertAssignUsersGuidance(error, /options.user_scope/))
|
|
1319
1323
|
})
|
|
1320
1324
|
|
|
1321
|
-
test('access-rule assign-users rejects
|
|
1325
|
+
test('access-rule assign-users rejects FieldVO member scope type', async () => {
|
|
1322
1326
|
await assert.rejects(async () => {
|
|
1323
1327
|
await executeCLI(
|
|
1324
1328
|
'admin',
|
|
@@ -1326,10 +1330,10 @@ test('access-rule assign-users rejects non-user member scope type', async () =>
|
|
|
1326
1330
|
env as any,
|
|
1327
1331
|
async () => new Response('{}', { status: 200 }),
|
|
1328
1332
|
)
|
|
1329
|
-
}, (error: unknown) => assertAssignUsersGuidance(error, /type
|
|
1333
|
+
}, (error: unknown) => assertAssignUsersGuidance(error, /user.*department.*actor|FieldVO type "member"/))
|
|
1330
1334
|
})
|
|
1331
1335
|
|
|
1332
|
-
test('access-rule assign-users rejects non-numeric
|
|
1336
|
+
test('access-rule assign-users rejects non-numeric user scope id', async () => {
|
|
1333
1337
|
await assert.rejects(async () => {
|
|
1334
1338
|
await executeCLI(
|
|
1335
1339
|
'admin',
|
|
@@ -1337,17 +1341,91 @@ test('access-rule assign-users rejects non-numeric arcubaseUserId', async () =>
|
|
|
1337
1341
|
env as any,
|
|
1338
1342
|
async () => new Response('{}', { status: 200 }),
|
|
1339
1343
|
)
|
|
1340
|
-
}, (error: unknown) => assertAssignUsersGuidance(error, /numeric arcubaseUserId/))
|
|
1344
|
+
}, (error: unknown) => assertAssignUsersGuidance(error, /numeric arcubaseUserId, department ID, or actor ID/))
|
|
1341
1345
|
})
|
|
1342
1346
|
|
|
1343
|
-
test('access-rule
|
|
1347
|
+
test('access-rule create normalizes numeric string user_scope ids before validation and request', async () => {
|
|
1344
1348
|
const out = await executeCLI(
|
|
1345
1349
|
'admin',
|
|
1346
|
-
['access-rule', '
|
|
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
|
+
|
|
1374
|
+
test('access-rule assign-users accepts canonical mixed user_scope body', async () => {
|
|
1375
|
+
const out = await executeCLI(
|
|
1376
|
+
'admin',
|
|
1377
|
+
['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}]}}'],
|
|
1347
1378
|
env as any,
|
|
1348
1379
|
async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
|
|
1349
1380
|
)
|
|
1350
1381
|
assert.equal(out.kind, 'result')
|
|
1351
1382
|
assert.deepEqual(out.data.update, ['user_scope'])
|
|
1352
|
-
assert.deepEqual(out.data.options.user_scope, [
|
|
1383
|
+
assert.deepEqual(out.data.options.user_scope, [
|
|
1384
|
+
{ type: 'user', id: 2188889901 },
|
|
1385
|
+
{ type: 'department', id: 2188881201 },
|
|
1386
|
+
{ type: 'actor', id: 2188883301 },
|
|
1387
|
+
])
|
|
1388
|
+
})
|
|
1389
|
+
|
|
1390
|
+
test('access-rule create accepts nested custom policy condition with field columns', async () => {
|
|
1391
|
+
const body = JSON.stringify({
|
|
1392
|
+
name: 'Sales own or west region',
|
|
1393
|
+
enabled: true,
|
|
1394
|
+
type: 'form',
|
|
1395
|
+
options: {
|
|
1396
|
+
user_scope: [{ type: 'department', id: 2188881201 }],
|
|
1397
|
+
policy: {
|
|
1398
|
+
key: 'custom',
|
|
1399
|
+
actions: ['view', 'add', 'edit'],
|
|
1400
|
+
all_fields_read: true,
|
|
1401
|
+
all_fields_write: true,
|
|
1402
|
+
condition: {
|
|
1403
|
+
mode: 'and',
|
|
1404
|
+
conditions: [
|
|
1405
|
+
{
|
|
1406
|
+
subset: {
|
|
1407
|
+
mode: 'or',
|
|
1408
|
+
conditions: [
|
|
1409
|
+
{ column: 'creator', operator: '$eq', value: '[[current_user]]' },
|
|
1410
|
+
{ column: ':1002', operator: '$eq', value: '[[current_user]]' },
|
|
1411
|
+
],
|
|
1412
|
+
},
|
|
1413
|
+
},
|
|
1414
|
+
{ column: ':1003', operator: '$eq', value: '华东' },
|
|
1415
|
+
],
|
|
1416
|
+
},
|
|
1417
|
+
},
|
|
1418
|
+
list_options: {},
|
|
1419
|
+
},
|
|
1420
|
+
})
|
|
1421
|
+
const out = await executeCLI(
|
|
1422
|
+
'admin',
|
|
1423
|
+
['access-rule', 'create', '--app-id', 'app_1', '--table-id', 'table_1', '--body-json', body],
|
|
1424
|
+
env as any,
|
|
1425
|
+
async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
|
|
1426
|
+
)
|
|
1427
|
+
assert.equal(out.kind, 'result')
|
|
1428
|
+
assert.equal(out.data.options.policy.condition.mode, 'and')
|
|
1429
|
+
assert.equal(out.data.options.policy.condition.conditions[0].subset.mode, 'or')
|
|
1430
|
+
assert.equal(out.data.options.policy.condition.conditions[0].subset.conditions[1].column, ':1002')
|
|
1353
1431
|
})
|
package/src/tests/help.test.ts
CHANGED
|
@@ -120,14 +120,48 @@ test('admin access-rule help gives update whitelist and assign-users body-json e
|
|
|
120
120
|
const assign = await executeCLI('admin', ['access-rule', 'assign-users', '--help'], env as any, async () => new Response('{}'))
|
|
121
121
|
assert.equal(assign.kind, 'help')
|
|
122
122
|
assert.match(assign.text, /"update":\["user_scope"\]/)
|
|
123
|
-
assert.match(assign.text, /\{"update":\["user_scope"\],"options":\{"user_scope":\[\{"type":"user","id":2188889901\}\]\}\}/)
|
|
123
|
+
assert.match(assign.text, /\{"update":\["user_scope"\],"options":\{"user_scope":\[\{"type":"user","id":2188889901\},\{"type":"department","id":2188881201\},\{"type":"actor","id":2188883301\}\]\}\}/)
|
|
124
124
|
assert.match(assign.text, /arcubaseUserId/)
|
|
125
|
+
assert.match(assign.text, /department ID/)
|
|
126
|
+
assert.match(assign.text, /actor ID/)
|
|
125
127
|
assert.doesNotMatch(assign.text, forbiddenArcubaseUserTerms())
|
|
126
128
|
assert.doesNotMatch(assign.text, /"type":"member"/)
|
|
127
129
|
assert.doesNotMatch(assign.text, /"users"/)
|
|
128
130
|
assert.doesNotMatch(assign.text, /"allowedUsers"/)
|
|
129
131
|
})
|
|
130
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
|
+
|
|
131
165
|
test('admin assign-users help uses arcubaseUserId terminology', async () => {
|
|
132
166
|
const help = await executeCLI('admin', ['access-rule', 'assign-users', '--help'], env as any, async () => new Response('{}'))
|
|
133
167
|
assert.equal(help.kind, 'help')
|
|
@@ -41,6 +41,44 @@ test('EntityQueryReqVO search only accepts route-query string keys', () => {
|
|
|
41
41
|
}
|
|
42
42
|
})
|
|
43
43
|
|
|
44
|
+
test('AppIngressCreateReqVO accepts nested ConditionSet with field EntityProp columns', () => {
|
|
45
|
+
const schema = getBodySchema('AppIngressCreateReqVO')
|
|
46
|
+
assert.ok(schema)
|
|
47
|
+
|
|
48
|
+
const result = schema.safeParse({
|
|
49
|
+
name: 'Sales own or west region',
|
|
50
|
+
enabled: true,
|
|
51
|
+
type: 'form',
|
|
52
|
+
options: {
|
|
53
|
+
user_scope: [{ type: 'department', id: 2188881201 }],
|
|
54
|
+
policy: {
|
|
55
|
+
key: 'custom',
|
|
56
|
+
actions: ['view', 'add', 'edit'],
|
|
57
|
+
all_fields_read: true,
|
|
58
|
+
all_fields_write: true,
|
|
59
|
+
condition: {
|
|
60
|
+
mode: 'and',
|
|
61
|
+
conditions: [
|
|
62
|
+
{
|
|
63
|
+
subset: {
|
|
64
|
+
mode: 'or',
|
|
65
|
+
conditions: [
|
|
66
|
+
{ column: 'creator', operator: '$eq', value: '[[current_user]]' },
|
|
67
|
+
{ column: ':1002', operator: '$eq', value: '[[current_user]]' },
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{ column: ':1003', operator: '$eq', value: '华东' },
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
list_options: {},
|
|
76
|
+
},
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
assert.equal(result.success, true)
|
|
80
|
+
})
|
|
81
|
+
|
|
44
82
|
test('unknown request type has no schema and no unsupported reason', () => {
|
|
45
83
|
assert.equal(getBodySchema('DefinitelyMissingReqVO'), null)
|
|
46
84
|
assert.equal(getUnsupportedBodySchemaReason('DefinitelyMissingReqVO'), null)
|