@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.
Files changed (31) hide show
  1. package/bundle/arcubase-admin.mjs +423 -34
  2. package/bundle/arcubase.mjs +423 -34
  3. package/dist/generated/help_examples.generated.d.ts +205 -0
  4. package/dist/generated/help_examples.generated.d.ts.map +1 -0
  5. package/dist/generated/help_examples.generated.js +282 -0
  6. package/dist/generated/zod_registry.generated.d.ts.map +1 -1
  7. package/dist/generated/zod_registry.generated.js +1 -1
  8. package/dist/runtime/execute.d.ts.map +1 -1
  9. package/dist/runtime/execute.js +84 -17
  10. package/dist/runtime/help_examples.d.ts +10 -0
  11. package/dist/runtime/help_examples.d.ts.map +1 -0
  12. package/dist/runtime/help_examples.js +48 -0
  13. package/package.json +4 -3
  14. package/sdk-dist/docs/runtime-reference/access-rule.md +12 -10
  15. package/sdk-dist/docs/runtime-reference/help-examples/admin/access-rule/assign-users.json +23 -0
  16. package/sdk-dist/docs/runtime-reference/help-examples/admin/access-rule/create.json +95 -0
  17. package/sdk-dist/docs/runtime-reference/help-examples/user/row/bulk-update.json +23 -0
  18. package/sdk-dist/docs/runtime-reference/help-examples/user/row/selection-action.json +25 -0
  19. package/sdk-dist/generated/help_examples.generated.ts +289 -0
  20. package/sdk-dist/generated/zod_registry.generated.ts +1 -1
  21. package/sdk-dist/types/common.ts +1 -0
  22. package/sdk-dist/types/entity.ts +1 -1
  23. package/sdk-dist/types/kiosk-ui.ts +1 -1
  24. package/sdk-dist/types/tenant.ts +1 -1
  25. package/src/generated/help_examples.generated.ts +289 -0
  26. package/src/generated/zod_registry.generated.ts +1 -1
  27. package/src/runtime/execute.ts +98 -25
  28. package/src/runtime/help_examples.ts +61 -0
  29. package/src/tests/execute_validation.test.ts +88 -10
  30. package/src/tests/help.test.ts +35 -1
  31. package/src/tests/zod_registry.test.ts +38 -0
@@ -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" for tenant users',
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 the assigned numeric arcubaseUserId values',
127
- ' - PermitUserScope.id must be the numeric arcubaseUserId',
128
- ' - Do not copy a non-numeric arcubaseUserId into PermitUserScope.id',
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: [{ type: 'user', id: 2188889901 }],
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 arcubaseUserId as id',
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
- throw new CLIError('BODY_VALIDATION_FAILED', hasUserScopeIDIssue ? 'access-rule assign-users requires body.options.user_scope[].id to be numeric arcubaseUserId' : message, 2, buildAssignUsersValidationDetails(scope, command, command.requestType, hasUserScopeIDIssue ? 'body.options.user_scope' : 'body', hasUserScopeIDIssue ? 'access-rule assign-users requires body.options.user_scope[].id to be numeric arcubaseUserId' : message, runtimeEnv));
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.18",
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
- "bundle": "npm run codegen:commands && npm run codegen:zod && npm run codegen:type-index && tsc && node scripts/bundle-cli.mjs",
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 users with `options.user_scope`.
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 Arcubase user ids in `user_scope[].id`.
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 Users Only
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` is `user` for Arcubase organization users.
111
- - `user_scope[].id` is the numeric `arcubaseUserId`.
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 the numeric user id
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
+ }