@carthooks/arcubase-cli 0.1.8 → 0.1.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carthooks/arcubase-cli",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Arcubase runtime CLI for admin and user command execution",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -93,6 +93,23 @@ export function renderCommandHelp(scope: CommandScope, moduleName: string, comma
93
93
  ' - row query: arcubase row query --app-id <app_id> --table-id <table_id> --body-file query.json',
94
94
  ]
95
95
  : []),
96
+ ...(scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'update'
97
+ ? [
98
+ 'body-json examples:',
99
+ ' - enable: {"update":["enabled"],"enabled":true}',
100
+ ' - rename: {"update":["name"],"name":"Sales read only"}',
101
+ ' - update options: {"update":["options"],"options":{"user_scope":[{"type":"user","id":123}]}}',
102
+ ]
103
+ : []),
104
+ ...(scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'assign-users'
105
+ ? [
106
+ 'body-json example:',
107
+ ' - {"update":["user_scope"],"options":{"user_scope":[{"type":"user","id":123}]}}',
108
+ 'success requires:',
109
+ ' - result.enabled must be true',
110
+ ' - result.options.user_scope must contain the assigned user ids',
111
+ ]
112
+ : []),
96
113
  ...(docHints.length > 0
97
114
  ? ['docs:', ...docHints.map((item) => ` - ${item.title}: ${item.file}`)]
98
115
  : []),
@@ -333,6 +350,51 @@ function throwBodyValidationFailure(message: string, requestType: string, path:
333
350
  })
334
351
  }
335
352
 
