@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 +1 -1
- package/src/runtime/execute.ts +102 -0
- package/src/tests/execute_validation.test.ts +74 -0
- package/src/tests/help.test.ts +14 -0
package/package.json
CHANGED
package/src/runtime/execute.ts
CHANGED
|
@@ -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
|
+
})
|
package/src/tests/help.test.ts
CHANGED
|
@@ -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
|
+
})
|