@carthooks/arcubase-cli 0.1.19 → 0.1.21

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 (44) hide show
  1. package/bundle/arcubase-admin.mjs +727 -45
  2. package/bundle/arcubase.mjs +727 -45
  3. package/dist/generated/command_registry.generated.d.ts +15 -0
  4. package/dist/generated/command_registry.generated.d.ts.map +1 -1
  5. package/dist/generated/command_registry.generated.js +21 -0
  6. package/dist/generated/help_examples.generated.d.ts +319 -0
  7. package/dist/generated/help_examples.generated.d.ts.map +1 -0
  8. package/dist/generated/help_examples.generated.js +437 -0
  9. package/dist/generated/type_index.generated.d.ts +4 -0
  10. package/dist/generated/type_index.generated.d.ts.map +1 -1
  11. package/dist/generated/type_index.generated.js +4 -0
  12. package/dist/generated/zod_registry.generated.d.ts +4 -1
  13. package/dist/generated/zod_registry.generated.d.ts.map +1 -1
  14. package/dist/generated/zod_registry.generated.js +4 -0
  15. package/dist/runtime/execute.d.ts.map +1 -1
  16. package/dist/runtime/execute.js +155 -17
  17. package/dist/runtime/help_examples.d.ts +10 -0
  18. package/dist/runtime/help_examples.d.ts.map +1 -0
  19. package/dist/runtime/help_examples.js +48 -0
  20. package/dist/runtime/zod_registry.d.ts.map +1 -1
  21. package/dist/runtime/zod_registry.js +8 -0
  22. package/package.json +4 -3
  23. package/sdk-dist/api/admin/ingress.ts +11 -0
  24. package/sdk-dist/docs/runtime-reference/access-rule.md +19 -0
  25. package/sdk-dist/docs/runtime-reference/help-examples/admin/access-rule/assign-users.json +23 -0
  26. package/sdk-dist/docs/runtime-reference/help-examples/admin/access-rule/bulk-apply.json +105 -0
  27. package/sdk-dist/docs/runtime-reference/help-examples/admin/access-rule/create.json +95 -0
  28. package/sdk-dist/docs/runtime-reference/help-examples/user/row/bulk-update.json +23 -0
  29. package/sdk-dist/docs/runtime-reference/help-examples/user/row/selection-action.json +25 -0
  30. package/sdk-dist/generated/command_registry.generated.ts +21 -0
  31. package/sdk-dist/generated/help_examples.generated.ts +444 -0
  32. package/sdk-dist/generated/type_index.generated.ts +4 -0
  33. package/sdk-dist/generated/zod_registry.generated.ts +6 -0
  34. package/sdk-dist/types/ingress.ts +31 -0
  35. package/src/generated/command_registry.generated.ts +21 -0
  36. package/src/generated/help_examples.generated.ts +444 -0
  37. package/src/generated/type_index.generated.ts +4 -0
  38. package/src/generated/zod_registry.generated.ts +6 -0
  39. package/src/runtime/execute.ts +223 -37
  40. package/src/runtime/help_examples.ts +61 -0
  41. package/src/runtime/zod_registry.ts +8 -0
  42. package/src/tests/command_registry.test.ts +1 -0
  43. package/src/tests/execute_validation.test.ts +113 -0
  44. package/src/tests/help.test.ts +45 -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:',
@@ -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);
@@ -555,6 +577,9 @@ function buildTableCreateExampleBody() {
555
577
  function isAssignUsersCommand(scope, command) {
556
578
  return scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'assign-users';
557
579
  }
580
+ function isAccessRuleBulkApplyCommand(scope, command) {
581
+ return scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'bulk-apply';
582
+ }
558
583
  function isTableCreateCommand(scope, command) {
559
584
  return scope === 'admin' && command.commandPath[0] === 'table' && command.commandPath[1] === 'create';
560
585
  }
@@ -914,6 +939,97 @@ function validateAccessRuleUserScope(scope, command, requestType, userScope, env
914
939
  }
915
940
  }
916
941
  }
