@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/dist/runtime/execute.js
CHANGED
|
@@ -10,6 +10,7 @@ import { getBodySchema, getCommandDocHints, getDocHints, getTypeHints, getUnsupp
|
|
|
10
10
|
import { resolveArcubaseSDKPath } from './paths.js';
|
|
11
11
|
import { compileCSVImportMapping, compileExcelImportMapping } from './entity_import_mapping.js';
|
|
12
12
|
import { uploadLocalFileWithToken } from './local_upload.js';
|
|
13
|
+
import { renderCommandHelpExamples } from './help_examples.js';
|
|
13
14
|
export function renderRootHelp(scope) {
|
|
14
15
|
const binary = scope === 'admin' ? 'arcubase-admin' : 'arcubase';
|
|
15
16
|
const items = listModules(scope).map((item) => ` - ${item}`);
|
|
@@ -50,9 +51,29 @@ function buildUnknownCommandDetails(scope, moduleName, commandName) {
|
|
|
50
51
|
const suggestions = findCommandSuggestions(moduleName, commandName).map((item) => `${item.binary} ${item.moduleName}${item.commandName ? ` ${item.commandName}` : ''}`);
|
|
51
52
|
return suggestions.length > 0 ? { suggestions } : undefined;
|
|
52
53
|
}
|
|
54
|
+
function throwHelpExamplesRequiresCommand(scope, moduleName) {
|
|
55
|
+
const binary = binaryForScope(scope);
|
|
56
|
+
const suggestions = scope === 'admin'
|
|
57
|
+
? [
|
|
58
|
+
`${binary} access-rule create --help-examples`,
|
|
59
|
+
`${binary} access-rule assign-users --help-examples`,
|
|
60
|
+
]
|
|
61
|
+
: [
|
|
62
|
+
`${binary} row bulk-update --help-examples`,
|
|
63
|
+
`${binary} row selection-action --help-examples`,
|
|
64
|
+
];
|
|
65
|
+
throw new CLIError('HELP_EXAMPLES_REQUIRES_COMMAND', '--help-examples must follow a final command', 2, {
|
|
66
|
+
operation: moduleName ? `${binary} ${moduleName} --help-examples` : `${binary} --help-examples`,
|
|
67
|
+
hint: `retry with a final command, for example: ${suggestions[0]}`,
|
|
68
|
+
suggestions,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
53
71
|
function sdkPath(file, env = process.env) {
|
|
54
72
|
return resolveArcubaseSDKPath(file, env);
|
|
55
73
|
}
|
|
74
|
+
function binaryForScope(scope) {
|
|
75
|
+
return scope === 'admin' ? 'arcubase-admin' : 'arcubase';
|
|
76
|
+
}
|
|
56
77
|
export function renderCommandHelp(scope, moduleName, commandName, env = process.env) {
|
|
57
78
|
const command = findCommand(scope, moduleName, commandName);
|
|
58
79
|
if (!command) {
|
|
@@ -75,6 +96,7 @@ export function renderCommandHelp(scope, moduleName, commandName, env = process.
|
|
|
75
96
|
command.requestType ? `body: ${bodyTypeText} via --body-json <json-string> | --body-file <file>.json` : 'body: none',
|
|
76
97
|
queryFlags.length ? `query flags: ${queryFlags.join(', ')}, --query-file` : 'query flags: --query-file',
|
|
77
98
|
pathFlags.length ? `path flags: ${pathFlags.join(', ')}` : 'path flags: none',
|
|
99
|
+
`copyable examples: ${scope === 'admin' ? 'arcubase-admin' : 'arcubase'} ${command.commandPath.join(' ')} --help-examples`,
|
|
78
100
|
...(isTableCreate
|
|
79
101
|
? [
|
|
80
102
|
'body-json example:',
|
|
@@ -113,19 +135,19 @@ export function renderCommandHelp(scope, moduleName, commandName, env = process.
|
|
|
113
135
|
` - hide field: ${JSON.stringify(buildHiddenFieldAccessRuleBody())}`,
|
|
114
136
|
'success requires:',
|
|
115
137
|
' - result.enabled must be true',
|
|
116
|
-
' - options.user_scope[].type is "user"
|
|
138
|
+
' - options.user_scope[].type is "user", "department", or "actor"',
|
|
117
139
|
' - hidden fields use colon field keys such as ":1002"',
|
|
118
140
|
]
|
|
119
141
|
: []),
|
|
120
142
|
...(scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'assign-users'
|
|
121
143
|
? [
|
|
122
144
|
'body-json example:',
|
|
123
|
-
' - {"update":["user_scope"],"options":{"user_scope":[{"type":"user","id":2188889901}]}}',
|
|
145
|
+
' - {"update":["user_scope"],"options":{"user_scope":[{"type":"user","id":2188889901},{"type":"department","id":2188881201},{"type":"actor","id":2188883301}]}}',
|
|
124
146
|
'success requires:',
|
|
125
147
|
' - result.enabled must be true',
|
|
126
|
-
' - result.options.user_scope must contain
|
|
127
|
-
' - PermitUserScope.id must be the numeric arcubaseUserId',
|
|
128
|
-
' - Do not copy a non-numeric
|
|
148
|
+
' - result.options.user_scope must contain every assigned user, department, or actor scope',
|
|
149
|
+
' - PermitUserScope.id must be the numeric arcubaseUserId, department ID, or actor ID',
|
|
150
|
+
' - Do not copy a non-numeric BotWorks userId into PermitUserScope.id',
|
|
129
151
|
' - If result.enabled is false, enable the rule first and retry assign-users',
|
|
130
152
|
]
|
|
131
153
|
: []),
|
|
@@ -229,7 +251,7 @@ function buildScalarQuery(command, flags) {
|
|
|
229
251
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
230
252
|
}
|
|
231
253
|
function validateKnownFlags(command, flags, env = process.env) {
|
|
232
|
-
const allowed = new Set(['help', 'body-file', 'body-json', 'query-file']);
|
|
254
|
+
const allowed = new Set(['help', 'help-examples', 'body-file', 'body-json', 'query-file']);
|
|
233
255
|
for (const mapping of command.pathParams) {
|
|
234
256
|
if (!getFixedValue(mapping)) {
|
|
235
257
|
allowed.add(mapping.flag);
|
|
@@ -628,14 +650,18 @@ function buildAssignUsersValidationDetails(scope, command, requestType, path, me
|
|
|
628
650
|
shapeHint: {
|
|
629
651
|
update: ['user_scope'],
|
|
630
652
|
options: {
|
|
631
|
-
user_scope: [
|
|
653
|
+
user_scope: [
|
|
654
|
+
{ type: 'user', id: 2188889901 },
|
|
655
|
+
{ type: 'department', id: 2188881201 },
|
|
656
|
+
{ type: 'actor', id: 2188883301 },
|
|
657
|
+
],
|
|
632
658
|
},
|
|
633
659
|
},
|
|
634
660
|
commonMistakes: [
|
|
635
661
|
'do not use user_scope at top level',
|
|
636
662
|
'do not use users or allowedUsers',
|
|
637
|
-
'do not use non-numeric
|
|
638
|
-
'id must be the numeric arcubaseUserId',
|
|
663
|
+
'do not use non-numeric BotWorks userId as id',
|
|
664
|
+
'id must be the numeric arcubaseUserId, department ID, or actor ID',
|
|
639
665
|
],
|
|
640
666
|
};
|
|
641
667
|
}
|
|
@@ -895,7 +921,7 @@ function validateAccessRuleUserScope(scope, command, requestType, userScope, env
|
|
|
895
921
|
if (userScope === undefined)
|
|
896
922
|
return;
|
|
897
923
|
if (!Array.isArray(userScope)) {
|
|
898
|
-
throwBodyValidationFailure('access-rule user_scope must be an array of {"type":"user","id":2188889901}', requestType, pathPrefix, scope, command, env);
|
|
924
|
+
throwBodyValidationFailure('access-rule user_scope must be an array of {"type":"user","id":2188889901}, {"type":"department","id":2188881201}, or {"type":"actor","id":2188883301}', requestType, pathPrefix, scope, command, env);
|
|
899
925
|
}
|
|
900
926
|
const allowedTypes = new Set(['user', 'department', 'actor']);
|
|
901
927
|
for (const [index, item] of userScope.entries()) {
|
|
@@ -910,6 +936,32 @@ function validateAccessRuleUserScope(scope, command, requestType, userScope, env
|
|
|
910
936
|
}
|
|
911
937
|
}
|
|
912
938
|
}
|
|
939
|
+
function normalizeNumericAccessRuleUserScopeIDs(scope, command, body) {
|
|
940
|
+
if (!isAccessRuleCommand(scope, command) || !isRecord(body) || !isRecord(body.options) || !Array.isArray(body.options.user_scope)) {
|
|
941
|
+
return body;
|
|
942
|
+
}
|
|
943
|
+
let changed = false;
|
|
944
|
+
const userScope = body.options.user_scope.map((item) => {
|
|
945
|
+
if (!isRecord(item) || typeof item.id !== 'string' || !/^\d+$/.test(item.id)) {
|
|
946
|
+
return item;
|
|
947
|
+
}
|
|
948
|
+
const id = Number(item.id);
|
|
949
|
+
if (!Number.isFinite(id) || !Number.isInteger(id)) {
|
|
950
|
+
return item;
|
|
951
|
+
}
|
|
952
|
+
changed = true;
|
|
953
|
+
return { ...item, id };
|
|
954
|
+
});
|
|
955
|
+
if (!changed)
|
|
956
|
+
return body;
|
|
957
|
+
return {
|
|
958
|
+
...body,
|
|
959
|
+
options: {
|
|
960
|
+
...body.options,
|
|
961
|
+
user_scope: userScope,
|
|
962
|
+
},
|
|
963
|
+
};
|
|
964
|
+
}
|
|
913
965
|
function requireUpdateContains(scope, command, body, field, env) {
|
|
914
966
|
if (!(field in body))
|
|
915
967
|
return;
|
|
@@ -924,7 +976,7 @@ function validateActionSpecificRawBody(scope, command, body, env) {
|
|
|
924
976
|
}
|
|
925
977
|
if (isAssignUsersCommand(scope, command)) {
|
|
926
978
|
if ('users' in body || 'allowedUsers' in body || 'user_scope' in body) {
|
|
927
|
-
throwBodyValidationFailure('access-rule assign-users only accepts body.options.user_scope with {"update":["user_scope"],"options":{"user_scope":[{"type":"user","id":2188889901}]}}', command.requestType, 'body.options.user_scope', scope, command, env);
|
|
979
|
+
throwBodyValidationFailure('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}]}}', command.requestType, 'body.options.user_scope', scope, command, env);
|
|
928
980
|
}
|
|
929
981
|
}
|
|
930
982
|
if (isTableSchemaCommand(scope, command)) {
|
|
@@ -983,7 +1035,7 @@ function validateActionSpecificBody(scope, command, flags, body, env) {
|
|
|
983
1035
|
const options = isRecord(body.options) ? body.options : undefined;
|
|
984
1036
|
const userScope = options && Array.isArray(options.user_scope) ? options.user_scope : undefined;
|
|
985
1037
|
if ('users' in body || 'allowedUsers' in body) {
|
|
986
|
-
throwBodyValidationFailure('access-rule assign-users only accepts body.options.user_scope with {"update":["user_scope"],"options":{"user_scope":[{"type":"user","id":2188889901}]}}', command.requestType, 'body.options.user_scope', scope, command, env);
|
|
1038
|
+
throwBodyValidationFailure('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}]}}', command.requestType, 'body.options.user_scope', scope, command, env);
|
|
987
1039
|
}
|
|
988
1040
|
if (!update.includes('user_scope')) {
|
|
989
1041
|
throwBodyValidationFailure('access-rule assign-users requires body.update to include "user_scope"', command.requestType, 'body.update', scope, command, env);
|
|
@@ -997,11 +1049,8 @@ function validateActionSpecificBody(scope, command, flags, body, env) {
|
|
|
997
1049
|
if (!isRecord(item)) {
|
|
998
1050
|
throwBodyValidationFailure('access-rule assign-users requires each body.options.user_scope item to be an object', command.requestType, `body.options.user_scope.${index}`, scope, command, env);
|
|
999
1051
|
}
|
|
1000
|
-
if (item.type !== 'user') {
|
|
1001
|
-
throwBodyValidationFailure('access-rule assign-users requires body.options.user_scope[].type to be "user"', command.requestType, `body.options.user_scope.${index}.type`, scope, command, env);
|
|
1002
|
-
}
|
|
1003
1052
|
if (typeof item.id !== 'number' || !Number.isInteger(item.id) || item.id <= 0) {
|
|
1004
|
-
throwBodyValidationFailure('access-rule assign-users requires body.options.user_scope[].id to be numeric arcubaseUserId', command.requestType, `body.options.user_scope.${index}.id`, scope, command, env);
|
|
1053
|
+
throwBodyValidationFailure('access-rule assign-users requires body.options.user_scope[].id to be numeric arcubaseUserId, department ID, or actor ID', command.requestType, `body.options.user_scope.${index}.id`, scope, command, env);
|
|
1005
1054
|
}
|
|
1006
1055
|
}
|
|
1007
1056
|
}
|
|
@@ -1638,16 +1687,32 @@ export async function executeCLI(scope, argv, env, fetchImpl = fetch) {
|
|
|
1638
1687
|
const parsed = parseCLIArgs(argv);
|
|
1639
1688
|
const envInput = env ?? process.env;
|
|
1640
1689
|
if (parsed.commandTokens.length === 0) {
|
|
1690
|
+
if (hasFlag(parsed.flags, 'help-examples')) {
|
|
1691
|
+
throwHelpExamplesRequiresCommand(scope);
|
|
1692
|
+
}
|
|
1641
1693
|
return { kind: 'help', text: renderRootHelp(scope) };
|
|
1642
1694
|
}
|
|
1643
1695
|
const [moduleName, commandName] = parsed.commandTokens;
|
|
1644
1696
|
if (!moduleName) {
|
|
1697
|
+
if (hasFlag(parsed.flags, 'help-examples')) {
|
|
1698
|
+
throwHelpExamplesRequiresCommand(scope);
|
|
1699
|
+
}
|
|
1645
1700
|
return { kind: 'help', text: renderRootHelp(scope) };
|
|
1646
1701
|
}
|
|
1647
1702
|
const singleTokenCommand = !commandName ? findCommand(scope, moduleName) : null;
|
|
1648
1703
|
if (!commandName && !singleTokenCommand) {
|
|
1704
|
+
if (hasFlag(parsed.flags, 'help-examples')) {
|
|
1705
|
+
throwHelpExamplesRequiresCommand(scope, moduleName);
|
|
1706
|
+
}
|
|
1649
1707
|
return { kind: 'help', text: renderModuleHelp(scope, moduleName, envInput) };
|
|
1650
1708
|
}
|
|
1709
|
+
if (hasFlag(parsed.flags, 'help-examples')) {
|
|
1710
|
+
const command = commandName ? findCommand(scope, moduleName, commandName) : singleTokenCommand;
|
|
1711
|
+
if (!command) {
|
|
1712
|
+
throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName}${commandName ? ` ${commandName}` : ''}`, 2, buildUnknownCommandDetails(scope, moduleName, commandName ?? ''));
|
|
1713
|
+
}
|
|
1714
|
+
return { kind: 'help', text: renderCommandHelpExamples(scope, command) };
|
|
1715
|
+
}
|
|
1651
1716
|
if (hasFlag(parsed.flags, 'help')) {
|
|
1652
1717
|
return { kind: 'help', text: renderCommandHelp(scope, moduleName, commandName, envInput) };
|
|
1653
1718
|
}
|
|
@@ -1688,6 +1753,7 @@ export async function executeCLI(scope, argv, env, fetchImpl = fetch) {
|
|
|
1688
1753
|
if (body === undefined) {
|
|
1689
1754
|
throw new CLIError('MISSING_BODY_FILE', `command requires --body-json or --body-file for ${command.requestType}; prefer --body-json to avoid file creation`, 2);
|
|
1690
1755
|
}
|
|
1756
|
+
body = normalizeNumericAccessRuleUserScopeIDs(scope, command, body);
|
|
1691
1757
|
if (isTableCreateCommand(scope, command)) {
|
|
1692
1758
|
requireTableCreateSchemaBody(scope, command, body, runtimeEnv);
|
|
1693
1759
|
validateActionSpecificRawBody(scope, command, body, runtimeEnv);
|
|
@@ -1714,7 +1780,8 @@ export async function executeCLI(scope, argv, env, fetchImpl = fetch) {
|
|
|
1714
1780
|
const message = parsedBody.error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`).join('; ');
|
|
1715
1781
|
if (isAssignUsersCommand(scope, command)) {
|
|
1716
1782
|
const hasUserScopeIDIssue = parsedBody.error.issues.some((issue) => issue.path.join('.') === 'options.user_scope.0.id' || issue.path.join('.').startsWith('options.user_scope.'));
|
|
1717
|
-
|
|
1783
|
+
const userScopeIDMessage = 'access-rule assign-users requires body.options.user_scope[].id to be numeric arcubaseUserId, department ID, or actor ID';
|
|
1784
|
+
throw new CLIError('BODY_VALIDATION_FAILED', hasUserScopeIDIssue ? userScopeIDMessage : message, 2, buildAssignUsersValidationDetails(scope, command, command.requestType, hasUserScopeIDIssue ? 'body.options.user_scope' : 'body', hasUserScopeIDIssue ? userScopeIDMessage : message, runtimeEnv));
|
|
1718
1785
|
}
|
|
1719
1786
|
throw new CLIError('BODY_VALIDATION_FAILED', message, 2, buildBodyValidationDetails(scope, command, command.requestType, parsedBody.error.issues.map((issue) => ({
|
|
1720
1787
|
path: issue.path.length > 0 ? `body.${issue.path.join('.')}` : 'body',
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CommandDef, CommandScope } from './command_registry.js';
|
|
2
|
+
type HelpExample = {
|
|
3
|
+
title: string;
|
|
4
|
+
body?: unknown;
|
|
5
|
+
notes?: readonly string[];
|
|
6
|
+
};
|
|
7
|
+
export declare function getCommandHelpExamples(scope: CommandScope, command: CommandDef): readonly HelpExample[];
|
|
8
|
+
export declare function renderCommandHelpExamples(scope: CommandScope, command: CommandDef): string;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=help_examples.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"help_examples.d.ts","sourceRoot":"","sources":["../../src/runtime/help_examples.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AAErE,KAAK,WAAW,GAAG;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAC1B,CAAA;AAyBD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,UAAU,GAAG,SAAS,WAAW,EAAE,CAEvG;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,UAAU,GAAG,MAAM,CAwB1F"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { helpExamplesIndex } from '../generated/help_examples.generated.js';
|
|
2
|
+
function binaryForScope(scope) {
|
|
3
|
+
return scope === 'admin' ? 'arcubase-admin' : 'arcubase';
|
|
4
|
+
}
|
|
5
|
+
function getFixedValue(value) {
|
|
6
|
+
if (!('fixedValue' in value)) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
const fixedValue = value.fixedValue;
|
|
10
|
+
return typeof fixedValue === 'string' ? fixedValue : undefined;
|
|
11
|
+
}
|
|
12
|
+
function pathFlagPlaceholders(command) {
|
|
13
|
+
const parts = command.pathParams
|
|
14
|
+
.filter((item) => !getFixedValue(item))
|
|
15
|
+
.map((item) => `--${item.flag} <${item.flag.replace(/-/g, '_')}>`);
|
|
16
|
+
return parts.length > 0 ? `${parts.join(' ')} ` : '';
|
|
17
|
+
}
|
|
18
|
+
function helpExampleKey(scope, command) {
|
|
19
|
+
return `${scope}.${command.commandPath.join('.')}`;
|
|
20
|
+
}
|
|
21
|
+
export function getCommandHelpExamples(scope, command) {
|
|
22
|
+
return helpExamplesIndex[helpExampleKey(scope, command)]?.examples ?? [];
|
|
23
|
+
}
|
|
24
|
+
export function renderCommandHelpExamples(scope, command) {
|
|
25
|
+
const binary = binaryForScope(scope);
|
|
26
|
+
const examples = getCommandHelpExamples(scope, command);
|
|
27
|
+
const header = `${binary} ${command.commandPath.join(' ')} --help-examples`;
|
|
28
|
+
if (examples.length === 0) {
|
|
29
|
+
return [
|
|
30
|
+
header,
|
|
31
|
+
'',
|
|
32
|
+
'No dedicated examples for this final command yet.',
|
|
33
|
+
`Use ${binary} ${command.commandPath.join(' ')} --help for flags, docs, and type paths.`,
|
|
34
|
+
].join('\n');
|
|
35
|
+
}
|
|
36
|
+
return [
|
|
37
|
+
header,
|
|
38
|
+
'',
|
|
39
|
+
...examples.flatMap((example) => [
|
|
40
|
+
`${example.title}:`,
|
|
41
|
+
...(example.body === undefined
|
|
42
|
+
? []
|
|
43
|
+
: [` ${binary} ${command.commandPath.join(' ')} ${pathFlagPlaceholders(command)}--body-json '${JSON.stringify(example.body)}'`.replace(/\s+/g, ' ').trim()]),
|
|
44
|
+
...(example.notes ?? []).map((note) => ` - ${note}`),
|
|
45
|
+
'',
|
|
46
|
+
]),
|
|
47
|
+
].join('\n').trimEnd();
|
|
48
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@carthooks/arcubase-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
4
4
|
"description": "Arcubase runtime CLI for admin and user command execution",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -22,9 +22,10 @@
|
|
|
22
22
|
"codegen:commands": "node scripts/generate-command-registry.mjs",
|
|
23
23
|
"codegen:zod": "node scripts/generate-zod-registry.mjs",
|
|
24
24
|
"codegen:type-index": "node scripts/generate-type-index.mjs",
|
|
25
|
-
"
|
|
25
|
+
"codegen:help-examples": "node scripts/generate-help-examples.mjs",
|
|
26
|
+
"bundle": "npm run codegen:commands && npm run codegen:zod && npm run codegen:type-index && npm run codegen:help-examples && tsc && node scripts/bundle-cli.mjs",
|
|
26
27
|
"build": "npm run bundle && npm run docs:llms",
|
|
27
|
-
"test": "npm run codegen:commands && npm run codegen:zod && npm run codegen:type-index && tsx --test src/tests/*.test.ts",
|
|
28
|
+
"test": "npm run codegen:commands && npm run codegen:zod && npm run codegen:type-index && npm run codegen:help-examples && tsx --test src/tests/*.test.ts",
|
|
28
29
|
"watch": "tsc --watch",
|
|
29
30
|
"type-check": "tsc --noEmit",
|
|
30
31
|
"docs:llms": "node -e \"\""
|
|
@@ -4,9 +4,9 @@ Use `arcubase-admin access-rule` for app/table access.
|
|
|
4
4
|
|
|
5
5
|
## One Path
|
|
6
6
|
|
|
7
|
-
1. Get the numeric `arcubaseUserId
|
|
7
|
+
1. Get the numeric `arcubaseUserId`, department ID, or actor ID.
|
|
8
8
|
2. Create an enabled rule with `options.policy`.
|
|
9
|
-
3. Assign
|
|
9
|
+
3. Assign user scope with `options.user_scope`.
|
|
10
10
|
4. Verify with `access-rule get`.
|
|
11
11
|
|
|
12
12
|
## User Mapping
|
|
@@ -17,8 +17,8 @@ arcubase-admin get-digiemployee-users
|
|
|
17
17
|
|
|
18
18
|
Use the numeric `arcubaseUserId`.
|
|
19
19
|
|
|
20
|
-
Do not use non-numeric
|
|
21
|
-
Use `user_scope[].type:"user"` for a tenant user. Do not use FieldVO field type `member` in access rules.
|
|
20
|
+
Do not use non-numeric BotWorks user ids in `user_scope[].id`.
|
|
21
|
+
Use `user_scope[].type:"user"` for a tenant user, `type:"department"` for a department, and `type:"actor"` for a role/actor. Do not use FieldVO field type `member` in access rules.
|
|
22
22
|
|
|
23
23
|
## Body Input
|
|
24
24
|
|
|
@@ -90,7 +90,7 @@ Rules:
|
|
|
90
90
|
- field key is not the FieldVO `key`; do not use values like `vote_description`
|
|
91
91
|
- preserve `list_options:{}` unless the existing rule has a known replacement
|
|
92
92
|
|
|
93
|
-
## Assign
|
|
93
|
+
## Assign User Scope Only
|
|
94
94
|
|
|
95
95
|
Use this only when the rule already has the correct `policy`.
|
|
96
96
|
|
|
@@ -99,16 +99,18 @@ arcubase-admin access-rule assign-users \
|
|
|
99
99
|
--app-id 2188893436 \
|
|
100
100
|
--table-id 2188893443 \
|
|
101
101
|
--rule-id 2188893557 \
|
|
102
|
-
--body-json '{"update":["user_scope"],"options":{"user_scope":[{"type":"user","id":2188889901}]}}'
|
|
102
|
+
--body-json '{"update":["user_scope"],"options":{"user_scope":[{"type":"user","id":2188889901},{"type":"department","id":2188881201},{"type":"actor","id":2188883301}]}}'
|
|
103
103
|
```
|
|
104
104
|
|
|
105
105
|
Rules:
|
|
106
106
|
|
|
107
|
-
- `assign-users` preserves the existing policy.
|
|
107
|
+
- `assign-users` preserves the existing policy and only replaces `options.user_scope`.
|
|
108
108
|
- body must use `update:["user_scope"]`.
|
|
109
109
|
- body must use `options.user_scope`.
|
|
110
|
-
- `user_scope[].type`
|
|
111
|
-
- `user_scope[].id`
|
|
110
|
+
- `user_scope[].type` must be `user`, `department`, or `actor`.
|
|
111
|
+
- `user_scope[].id` must be the numeric `arcubaseUserId`, department ID, or actor ID.
|
|
112
|
+
- Multiple scope items are OR: any matching user, department membership, or actor membership can use the ingress.
|
|
113
|
+
- Department scope includes child departments because Arcubase matches the user's department path.
|
|
112
114
|
|
|
113
115
|
## Verify
|
|
114
116
|
|
|
@@ -122,7 +124,7 @@ arcubase-admin access-rule get \
|
|
|
122
124
|
Success:
|
|
123
125
|
|
|
124
126
|
- `enabled=true`
|
|
125
|
-
- `options.user_scope` contains
|
|
127
|
+
- `options.user_scope` contains every expected `type/id` scope item
|
|
126
128
|
- `options.policy.actions` contains `view`, `add`, `edit`
|
|
127
129
|
- `options.policy.fields` contains hidden fields as `{"key":":1002","read":false,"write":false,"report":false}`
|
|
128
130
|
- visible fields are explicitly present with `read/write=true` when `all_fields_read/write=false`
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"scope": "admin",
|
|
3
|
+
"command": ["access-rule", "assign-users"],
|
|
4
|
+
"examples": [
|
|
5
|
+
{
|
|
6
|
+
"title": "replace user_scope with user, department, and actor scopes",
|
|
7
|
+
"body": {
|
|
8
|
+
"update": ["user_scope"],
|
|
9
|
+
"options": {
|
|
10
|
+
"user_scope": [
|
|
11
|
+
{"type": "user", "id": 2188889901},
|
|
12
|
+
{"type": "department", "id": 2188881201},
|
|
13
|
+
{"type": "actor", "id": 2188883301}
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"notes": [
|
|
18
|
+
"id must be numeric arcubaseUserId, department ID, or actor ID",
|
|
19
|
+
"assign-users updates user_scope only and preserves the existing policy"
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
{
|
|
2
|
+
"scope": "admin",
|
|
3
|
+
"command": ["access-rule", "create"],
|
|
4
|
+
"examples": [
|
|
5
|
+
{
|
|
6
|
+
"title": "full access for a specific arcubaseUserId",
|
|
7
|
+
"body": {
|
|
8
|
+
"name": "Admin full access",
|
|
9
|
+
"enabled": true,
|
|
10
|
+
"type": "form",
|
|
11
|
+
"options": {
|
|
12
|
+
"user_scope": [{"type": "user", "id": 2188889977}],
|
|
13
|
+
"policy": {
|
|
14
|
+
"key": "custom",
|
|
15
|
+
"actions": ["view", "add", "edit", "delete"],
|
|
16
|
+
"all_fields_read": true,
|
|
17
|
+
"all_fields_write": true
|
|
18
|
+
},
|
|
19
|
+
"list_options": {}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"title": "department can view/add/edit only rows created by themselves",
|
|
25
|
+
"body": {
|
|
26
|
+
"name": "Sales own rows",
|
|
27
|
+
"enabled": true,
|
|
28
|
+
"type": "form",
|
|
29
|
+
"options": {
|
|
30
|
+
"user_scope": [{"type": "department", "id": 2188881201}],
|
|
31
|
+
"policy": {
|
|
32
|
+
"key": "custom",
|
|
33
|
+
"actions": ["view", "add", "edit"],
|
|
34
|
+
"all_fields_read": true,
|
|
35
|
+
"all_fields_write": true,
|
|
36
|
+
"condition": {
|
|
37
|
+
"mode": "and",
|
|
38
|
+
"conditions": [
|
|
39
|
+
{"column": "creator", "operator": "$eq", "value": "[[current_user]]", "value_from": 0, "value_relation": ""}
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"list_options": {}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"notes": [
|
|
47
|
+
"department scope includes child departments",
|
|
48
|
+
"row-owner isolation uses [[current_user]] on creator"
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"title": "role actor can view rows matching a complex AND/OR condition",
|
|
53
|
+
"body": {
|
|
54
|
+
"name": "Sales manager review queue",
|
|
55
|
+
"enabled": true,
|
|
56
|
+
"type": "form",
|
|
57
|
+
"options": {
|
|
58
|
+
"user_scope": [{"type": "actor", "id": 2188883301}],
|
|
59
|
+
"policy": {
|
|
60
|
+
"key": "custom",
|
|
61
|
+
"actions": ["view", "edit"],
|
|
62
|
+
"all_fields_read": true,
|
|
63
|
+
"all_fields_write": true,
|
|
64
|
+
"condition": {
|
|
65
|
+
"mode": "and",
|
|
66
|
+
"conditions": [
|
|
67
|
+
{"column": ":1004", "operator": "$eq", "value": "pending", "value_from": 0, "value_relation": ""},
|
|
68
|
+
{
|
|
69
|
+
"column": "",
|
|
70
|
+
"operator": "",
|
|
71
|
+
"value": null,
|
|
72
|
+
"value_from": 0,
|
|
73
|
+
"value_relation": "",
|
|
74
|
+
"subset": {
|
|
75
|
+
"mode": "or",
|
|
76
|
+
"conditions": [
|
|
77
|
+
{"column": "creator", "operator": "$eq", "value": "[[current_user]]", "value_from": 0, "value_relation": ""},
|
|
78
|
+
{"column": ":1005", "operator": "$eq", "value": "high", "value_from": 0, "value_relation": ""}
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"list_options": {}
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"notes": [
|
|
89
|
+
"actor means Arcubase role id",
|
|
90
|
+
"condition mode \"and\" and \"or\" are canonical; legacy \"all\" also reads as AND",
|
|
91
|
+
"nested OR uses subset and leaves the wrapper column/operator empty"
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"scope": "user",
|
|
3
|
+
"command": ["row", "bulk-update"],
|
|
4
|
+
"examples": [
|
|
5
|
+
{
|
|
6
|
+
"title": "bulk update explicit row ids",
|
|
7
|
+
"body": {
|
|
8
|
+
"selection": {"type": "ids", "ids": [2188890411, 2188890412], "length": 2},
|
|
9
|
+
"fields": [{"id": 1003, "action": {"type": "set", "value": 2}}]
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"title": "bulk update rows selected by a search condition",
|
|
14
|
+
"body": {
|
|
15
|
+
"selection": {"type": "condition", "condition": {"selectAll": true, "q": "SO-1001"}, "length": 0},
|
|
16
|
+
"fields": [{"id": 1003, "action": {"type": "set", "value": 2}}]
|
|
17
|
+
},
|
|
18
|
+
"notes": [
|
|
19
|
+
"condition selection uses row-query style keys such as q, tab, and filter_:1002"
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"scope": "user",
|
|
3
|
+
"command": ["row", "selection-action"],
|
|
4
|
+
"examples": [
|
|
5
|
+
{
|
|
6
|
+
"title": "run a custom action against explicit row ids",
|
|
7
|
+
"body": {
|
|
8
|
+
"selection": {"type": "ids", "ids": [2188890411], "length": 1}
|
|
9
|
+
},
|
|
10
|
+
"notes": [
|
|
11
|
+
"pass the backend action code with --action",
|
|
12
|
+
"for query action, use ids or all selection instead of condition selection"
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"title": "run a custom action against a filtered selection",
|
|
17
|
+
"body": {
|
|
18
|
+
"selection": {"type": "condition", "condition": {"selectAll": true, "q": "SO-1001"}, "length": 0}
|
|
19
|
+
},
|
|
20
|
+
"notes": [
|
|
21
|
+
"condition selection uses row-query style keys such as q, tab, and filter_:1002"
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|