353
+ function stringArray(value: unknown): string[] {
354
+ return Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : []
355
+ }
356
+
357
+ function requireUpdateContains(
358
+ scope: CommandScope,
359
+ command: NonNullable<ReturnType<typeof findCommand>>,
360
+ body: Record<string, unknown>,
361
+ field: string,
362
+ ) {
363
+ if (!(field in body)) return
364
+ const update = stringArray(body.update)
365
+ if (!update.includes(field)) {
366
+ throwBodyValidationFailure(
367
+ `body.update must include "${field}" when body.${field} is present. Example: {"update":["${field}"],"${field}":${field === 'enabled' ? 'true' : '"value"'}}`,
368
+ command.requestType ?? 'AppIngressUpdateReqVO',
369
+ 'body.update',
370
+ scope,
371
+ command,
372
+ )
373
+ }
374
+ }
375
+
376
+ function validateActionSpecificRawBody(
377
+ scope: CommandScope,
378
+ command: NonNullable<ReturnType<typeof findCommand>>,
379
+ body: unknown,
380
+ ) {
381
+ if (!isRecord(body) || !command.requestType) {
382
+ return
383
+ }
384
+
385
+ if (scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'assign-users') {
386
+ if ('users' in body || 'allowedUsers' in body) {
387
+ throwBodyValidationFailure(
388
+ 'access-rule assign-users does not accept users or allowedUsers; use body.options.user_scope with {"update":["user_scope"],"options":{"user_scope":[{"type":"user","id":123}]}}',
389
+ command.requestType,
390
+ 'body.options.user_scope',
391
+ scope,
392
+ command,
393
+ )
394
+ }
395
+ }
396
+ }
397
+
336
398
  function validateActionSpecificBody(
337
399
  scope: CommandScope,
338
400
  command: NonNullable<ReturnType<typeof findCommand>>,
@@ -343,6 +405,45 @@ function validateActionSpecificBody(
343
405
  return
344
406
  }
345
407
 
408
+ if (scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'update') {
409
+ requireUpdateContains(scope, command, body, 'enabled')
410
+ requireUpdateContains(scope, command, body, 'name')
411
+ requireUpdateContains(scope, command, body, 'options')
412
+ }
413
+
414
+ if (scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'assign-users') {
415
+ const update = stringArray(body.update)
416
+ const options = isRecord(body.options) ? body.options : undefined
417
+ const userScope = options && Array.isArray(options.user_scope) ? options.user_scope : undefined
418
+ if ('users' in body || 'allowedUsers' in body) {
419
+ throwBodyValidationFailure(
420
+ 'access-rule assign-users does not accept users or allowedUsers; use body.options.user_scope with {"update":["user_scope"],"options":{"user_scope":[{"type":"user","id":123}]}}',
421
+ command.requestType,
422
+ 'body.options.user_scope',
423
+ scope,
424
+ command,
425
+ )
426
+ }
427
+ if (!update.includes('user_scope')) {
428
+ throwBodyValidationFailure(
429
+ 'access-rule assign-users requires body.update to include "user_scope"',
430
+ command.requestType,
431
+ 'body.update',
432
+ scope,
433
+ command,
434
+ )
435
+ }
436
+ if (!userScope || userScope.length === 0) {
437
+ throwBodyValidationFailure(
438
+ 'access-rule assign-users requires non-empty body.options.user_scope',
439
+ command.requestType,
440
+ 'body.options.user_scope',
441
+ scope,
442
+ command,
443
+ )
444
+ }
445
+ }
446
+
346
447
  if (command.commandPath[0] === 'row' && command.commandPath[1] === 'selection-action') {
347
448
  const action = flagValue(flags, 'action')
348
449
  const selection = body.selection
@@ -469,6 +570,7 @@ export async function executeCLI(scope: CommandScope, argv: string[], env?: Runt
469
570
  if (body === undefined) {
470
571
  throw new CLIError('MISSING_BODY_FILE', `command requires --body-file or --body-json for ${command.requestType}`, 2)
471
572
  }
573
+ validateActionSpecificRawBody(scope, command, body)
472
574
  const schema = getBodySchema(command.requestType)
473
575
  if (schema) {
474
576
  const parsedBody = schema.safeParse(body)
@@ -463,3 +463,77 @@ test('entry passes include_app_tables query flag', async () => {
463
463
  assert.equal(out.kind, 'result')
464
464
  assert.equal(out.data.url, 'https://arcubase.example.com/api/entry?include_app_tables=true')
465
465
  })
466
+
467
+ test('access-rule update rejects enabled without update whitelist', async () => {
468
+ await assert.rejects(async () => {
469
+ await executeCLI(
470
+ 'admin',
471
+ ['access-rule', 'update', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"enabled":true}'],
472
+ env as any,
473
+ async () => new Response('{}', { status: 200 }),
474
+ )
475
+ }, (error: unknown) => {
476
+ assert.ok(error instanceof CLIError)
477
+ assert.equal(error.code, 'BODY_VALIDATION_FAILED')
478
+ assert.match(error.message, /update.*enabled/)
479
+ assert.equal(error.details?.requestType, 'AppIngressUpdateReqVO')
480
+ assert.ok((error.details?.tsHints ?? []).some((item) => item.file === '/opt/arcubase-sdk/types/ingress.ts'))
481
+ return true
482
+ })
483
+ })
484
+
485
+ test('access-rule update accepts enabled with update whitelist', async () => {
486
+ const out = await executeCLI(
487
+ 'admin',
488
+ ['access-rule', 'update', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"update":["enabled"],"enabled":true}'],
489
+ env as any,
490
+ async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
491
+ )
492
+ assert.equal(out.kind, 'result')
493
+ assert.deepEqual(out.data, { update: ['enabled'], enabled: true })
494
+ })
495
+
496
+ test('access-rule assign-users rejects users shortcut', async () => {
497
+ await assert.rejects(async () => {
498
+ await executeCLI(
499
+ 'admin',
500
+ ['access-rule', 'assign-users', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"users":["tu_1"]}'],
501
+ env as any,
502
+ async () => new Response('{}', { status: 200 }),
503
+ )
504
+ }, (error: unknown) => {
505
+ assert.ok(error instanceof CLIError)
506
+ assert.equal(error.code, 'BODY_VALIDATION_FAILED')
507
+ assert.match(error.message, /options.user_scope/)
508
+ assert.equal(error.details?.requestType, 'AppIngressUpdateReqVO')
509
+ return true
510
+ })
511
+ })
512
+
513
+ test('access-rule assign-users requires update user_scope and non-empty user_scope', async () => {
514
+ await assert.rejects(async () => {
515
+ await executeCLI(
516
+ 'admin',
517
+ ['access-rule', 'assign-users', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"update":["user_scope"],"options":{"user_scope":[]}}'],
518
+ env as any,
519
+ async () => new Response('{}', { status: 200 }),
520
+ )
521
+ }, (error: unknown) => {
522
+ assert.ok(error instanceof CLIError)
523
+ assert.equal(error.code, 'BODY_VALIDATION_FAILED')
524
+ assert.match(error.message, /non-empty/)
525
+ return true
526
+ })
527
+ })
528
+
529
+ test('access-rule assign-users accepts canonical user_scope body', async () => {
530
+ const out = await executeCLI(
531
+ 'admin',
532
+ ['access-rule', 'assign-users', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"update":["user_scope"],"options":{"user_scope":[{"type":"user","id":123}]}}'],
533
+ env as any,
534
+ async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
535
+ )
536
+ assert.equal(out.kind, 'result')
537
+ assert.deepEqual(out.data.update, ['user_scope'])
538
+ assert.deepEqual(out.data.options.user_scope, [{ type: 'user', id: 123 }])
539
+ })
@@ -64,3 +64,17 @@ test('admin table update-schema help uses table nouns and body-json', async () =
64
64
  assert.doesNotMatch(out.text, /--entity-id/)
65
65
  assert.doesNotMatch(out.text, /admin-save-entity/)
66
66
  })
67
+
68
+ test('admin access-rule help gives update whitelist and assign-users body-json examples', async () => {
69
+ const update = await executeCLI('admin', ['access-rule', 'update', '--help'], undefined, async () => new Response('{}'))
70
+ assert.equal(update.kind, 'help')
71
+ assert.match(update.text, /--app-id/)
72
+ assert.match(update.text, /--table-id/)
73
+ assert.match(update.text, /--rule-id/)
74
+ assert.match(update.text, /"update":\["enabled"\]/)
75
+
76
+ const assign = await executeCLI('admin', ['access-rule', 'assign-users', '--help'], undefined, async () => new Response('{}'))
77
+ assert.equal(assign.kind, 'help')
78
+ assert.match(assign.text, /"update":\["user_scope"\]/)
79
+ assert.match(assign.text, /"options":\{"user_scope":\[/)
80
+ })