942
+ function normalizeNumericAccessRuleUserScopeIDs(scope, command, body) {
943
+ if (!isAccessRuleCommand(scope, command) || !isRecord(body)) {
944
+ return body;
945
+ }
946
+ const normalizeUserScope = (userScope) => {
947
+ if (!Array.isArray(userScope))
948
+ return { value: userScope, changed: false };
949
+ let changed = false;
950
+ const value = userScope.map((item) => {
951
+ if (!isRecord(item) || typeof item.id !== 'string' || !/^\d+$/.test(item.id)) {
952
+ return item;
953
+ }
954
+ const id = Number(item.id);
955
+ if (!Number.isFinite(id) || !Number.isInteger(id)) {
956
+ return item;
957
+ }
958
+ changed = true;
959
+ return { ...item, id };
960
+ });
961
+ return { value, changed };
962
+ };
963
+ const normalizeNumericID = (value) => {
964
+ if (typeof value !== 'string' || !/^\d+$/.test(value))
965
+ return { value, changed: false };
966
+ const id = Number(value);
967
+ if (!Number.isFinite(id) || !Number.isInteger(id))
968
+ return { value, changed: false };
969
+ return { value: id, changed: true };
970
+ };
971
+ if (isAccessRuleBulkApplyCommand(scope, command) && Array.isArray(body.items)) {
972
+ let changed = false;
973
+ const items = body.items.map((item) => {
974
+ if (!isRecord(item))
975
+ return item;
976
+ let nextItem = item;
977
+ for (const key of ['table_id', 'rule_id']) {
978
+ const normalized = normalizeNumericID(nextItem[key]);
979
+ if (normalized.changed) {
980
+ nextItem = { ...nextItem, [key]: normalized.value };
981
+ changed = true;
982
+ }
983
+ }
984
+ if (isRecord(nextItem.options)) {
985
+ const normalizedUserScope = normalizeUserScope(nextItem.options.user_scope);
986
+ if (normalizedUserScope.changed) {
987
+ nextItem = {
988
+ ...nextItem,
989
+ options: {
990
+ ...nextItem.options,
991
+ user_scope: normalizedUserScope.value,
992
+ },
993
+ };
994
+ changed = true;
995
+ }
996
+ }
997
+ return nextItem;
998
+ });
999
+ return changed ? { ...body, items } : body;
1000
+ }
1001
+ if (!isRecord(body.options)) {
1002
+ return body;
1003
+ }
1004
+ const normalizedUserScope = normalizeUserScope(body.options.user_scope);
1005
+ if (!normalizedUserScope.changed)
1006
+ return body;
1007
+ return {
1008
+ ...body,
1009
+ options: {
1010
+ ...body.options,
1011
+ user_scope: normalizedUserScope.value,
1012
+ },
1013
+ };
1014
+ }
1015
+ function validateAccessRulePolicyShape(scope, command, requestType, policy, env, pathPrefix) {
1016
+ const actions = policy && Array.isArray(policy.actions) ? policy.actions : [];
1017
+ if (actions.includes('read') || actions.includes('write')) {
1018
+ throwBodyValidationFailure('access-rule policy.actions must use Arcubase action codes: use "view" for read and "add","edit" for write; do not use "read", "write", or "insert"', requestType, `${pathPrefix}.actions`, scope, command, env);
1019
+ }
1020
+ const fields = policy && Array.isArray(policy.fields) ? policy.fields : [];
1021
+ const allowedSystemFieldKeys = new Set(['created', 'updated', 'creator']);
1022
+ for (const [index, field] of fields.entries()) {
1023
+ if (!isRecord(field))
1024
+ continue;
1025
+ if (typeof field.key === 'number' || (typeof field.key === 'string' && /^\d+$/.test(field.key))) {
1026
+ throwBodyValidationFailure('access-rule policy.fields[].key must be a string prefixed with colon, for example ":1002"; do not use raw numbers', requestType, `${pathPrefix}.fields.${index}.key`, scope, command, env);
1027
+ }
1028
+ if (typeof field.key === 'string' && !field.key.startsWith(':') && !allowedSystemFieldKeys.has(field.key)) {
1029
+ throwBodyValidationFailure('access-rule policy.fields[].key must use Arcubase permission keys such as ":1002"; do not use FieldVO.key values such as "vote_name"', requestType, `${pathPrefix}.fields.${index}.key`, scope, command, env);
1030
+ }
1031
+ }
1032
+ }
917
1033
  function requireUpdateContains(scope, command, body, field, env) {
918
1034
  if (!(field in body))
919
1035
  return;
@@ -939,6 +1055,26 @@ function validateActionSpecificRawBody(scope, command, body, env) {
939
1055
  }
940
1056
  }
941
1057
  if (isAccessRuleCommand(scope, command)) {
1058
+ if (isAccessRuleBulkApplyCommand(scope, command)) {
1059
+ if (!Array.isArray(body.items) || body.items.length === 0) {
1060
+ throwBodyValidationFailure('access-rule bulk-apply requires non-empty body.items; put every table/rule assignment in body.items', command.requestType, 'body.items', scope, command, env);
1061
+ }
1062
+ if (body.mode !== undefined && body.mode !== 'update_existing' && body.mode !== 'upsert_by_name') {
1063
+ throwBodyValidationFailure('access-rule bulk-apply body.mode must be "update_existing" or "upsert_by_name"', command.requestType, 'body.mode', scope, command, env);
1064
+ }
1065
+ for (const [index, item] of body.items.entries()) {
1066
+ if (!isRecord(item)) {
1067
+ throwBodyValidationFailure('access-rule bulk-apply body.items[] must be an object', command.requestType, `body.items.${index}`, scope, command, env);
1068
+ }
1069
+ const options = isRecord(item.options) ? item.options : undefined;
1070
+ if (!options) {
1071
+ throwBodyValidationFailure('access-rule bulk-apply body.items[].options is required and must include the full canonical options object', command.requestType, `body.items.${index}.options`, scope, command, env);
1072
+ }
1073
+ validateAccessRuleUserScope(scope, command, command.requestType, options.user_scope, env, `body.items.${index}.options.user_scope`);
1074
+ validateAccessRulePolicyShape(scope, command, command.requestType, isRecord(options.policy) ? options.policy : undefined, env, `body.items.${index}.options.policy`);
1075
+ }
1076
+ return;
1077
+ }
942
1078
  const update = stringArray(body.update);
943
1079
  if (isAccessRuleUpdateCommand(scope, command) && update.some((item) => item.startsWith('options.'))) {
944
1080
  throwBodyValidationFailure('access-rule update cannot patch nested options paths. Use {"update":["options"],"options":{"policy":{"key":"custom","actions":["view","add","edit"],"all_fields_read":true,"all_fields_write":true,"fields":[{"key":":1002","read":false,"write":false,"report":false}]},"list_options":{}}}', command.requestType, 'body.update', scope, command, env);
@@ -955,22 +1091,7 @@ function validateActionSpecificRawBody(scope, command, body, env) {
955
1091
  throwBodyValidationFailure('access-rule update with update:["options"] cannot change only options.user_scope because it overwrites policy. Use access-rule assign-users for user assignment, or include the full options.policy and options.list_options.', command.requestType, 'body.options.policy', scope, command, env);
956
1092
  }
957
1093
  validateAccessRuleUserScope(scope, command, command.requestType, options?.user_scope, env);
958
- const actions = policy && Array.isArray(policy.actions) ? policy.actions : [];
959
- if (actions.includes('read') || actions.includes('write')) {
960
- throwBodyValidationFailure('access-rule policy.actions must use Arcubase action codes: use "view" for read and "add","edit" for write; do not use "read", "write", or "insert"', command.requestType, 'body.options.policy.actions', scope, command, env);
961
- }
962
- const fields = policy && Array.isArray(policy.fields) ? policy.fields : [];
963
- const allowedSystemFieldKeys = new Set(['created', 'updated', 'creator']);
964
- for (const [index, field] of fields.entries()) {
965
- if (!isRecord(field))
966
- continue;
967
- if (typeof field.key === 'number' || (typeof field.key === 'string' && /^\d+$/.test(field.key))) {
968
- throwBodyValidationFailure('access-rule policy.fields[].key must be a string prefixed with colon, for example ":1002"; do not use raw numbers', command.requestType, `body.options.policy.fields.${index}.key`, scope, command, env);
969
- }
970
- if (typeof field.key === 'string' && !field.key.startsWith(':') && !allowedSystemFieldKeys.has(field.key)) {
971
- throwBodyValidationFailure('access-rule policy.fields[].key must use Arcubase permission keys such as ":1002"; do not use FieldVO.key values such as "vote_name"', command.requestType, `body.options.policy.fields.${index}.key`, scope, command, env);
972
- }
973
- }
1094
+ validateAccessRulePolicyShape(scope, command, command.requestType, policy, env, 'body.options.policy');
974
1095
  }
975
1096
  }
976
1097
  function validateActionSpecificBody(scope, command, flags, body, env) {
@@ -1639,16 +1760,32 @@ export async function executeCLI(scope, argv, env, fetchImpl = fetch) {
1639
1760
  const parsed = parseCLIArgs(argv);
1640
1761
  const envInput = env ?? process.env;
1641
1762
  if (parsed.commandTokens.length === 0) {
1763
+ if (hasFlag(parsed.flags, 'help-examples')) {
1764
+ throwHelpExamplesRequiresCommand(scope);
1765
+ }
1642
1766
  return { kind: 'help', text: renderRootHelp(scope) };
1643
1767
  }
1644
1768
  const [moduleName, commandName] = parsed.commandTokens;
1645
1769
  if (!moduleName) {
1770
+ if (hasFlag(parsed.flags, 'help-examples')) {
1771
+ throwHelpExamplesRequiresCommand(scope);
1772
+ }
1646
1773
  return { kind: 'help', text: renderRootHelp(scope) };
1647
1774
  }
1648
1775
  const singleTokenCommand = !commandName ? findCommand(scope, moduleName) : null;
1649
1776
  if (!commandName && !singleTokenCommand) {
1777
+ if (hasFlag(parsed.flags, 'help-examples')) {
1778
+ throwHelpExamplesRequiresCommand(scope, moduleName);
1779
+ }
1650
1780
  return { kind: 'help', text: renderModuleHelp(scope, moduleName, envInput) };
1651
1781
  }
1782
+ if (hasFlag(parsed.flags, 'help-examples')) {
1783
+ const command = commandName ? findCommand(scope, moduleName, commandName) : singleTokenCommand;
1784
+ if (!command) {
1785
+ throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName}${commandName ? ` ${commandName}` : ''}`, 2, buildUnknownCommandDetails(scope, moduleName, commandName ?? ''));
1786
+ }
1787
+ return { kind: 'help', text: renderCommandHelpExamples(scope, command) };
1788
+ }
1652
1789
  if (hasFlag(parsed.flags, 'help')) {
1653
1790
  return { kind: 'help', text: renderCommandHelp(scope, moduleName, commandName, envInput) };
1654
1791
  }
@@ -1689,6 +1826,7 @@ export async function executeCLI(scope, argv, env, fetchImpl = fetch) {
1689
1826
  if (body === undefined) {
1690
1827
  throw new CLIError('MISSING_BODY_FILE', `command requires --body-json or --body-file for ${command.requestType}; prefer --body-json to avoid file creation`, 2);
1691
1828
  }
1829
+ body = normalizeNumericAccessRuleUserScopeIDs(scope, command, body);
1692
1830
  if (isTableCreateCommand(scope, command)) {
1693
1831
  requireTableCreateSchemaBody(scope, command, body, runtimeEnv);
1694
1832
  validateActionSpecificRawBody(scope, command, body, runtimeEnv);
@@ -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
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"zod_registry.d.ts","sourceRoot":"","sources":["../../src/runtime/zod_registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,KAAK,CAAA;AA8BrC,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAOjE;AAED,wBAAgB,8BAA8B,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAI9E;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,CAQ9D;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,KAAK,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;AAyGlD,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,GAAE,QAAsB,GAAG,eAAe,EAAE,CAqB7F;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,GAAE,QAAsB,GAAG,cAAc,EAAE,CAI3F;AAwED,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,GAAE,QAAsB,GAAG,cAAc,EAAE,CAInG"}
1
+ {"version":3,"file":"zod_registry.d.ts","sourceRoot":"","sources":["../../src/runtime/zod_registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,KAAK,CAAA;AA8BrC,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAOjE;AAED,wBAAgB,8BAA8B,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAI9E;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,CAQ9D;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,KAAK,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;AA6GlD,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,GAAE,QAAsB,GAAG,eAAe,EAAE,CAqB7F;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,GAAE,QAAsB,GAAG,cAAc,EAAE,CAI3F;AA4ED,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,GAAE,QAAsB,GAAG,cAAc,EAAE,CAInG"}
@@ -79,6 +79,10 @@ const docHintIndex = {
79
79
  { title: 'Access rule', file: 'docs/runtime-reference/access-rule.md' },
80
80
  examplesIndexDocHint,
81
81
  ],
82
+ AppIngressBulkApplyReqVO: [
83
+ { title: 'Access rule', file: 'docs/runtime-reference/access-rule.md' },
84
+ examplesIndexDocHint,
85
+ ],
82
86
  EntitySaveReqVO: [
83
87
  { title: 'Table schema cheatsheet', file: 'docs/runtime-reference/entity-schema.md' },
84
88
  { title: 'Field type index', file: 'docs/runtime-reference/entity-schema/README.md' },
@@ -197,6 +201,10 @@ const commandDocHintIndex = {
197
201
  { title: 'Access rule', file: 'docs/runtime-reference/access-rule.md' },
198
202
  examplesIndexDocHint,
199
203
  ],
204
+ 'admin.access-rule.bulkApplyEntityIngress': [
205
+ { title: 'Access rule', file: 'docs/runtime-reference/access-rule.md' },
206
+ examplesIndexDocHint,
207
+ ],
200
208
  'user.row.insertEntity': [
201
209
  { title: 'Row CRUD', file: 'docs/runtime-reference/row-crud.md' },
202
210
  { title: 'Uploads', file: 'docs/runtime-reference/uploads.md' },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carthooks/arcubase-cli",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
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 \"\""
@@ -7,6 +7,8 @@ import type {
7
7
  AppIngressGetBlocksRespVO,
8
8
  AppIngressGetByHashIDRespVO,
9
9
  AppIngressListRespVO,
10
+ AppIngressBulkApplyReqVO,
11
+ AppIngressBulkApplyRespVO,
10
12
  AppIngressUpdateReqVO,
11
13
  AppIngressUpdateRespVO,
12
14
  AppIngressUpdateBlocksReqVO,
@@ -83,6 +85,15 @@ export async function getEntityIngressList(
83
85
  return getDefaultClient().get(`/apps/${appId}/entity/${entityId}/ingress`)
84
86
  }
85
87
 
88
+ // @endpoint POST /apps/:app_id/access-rules/bulk-apply
89
+ // @controller App.IngressBulkApply
90
+ export async function bulkApplyEntityIngress(
91
+ appId: string,
92
+ data: AppIngressBulkApplyReqVO
93
+ ): Promise<AppIngressBulkApplyRespVO> {
94
+ return getDefaultClient().post(`/apps/${appId}/access-rules/bulk-apply`, data)
95
+ }
96
+
86
97
  // @endpoint PUT /apps/:app_id/entity/:entity_id/ingress/:id
87
98
  // @controller App.IngressUpdate
88
99
  export async function updateEntityIngress(
@@ -112,6 +112,25 @@ Rules:
112
112
  - Multiple scope items are OR: any matching user, department membership, or actor membership can use the ingress.
113
113
  - Department scope includes child departments because Arcubase matches the user's department path.
114
114
 
115
+ ## Bulk Apply Across Tables
116
+
117
+ Use this when assigning or updating many access rules for one app. This is the single path for matrix-style permission setup. Do not loop `access-rule list` and `access-rule update` for every table/rule.
118
+
119
+ ```bash
120
+ arcubase-admin access-rule bulk-apply \
121
+ --app-id 2188893436 \
122
+ --body-json '{"mode":"upsert_by_name","items":[{"table_id":2188893443,"name":"Sales read write","enabled":true,"type":"form","options":{"user_scope":[{"type":"department","id":2188881201}],"policy":{"key":"custom","actions":["view","add","edit"],"all_fields_read":true,"all_fields_write":true,"condition":{"mode":"and","conditions":[{"column":"creator","operator":"$eq","value":"[[current_user]]","value_from":0,"value_relation":""}]}},"list_options":{}}},{"table_id":2188893444,"name":"Sales readonly","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":{}}}]}'
123
+ ```
124
+
125
+ Rules:
126
+
127
+ - `bulk-apply` runs in one backend transaction.
128
+ - `mode:"update_existing"` requires every item to include `rule_id`.
129
+ - `mode:"upsert_by_name"` requires every item to include `name` and must not include `rule_id`.
130
+ - Each item must include `table_id` and the full canonical `options` object.
131
+ - Numeric IDs passed as strings are normalized by the CLI for `table_id`, `rule_id`, and `options.user_scope[].id`.
132
+ - Use `dry_run:true` to preview the exact operations without writing.
133
+
115
134
  ## Verify
116
135
 
117
136
  ```bash
@@ -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,105 @@
1
+ {
2
+ "scope": "admin",
3
+ "command": ["access-rule", "bulk-apply"],
4
+ "examples": [
5
+ {
6
+ "title": "upsert permission matrix by rule name in one transaction",
7
+ "body": {
8
+ "mode": "upsert_by_name",
9
+ "items": [
10
+ {
11
+ "table_id": 2188893443,
12
+ "name": "Customer archive - sales owns rows",
13
+ "enabled": true,
14
+ "type": "form",
15
+ "options": {
16
+ "user_scope": [{"type": "department", "id": 2188881201}],
17
+ "policy": {
18
+ "key": "custom",
19
+ "actions": ["view", "add", "edit"],
20
+ "all_fields_read": true,
21
+ "all_fields_write": true,
22
+ "condition": {
23
+ "mode": "and",
24
+ "conditions": [
25
+ {"column": "creator", "operator": "$eq", "value": "[[current_user]]", "value_from": 0, "value_relation": ""}
26
+ ]
27
+ }
28
+ },
29
+ "list_options": {}
30
+ }
31
+ },
32
+ {
33
+ "table_id": 2188893444,
34
+ "name": "Lead review - manager queue",
35
+ "enabled": true,
36
+ "type": "form",
37
+ "options": {
38
+ "user_scope": [{"type": "actor", "id": 2188883301}],
39
+ "policy": {
40
+ "key": "custom",
41
+ "actions": ["view", "edit"],
42
+ "all_fields_read": true,
43
+ "all_fields_write": true,
44
+ "condition": {
45
+ "mode": "and",
46
+ "conditions": [
47
+ {"column": ":1004", "operator": "$eq", "value": "pending", "value_from": 0, "value_relation": ""},
48
+ {
49
+ "column": "",
50
+ "operator": "",
51
+ "value": null,
52
+ "value_from": 0,
53
+ "value_relation": "",
54
+ "subset": {
55
+ "mode": "or",
56
+ "conditions": [
57
+ {"column": "creator", "operator": "$eq", "value": "[[current_user]]", "value_from": 0, "value_relation": ""},
58
+ {"column": ":1005", "operator": "$eq", "value": "high", "value_from": 0, "value_relation": ""}
59
+ ]
60
+ }
61
+ }
62
+ ]
63
+ }
64
+ },
65
+ "list_options": {}
66
+ }
67
+ }
68
+ ]
69
+ },
70
+ "notes": [
71
+ "mode upsert_by_name is for idempotent setup; rule names must be unique within each table",
72
+ "department, actor, and specific user scopes all use options.user_scope",
73
+ "row-owner isolation uses [[current_user]] in policy.condition"
74
+ ]
75
+ },
76
+ {
77
+ "title": "preview updates for existing rule ids",
78
+ "body": {
79
+ "mode": "update_existing",
80
+ "dry_run": true,
81
+ "items": [
82
+ {
83
+ "table_id": 2188893443,
84
+ "rule_id": 2188893557,
85
+ "enabled": true,
86
+ "options": {
87
+ "user_scope": [{"type": "user", "id": 2188889901}],
88
+ "policy": {
89
+ "key": "custom",
90
+ "actions": ["view"],
91
+ "all_fields_read": true,
92
+ "all_fields_write": false
93
+ },
94
+ "list_options": {}
95
+ }
96
+ }
97
+ ]
98
+ },
99
+ "notes": [
100
+ "dry_run true returns the planned operations without writing",
101
+ "update_existing requires rule_id on every item"
102
+ ]
103
+ }
104
+ ]
105
+ }
@@ -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
+ }