@carthooks/arcubase-cli 0.1.20 → 0.1.22
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 +1669 -163
- package/bundle/arcubase.mjs +1669 -163
- package/dist/generated/command_registry.generated.d.ts +300 -0
- package/dist/generated/command_registry.generated.d.ts.map +1 -1
- package/dist/generated/command_registry.generated.js +416 -0
- package/dist/generated/help_examples.generated.d.ts +191 -0
- package/dist/generated/help_examples.generated.d.ts.map +1 -1
- package/dist/generated/help_examples.generated.js +277 -0
- package/dist/generated/type_index.generated.d.ts +44 -1
- package/dist/generated/type_index.generated.d.ts.map +1 -1
- package/dist/generated/type_index.generated.js +46 -1
- package/dist/generated/zod_registry.generated.d.ts +32 -1
- package/dist/generated/zod_registry.generated.d.ts.map +1 -1
- package/dist/generated/zod_registry.generated.js +41 -0
- package/dist/runtime/dev_sdk_gen.d.ts +9 -0
- package/dist/runtime/dev_sdk_gen.d.ts.map +1 -0
- package/dist/runtime/dev_sdk_gen.js +319 -0
- package/dist/runtime/execute.d.ts.map +1 -1
- package/dist/runtime/execute.js +384 -32
- package/dist/runtime/zod_registry.d.ts.map +1 -1
- package/dist/runtime/zod_registry.js +66 -0
- package/package.json +1 -1
- package/sdk-dist/api/admin/index.ts +1 -0
- package/sdk-dist/api/admin/ingress.ts +11 -0
- package/sdk-dist/api/admin/public-link.ts +48 -0
- package/sdk-dist/api/shared-link/form.ts +42 -1
- package/sdk-dist/api/user/index.ts +0 -1
- package/sdk-dist/docs/runtime-reference/access-rule.md +19 -0
- package/sdk-dist/docs/runtime-reference/dashboard.md +15 -0
- package/sdk-dist/docs/runtime-reference/dev-sdk-gen.md +41 -0
- package/sdk-dist/docs/runtime-reference/help-examples/admin/access-rule/bulk-apply.json +105 -0
- package/sdk-dist/docs/runtime-reference/help-examples/admin/dashboard/update-layout.json +16 -0
- package/sdk-dist/docs/runtime-reference/help-examples/admin/widget/create.json +33 -0
- package/sdk-dist/docs/runtime-reference/help-examples/admin/widget/preview-data.json +42 -0
- package/sdk-dist/docs/runtime-reference/widgets.md +40 -0
- package/sdk-dist/generated/command_registry.generated.ts +416 -0
- package/sdk-dist/generated/help_examples.generated.ts +277 -0
- package/sdk-dist/generated/type_index.generated.ts +46 -1
- package/sdk-dist/generated/zod_registry.generated.ts +62 -0
- package/sdk-dist/types/app.ts +37 -1
- package/sdk-dist/types/common.ts +77 -35
- package/sdk-dist/types/index.ts +1 -1
- package/sdk-dist/types/ingress.ts +31 -0
- package/sdk-dist/types/public-link.ts +10 -0
- package/sdk-dist/types/shared-link.ts +16 -1
- package/sdk-dist/types/user-action.ts +1 -43
- package/src/generated/command_registry.generated.ts +416 -0
- package/src/generated/help_examples.generated.ts +277 -0
- package/src/generated/type_index.generated.ts +46 -1
- package/src/generated/zod_registry.generated.ts +62 -0
- package/src/runtime/dev_sdk_gen.ts +360 -0
- package/src/runtime/execute.ts +484 -52
- package/src/runtime/zod_registry.ts +66 -0
- package/src/tests/command_registry.test.ts +21 -2
- package/src/tests/dev_sdk_gen.test.ts +226 -0
- package/src/tests/docs_readability.test.ts +15 -0
- package/src/tests/execute_validation.test.ts +215 -0
- package/src/tests/help.test.ts +83 -0
- package/sdk-dist/api/user/copilot.ts +0 -20
- package/sdk-dist/types/copilot.ts +0 -34
package/src/runtime/execute.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { resolveArcubaseSDKPath } from './paths.js'
|
|
|
11
11
|
import { compileCSVImportMapping, compileExcelImportMapping, type CompiledEntityImportConfig } from './entity_import_mapping.js'
|
|
12
12
|
import { uploadLocalFileWithToken } from './local_upload.js'
|
|
13
13
|
import { renderCommandHelpExamples } from './help_examples.js'
|
|
14
|
+
import { generateArcubaseProjectSDK } from './dev_sdk_gen.js'
|
|
14
15
|
|
|
15
16
|
export type CommandExecutionSummary = {
|
|
16
17
|
scope: CommandScope
|
|
@@ -24,7 +25,8 @@ export type CommandExecutionSummary = {
|
|
|
24
25
|
|
|
25
26
|
export function renderRootHelp(scope: CommandScope): string {
|
|
26
27
|
const binary = scope === 'admin' ? 'arcubase-admin' : 'arcubase'
|
|
27
|
-
const
|
|
28
|
+
const modules = scope === 'admin' ? [...listModules(scope), 'dev'].sort() : listModules(scope)
|
|
29
|
+
const items = modules.map((item) => ` - ${item}`)
|
|
28
30
|
return [
|
|
29
31
|
`${binary} <command> [subcommand] [flags]`,
|
|
30
32
|
'',
|
|
@@ -38,6 +40,17 @@ export function renderRootHelp(scope: CommandScope): string {
|
|
|
38
40
|
].join('\n')
|
|
39
41
|
}
|
|
40
42
|
|
|
43
|
+
function renderDevModuleHelp(): string {
|
|
44
|
+
return [
|
|
45
|
+
'arcubase-admin dev <command> [flags]',
|
|
46
|
+
'',
|
|
47
|
+
'developer commands for local project tooling',
|
|
48
|
+
'',
|
|
49
|
+
'commands:',
|
|
50
|
+
' - sdk-gen',
|
|
51
|
+
].join('\n')
|
|
52
|
+
}
|
|
53
|
+
|
|
41
54
|
export function renderModuleHelp(scope: CommandScope, moduleName: string, env: EnvInput = process.env): string {
|
|
42
55
|
const items = listModuleCommands(scope, moduleName)
|
|
43
56
|
if (items.length === 0) {
|
|
@@ -251,6 +264,44 @@ function getFixedValue(value: object): string | undefined {
|
|
|
251
264
|
return typeof fixedValue === 'string' ? fixedValue : undefined
|
|
252
265
|
}
|
|
253
266
|
|
|
267
|
+
function isDevSDKGenCommand(scope: CommandScope, moduleName: string | undefined, commandName: string | undefined): boolean {
|
|
268
|
+
return scope === 'admin' && moduleName === 'dev' && commandName === 'sdk-gen'
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function validateDevSDKGenFlags(flags: Record<string, string | boolean>) {
|
|
272
|
+
const allowed = new Set(['help', 'app-id', 'out'])
|
|
273
|
+
for (const flag of Object.keys(flags)) {
|
|
274
|
+
if (!allowed.has(flag)) {
|
|
275
|
+
throw new CLIError('UNKNOWN_FLAG', `unknown flag --${flag} for dev sdk-gen`, 2, {
|
|
276
|
+
operation: 'dev sdk-gen',
|
|
277
|
+
hint: 'run dev sdk-gen --help for the supported flags',
|
|
278
|
+
})
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function renderDevSDKGenHelp(): string {
|
|
284
|
+
return [
|
|
285
|
+
'arcubase-admin dev sdk-gen',
|
|
286
|
+
'method: local generator',
|
|
287
|
+
'endpoint: /apps/:app_id',
|
|
288
|
+
'body: none',
|
|
289
|
+
'path flags: --app-id',
|
|
290
|
+
'flags: --out',
|
|
291
|
+
'',
|
|
292
|
+
'generates typed TypeScript source for the current Web project',
|
|
293
|
+
'',
|
|
294
|
+
'example:',
|
|
295
|
+
' - arcubase-admin dev sdk-gen --app-id <app_id> --out src/arcubase',
|
|
296
|
+
'',
|
|
297
|
+
'requirements:',
|
|
298
|
+
' - missing field.key values are initialized through admin auth as field<id>',
|
|
299
|
+
' - existing field.key values must be stable unique TypeScript identifiers',
|
|
300
|
+
' - entity property names use entity.key when available, otherwise a stable entity<id> fallback',
|
|
301
|
+
' - generated code accepts createArcubaseSdk({ baseURL, accessToken, refreshToken })',
|
|
302
|
+
].join('\n')
|
|
303
|
+
}
|
|
304
|
+
|
|
254
305
|
function resolveEndpoint(command: NonNullable<ReturnType<typeof findCommand>>, flags: Record<string, string | boolean>): string {
|
|
255
306
|
let resolved: string = command.endpoint
|
|
256
307
|
for (const mapping of command.pathParams) {
|
|
@@ -637,6 +688,10 @@ function isAssignUsersCommand(scope: CommandScope, command: NonNullable<ReturnTy
|
|
|
637
688
|
return scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'assign-users'
|
|
638
689
|
}
|
|
639
690
|
|
|
691
|
+
function isAccessRuleBulkApplyCommand(scope: CommandScope, command: NonNullable<ReturnType<typeof findCommand>>): boolean {
|
|
692
|
+
return scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'bulk-apply'
|
|
693
|
+
}
|
|
694
|
+
|
|
640
695
|
function isTableCreateCommand(scope: CommandScope, command: NonNullable<ReturnType<typeof findCommand>>): boolean {
|
|
641
696
|
return scope === 'admin' && command.commandPath[0] === 'table' && command.commandPath[1] === 'create'
|
|
642
697
|
}
|
|
@@ -653,6 +708,14 @@ function isAccessRuleCommand(scope: CommandScope, command: NonNullable<ReturnTyp
|
|
|
653
708
|
return scope === 'admin' && command.commandPath[0] === 'access-rule'
|
|
654
709
|
}
|
|
655
710
|
|
|
711
|
+
function isWidgetPreviewDataCommand(scope: CommandScope, command: NonNullable<ReturnType<typeof findCommand>>): boolean {
|
|
712
|
+
return scope === 'admin' && command.commandPath[0] === 'widget' && command.commandPath[1] === 'preview-data'
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function isWidgetUpdateCommand(scope: CommandScope, command: NonNullable<ReturnType<typeof findCommand>>): boolean {
|
|
716
|
+
return scope === 'admin' && command.commandPath[0] === 'widget' && command.commandPath[1] === 'update'
|
|
717
|
+
}
|
|
718
|
+
|
|
656
719
|
function isAccessRuleUpdateCommand(scope: CommandScope, command: NonNullable<ReturnType<typeof findCommand>>): boolean {
|
|
657
720
|
return scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'update'
|
|
658
721
|
}
|
|
@@ -962,6 +1025,45 @@ function buildBodyGuidance(requestType: string): Pick<CLIErrorDetails, 'retryToo
|
|
|
962
1025
|
],
|
|
963
1026
|
}
|
|
964
1027
|
}
|
|
1028
|
+
if (requestType === 'AppPreviewWidgetsDataReqVO') {
|
|
1029
|
+
return {
|
|
1030
|
+
shapeHint: {
|
|
1031
|
+
dataSource: 2188891001,
|
|
1032
|
+
dataSourceType: 'entity',
|
|
1033
|
+
dimensions: [{ IsField: true, FieldID: 1004, Name: 'Source' }],
|
|
1034
|
+
measures: [{ FieldID: 1005, Aggregate: 'sum', Name: 'Total Amount' }],
|
|
1035
|
+
},
|
|
1036
|
+
commonMistakes: [
|
|
1037
|
+
'widget preview-data body is the chart query itself',
|
|
1038
|
+
'do not send a saved widget object with id, entity_id, or options',
|
|
1039
|
+
'field dimensions with FieldID must set IsField:true',
|
|
1040
|
+
'unsupported aggregates are invalid; use sum, avg, max, min, count, or distinct_count',
|
|
1041
|
+
],
|
|
1042
|
+
suggestions: [
|
|
1043
|
+
'retry with: arcubase-admin widget preview-data --app-id <app_id> --body-json \'{"dataSource":<table_id>,"dataSourceType":"entity","dimensions":[{"IsField":true,"FieldID":<field_id>,"Name":"Source"}],"measures":[{"FieldID":<number_field_id>,"Aggregate":"sum","Name":"Total Amount"}]}\'',
|
|
1044
|
+
],
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
if (requestType === 'AppUpdateWidgetsReqVO') {
|
|
1048
|
+
return {
|
|
1049
|
+
shapeHint: {
|
|
1050
|
+
entity_id: 2188891001,
|
|
1051
|
+
options: {
|
|
1052
|
+
charts: {
|
|
1053
|
+
entity_id: 2188891001,
|
|
1054
|
+
dimensions: [{ IsField: true, FieldID: 1004, Name: 'Source' }],
|
|
1055
|
+
measures: [{ FieldID: 1005, Aggregate: 'sum', Name: 'Total Amount' }],
|
|
1056
|
+
type: 'bar',
|
|
1057
|
+
},
|
|
1058
|
+
},
|
|
1059
|
+
},
|
|
1060
|
+
commonMistakes: [
|
|
1061
|
+
'dashboard chart config belongs in body.options.charts',
|
|
1062
|
+
'field dimensions with FieldID must set IsField:true',
|
|
1063
|
+
'verify persistence with widget get or widget list after update',
|
|
1064
|
+
],
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
965
1067
|
return {}
|
|
966
1068
|
}
|
|
967
1069
|
|
|
@@ -1085,33 +1187,124 @@ function normalizeNumericAccessRuleUserScopeIDs(
|
|
|
1085
1187
|
command: NonNullable<ReturnType<typeof findCommand>>,
|
|
1086
1188
|
body: unknown,
|
|
1087
1189
|
): unknown {
|
|
1088
|
-
if (!isAccessRuleCommand(scope, command) || !isRecord(body)
|
|
1190
|
+
if (!isAccessRuleCommand(scope, command) || !isRecord(body)) {
|
|
1089
1191
|
return body
|
|
1090
1192
|
}
|
|
1091
1193
|
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1194
|
+
const normalizeUserScope = (userScope: unknown): { value: unknown; changed: boolean } => {
|
|
1195
|
+
if (!Array.isArray(userScope)) return { value: userScope, changed: false }
|
|
1196
|
+
let changed = false
|
|
1197
|
+
const value = userScope.map((item) => {
|
|
1198
|
+
if (!isRecord(item) || typeof item.id !== 'string' || !/^\d+$/.test(item.id)) {
|
|
1199
|
+
return item
|
|
1200
|
+
}
|
|
1201
|
+
const id = Number(item.id)
|
|
1202
|
+
if (!Number.isFinite(id) || !Number.isInteger(id)) {
|
|
1203
|
+
return item
|
|
1204
|
+
}
|
|
1205
|
+
changed = true
|
|
1206
|
+
return { ...item, id }
|
|
1207
|
+
})
|
|
1208
|
+
return { value, changed }
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
const normalizeNumericID = (value: unknown): { value: unknown; changed: boolean } => {
|
|
1212
|
+
if (typeof value !== 'string' || !/^\d+$/.test(value)) return { value, changed: false }
|
|
1213
|
+
const id = Number(value)
|
|
1214
|
+
if (!Number.isFinite(id) || !Number.isInteger(id)) return { value, changed: false }
|
|
1215
|
+
return { value: id, changed: true }
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
if (isAccessRuleBulkApplyCommand(scope, command) && Array.isArray(body.items)) {
|
|
1219
|
+
let changed = false
|
|
1220
|
+
const items = body.items.map((item) => {
|
|
1221
|
+
if (!isRecord(item)) return item
|
|
1222
|
+
let nextItem: Record<string, unknown> = item
|
|
1223
|
+
for (const key of ['table_id', 'rule_id']) {
|
|
1224
|
+
const normalized = normalizeNumericID(nextItem[key])
|
|
1225
|
+
if (normalized.changed) {
|
|
1226
|
+
nextItem = { ...nextItem, [key]: normalized.value }
|
|
1227
|
+
changed = true
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
if (isRecord(nextItem.options)) {
|
|
1231
|
+
const normalizedUserScope = normalizeUserScope(nextItem.options.user_scope)
|
|
1232
|
+
if (normalizedUserScope.changed) {
|
|
1233
|
+
nextItem = {
|
|
1234
|
+
...nextItem,
|
|
1235
|
+
options: {
|
|
1236
|
+
...nextItem.options,
|
|
1237
|
+
user_scope: normalizedUserScope.value,
|
|
1238
|
+
},
|
|
1239
|
+
}
|
|
1240
|
+
changed = true
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
return nextItem
|
|
1244
|
+
})
|
|
1245
|
+
return changed ? { ...body, items } : body
|
|
1246
|
+
}
|
|
1104
1247
|
|
|
1105
|
-
if (!
|
|
1248
|
+
if (!isRecord(body.options)) {
|
|
1249
|
+
return body
|
|
1250
|
+
}
|
|
1251
|
+
const normalizedUserScope = normalizeUserScope(body.options.user_scope)
|
|
1252
|
+
if (!normalizedUserScope.changed) return body
|
|
1106
1253
|
return {
|
|
1107
1254
|
...body,
|
|
1108
1255
|
options: {
|
|
1109
1256
|
...body.options,
|
|
1110
|
-
user_scope:
|
|
1257
|
+
user_scope: normalizedUserScope.value,
|
|
1111
1258
|
},
|
|
1112
1259
|
}
|
|
1113
1260
|
}
|
|
1114
1261
|
|
|
1262
|
+
function validateAccessRulePolicyShape(
|
|
1263
|
+
scope: CommandScope,
|
|
1264
|
+
command: NonNullable<ReturnType<typeof findCommand>>,
|
|
1265
|
+
requestType: string,
|
|
1266
|
+
policy: Record<string, unknown> | undefined,
|
|
1267
|
+
env: EnvInput,
|
|
1268
|
+
pathPrefix: string,
|
|
1269
|
+
) {
|
|
1270
|
+
const actions = policy && Array.isArray(policy.actions) ? policy.actions : []
|
|
1271
|
+
if (actions.includes('read') || actions.includes('write')) {
|
|
1272
|
+
throwBodyValidationFailure(
|
|
1273
|
+
'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"',
|
|
1274
|
+
requestType,
|
|
1275
|
+
`${pathPrefix}.actions`,
|
|
1276
|
+
scope,
|
|
1277
|
+
command,
|
|
1278
|
+
env,
|
|
1279
|
+
)
|
|
1280
|
+
}
|
|
1281
|
+
const fields = policy && Array.isArray(policy.fields) ? policy.fields : []
|
|
1282
|
+
const allowedSystemFieldKeys = new Set(['created', 'updated', 'creator'])
|
|
1283
|
+
for (const [index, field] of fields.entries()) {
|
|
1284
|
+
if (!isRecord(field)) continue
|
|
1285
|
+
if (typeof field.key === 'number' || (typeof field.key === 'string' && /^\d+$/.test(field.key))) {
|
|
1286
|
+
throwBodyValidationFailure(
|
|
1287
|
+
'access-rule policy.fields[].key must be a string prefixed with colon, for example ":1002"; do not use raw numbers',
|
|
1288
|
+
requestType,
|
|
1289
|
+
`${pathPrefix}.fields.${index}.key`,
|
|
1290
|
+
scope,
|
|
1291
|
+
command,
|
|
1292
|
+
env,
|
|
1293
|
+
)
|
|
1294
|
+
}
|
|
1295
|
+
if (typeof field.key === 'string' && !field.key.startsWith(':') && !allowedSystemFieldKeys.has(field.key)) {
|
|
1296
|
+
throwBodyValidationFailure(
|
|
1297
|
+
'access-rule policy.fields[].key must use Arcubase permission keys such as ":1002"; do not use FieldVO.key values such as "vote_name"',
|
|
1298
|
+
requestType,
|
|
1299
|
+
`${pathPrefix}.fields.${index}.key`,
|
|
1300
|
+
scope,
|
|
1301
|
+
command,
|
|
1302
|
+
env,
|
|
1303
|
+
)
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1115
1308
|
function requireUpdateContains(
|
|
1116
1309
|
scope: CommandScope,
|
|
1117
1310
|
command: NonNullable<ReturnType<typeof findCommand>>,
|
|
@@ -1133,6 +1326,32 @@ function requireUpdateContains(
|
|
|
1133
1326
|
}
|
|
1134
1327
|
}
|
|
1135
1328
|
|
|
1329
|
+
function validateChartFieldDimensions(
|
|
1330
|
+
scope: CommandScope,
|
|
1331
|
+
command: NonNullable<ReturnType<typeof findCommand>>,
|
|
1332
|
+
requestType: string,
|
|
1333
|
+
dimensions: unknown,
|
|
1334
|
+
pathPrefix: string,
|
|
1335
|
+
env: EnvInput,
|
|
1336
|
+
) {
|
|
1337
|
+
if (!Array.isArray(dimensions)) return
|
|
1338
|
+
for (const [index, dimension] of dimensions.entries()) {
|
|
1339
|
+
if (!isRecord(dimension) || typeof dimension.FieldID !== 'number') {
|
|
1340
|
+
continue
|
|
1341
|
+
}
|
|
1342
|
+
if (dimension.IsField !== true) {
|
|
1343
|
+
throwBodyValidationFailure(
|
|
1344
|
+
'widget chart field dimensions require IsField:true when FieldID is present; otherwise grouped statistics collapse into total rows',
|
|
1345
|
+
requestType,
|
|
1346
|
+
`${pathPrefix}.${index}.IsField`,
|
|
1347
|
+
scope,
|
|
1348
|
+
command,
|
|
1349
|
+
env,
|
|
1350
|
+
)
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1136
1355
|
function validateActionSpecificRawBody(
|
|
1137
1356
|
scope: CommandScope,
|
|
1138
1357
|
command: NonNullable<ReturnType<typeof findCommand>>,
|
|
@@ -1172,6 +1391,54 @@ function validateActionSpecificRawBody(
|
|
|
1172
1391
|
}
|
|
1173
1392
|
|
|
1174
1393
|
if (isAccessRuleCommand(scope, command)) {
|
|
1394
|
+
if (isAccessRuleBulkApplyCommand(scope, command)) {
|
|
1395
|
+
if (!Array.isArray(body.items) || body.items.length === 0) {
|
|
1396
|
+
throwBodyValidationFailure(
|
|
1397
|
+
'access-rule bulk-apply requires non-empty body.items; put every table/rule assignment in body.items',
|
|
1398
|
+
command.requestType,
|
|
1399
|
+
'body.items',
|
|
1400
|
+
scope,
|
|
1401
|
+
command,
|
|
1402
|
+
env,
|
|
1403
|
+
)
|
|
1404
|
+
}
|
|
1405
|
+
if (body.mode !== undefined && body.mode !== 'update_existing' && body.mode !== 'upsert_by_name') {
|
|
1406
|
+
throwBodyValidationFailure(
|
|
1407
|
+
'access-rule bulk-apply body.mode must be "update_existing" or "upsert_by_name"',
|
|
1408
|
+
command.requestType,
|
|
1409
|
+
'body.mode',
|
|
1410
|
+
scope,
|
|
1411
|
+
command,
|
|
1412
|
+
env,
|
|
1413
|
+
)
|
|
1414
|
+
}
|
|
1415
|
+
for (const [index, item] of body.items.entries()) {
|
|
1416
|
+
if (!isRecord(item)) {
|
|
1417
|
+
throwBodyValidationFailure(
|
|
1418
|
+
'access-rule bulk-apply body.items[] must be an object',
|
|
1419
|
+
command.requestType,
|
|
1420
|
+
`body.items.${index}`,
|
|
1421
|
+
scope,
|
|
1422
|
+
command,
|
|
1423
|
+
env,
|
|
1424
|
+
)
|
|
1425
|
+
}
|
|
1426
|
+
const options = isRecord(item.options) ? item.options : undefined
|
|
1427
|
+
if (!options) {
|
|
1428
|
+
throwBodyValidationFailure(
|
|
1429
|
+
'access-rule bulk-apply body.items[].options is required and must include the full canonical options object',
|
|
1430
|
+
command.requestType,
|
|
1431
|
+
`body.items.${index}.options`,
|
|
1432
|
+
scope,
|
|
1433
|
+
command,
|
|
1434
|
+
env,
|
|
1435
|
+
)
|
|
1436
|
+
}
|
|
1437
|
+
validateAccessRuleUserScope(scope, command, command.requestType, options.user_scope, env, `body.items.${index}.options.user_scope`)
|
|
1438
|
+
validateAccessRulePolicyShape(scope, command, command.requestType, isRecord(options.policy) ? options.policy : undefined, env, `body.items.${index}.options.policy`)
|
|
1439
|
+
}
|
|
1440
|
+
return
|
|
1441
|
+
}
|
|
1175
1442
|
const update = stringArray(body.update)
|
|
1176
1443
|
if (isAccessRuleUpdateCommand(scope, command) && update.some((item) => item.startsWith('options.'))) {
|
|
1177
1444
|
throwBodyValidationFailure(
|
|
@@ -1216,42 +1483,17 @@ function validateActionSpecificRawBody(
|
|
|
1216
1483
|
)
|
|
1217
1484
|
}
|
|
1218
1485
|
validateAccessRuleUserScope(scope, command, command.requestType, options?.user_scope, env)
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
const fields = policy && Array.isArray(policy.fields) ? policy.fields : []
|
|
1231
|
-
const allowedSystemFieldKeys = new Set(['created', 'updated', 'creator'])
|
|
1232
|
-
for (const [index, field] of fields.entries()) {
|
|
1233
|
-
if (!isRecord(field)) continue
|
|
1234
|
-
if (typeof field.key === 'number' || (typeof field.key === 'string' && /^\d+$/.test(field.key))) {
|
|
1235
|
-
throwBodyValidationFailure(
|
|
1236
|
-
'access-rule policy.fields[].key must be a string prefixed with colon, for example ":1002"; do not use raw numbers',
|
|
1237
|
-
command.requestType,
|
|
1238
|
-
`body.options.policy.fields.${index}.key`,
|
|
1239
|
-
scope,
|
|
1240
|
-
command,
|
|
1241
|
-
env,
|
|
1242
|
-
)
|
|
1243
|
-
}
|
|
1244
|
-
if (typeof field.key === 'string' && !field.key.startsWith(':') && !allowedSystemFieldKeys.has(field.key)) {
|
|
1245
|
-
throwBodyValidationFailure(
|
|
1246
|
-
'access-rule policy.fields[].key must use Arcubase permission keys such as ":1002"; do not use FieldVO.key values such as "vote_name"',
|
|
1247
|
-
command.requestType,
|
|
1248
|
-
`body.options.policy.fields.${index}.key`,
|
|
1249
|
-
scope,
|
|
1250
|
-
command,
|
|
1251
|
-
env,
|
|
1252
|
-
)
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1486
|
+
validateAccessRulePolicyShape(scope, command, command.requestType, policy, env, 'body.options.policy')
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
if (isWidgetPreviewDataCommand(scope, command)) {
|
|
1490
|
+
validateChartFieldDimensions(scope, command, command.requestType, body.dimensions, 'body.dimensions', env)
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
if (isWidgetUpdateCommand(scope, command)) {
|
|
1494
|
+
const options = isRecord(body.options) ? body.options : undefined
|
|
1495
|
+
const charts = options && isRecord(options.charts) ? options.charts : undefined
|
|
1496
|
+
validateChartFieldDimensions(scope, command, command.requestType, charts?.dimensions, 'body.options.charts.dimensions', env)
|
|
1255
1497
|
}
|
|
1256
1498
|
}
|
|
1257
1499
|
|
|
@@ -1710,6 +1952,186 @@ async function requestJSON(
|
|
|
1710
1952
|
return { status: response.status, data: parsedResponse }
|
|
1711
1953
|
}
|
|
1712
1954
|
|
|
1955
|
+
function unwrapResponseData(payload: unknown): unknown {
|
|
1956
|
+
return isRecord(payload) && 'data' in payload ? payload.data : payload
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
type SDKGenEntityRef = {
|
|
1960
|
+
id: string
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
type SDKGenInitializedFieldKeys = {
|
|
1964
|
+
entityId: number
|
|
1965
|
+
fields: string[]
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
function extractSDKGenAppPayload(payload: unknown): Record<string, any> {
|
|
1969
|
+
const appPayload = unwrapResponseData(payload)
|
|
1970
|
+
if (!isRecord(appPayload)) {
|
|
1971
|
+
throw new CLIError('SDK_GEN_INVALID_SCHEMA', 'sdk-gen expected app detail response data', 2)
|
|
1972
|
+
}
|
|
1973
|
+
return appPayload
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
function extractSDKGenEntityRefs(appPayload: Record<string, any>): SDKGenEntityRef[] {
|
|
1977
|
+
const sourceEntities = Array.isArray(appPayload.entities)
|
|
1978
|
+
? appPayload.entities
|
|
1979
|
+
: Array.isArray(appPayload.tables)
|
|
1980
|
+
? appPayload.tables
|
|
1981
|
+
: Array.isArray(appPayload.entitys)
|
|
1982
|
+
? appPayload.entitys
|
|
1983
|
+
: []
|
|
1984
|
+
const refs = sourceEntities
|
|
1985
|
+
.filter((item): item is Record<string, unknown> => isRecord(item))
|
|
1986
|
+
.map((item) => item.id)
|
|
1987
|
+
.filter((id): id is string | number => typeof id === 'string' || typeof id === 'number')
|
|
1988
|
+
.map((id) => ({ id: String(id) }))
|
|
1989
|
+
return refs
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
function resolveSDKGenAppId(flagAppId: string, appPayload: Record<string, any>): number {
|
|
1993
|
+
const fromPayload = typeof appPayload.id === 'number' ? appPayload.id : typeof appPayload.app_id === 'number' ? appPayload.app_id : undefined
|
|
1994
|
+
const fromFlag = Number(flagAppId)
|
|
1995
|
+
if (Number.isFinite(fromFlag)) return fromFlag
|
|
1996
|
+
if (fromPayload) return fromPayload
|
|
1997
|
+
throw new CLIError('SDK_GEN_APP_ID_REQUIRED', 'sdk-gen expected numeric app id', 2)
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
function walkSDKGenFields(fields: unknown, visit: (field: Record<string, any>) => void) {
|
|
2001
|
+
if (!Array.isArray(fields)) {
|
|
2002
|
+
return
|
|
2003
|
+
}
|
|
2004
|
+
for (const field of fields) {
|
|
2005
|
+
if (!isRecord(field)) {
|
|
2006
|
+
continue
|
|
2007
|
+
}
|
|
2008
|
+
visit(field)
|
|
2009
|
+
walkSDKGenFields(field.children, visit)
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
function collectExistingSDKGenFieldKeys(entity: Record<string, any>): Set<string> {
|
|
2014
|
+
const keys = new Set<string>()
|
|
2015
|
+
walkSDKGenFields(entity.fields, (field) => {
|
|
2016
|
+
if (typeof field.key === 'string' && field.key.trim() !== '') {
|
|
2017
|
+
keys.add(field.key.trim())
|
|
2018
|
+
}
|
|
2019
|
+
})
|
|
2020
|
+
return keys
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
function nextSDKGenFallbackFieldKey(fieldId: number, usedKeys: Set<string>): string {
|
|
2024
|
+
const base = `field${fieldId}`
|
|
2025
|
+
if (!usedKeys.has(base)) {
|
|
2026
|
+
usedKeys.add(base)
|
|
2027
|
+
return base
|
|
2028
|
+
}
|
|
2029
|
+
let index = 2
|
|
2030
|
+
while (usedKeys.has(`${base}_${index}`)) {
|
|
2031
|
+
index++
|
|
2032
|
+
}
|
|
2033
|
+
const key = `${base}_${index}`
|
|
2034
|
+
usedKeys.add(key)
|
|
2035
|
+
return key
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
function initializeMissingSDKGenFieldKeys(entity: Record<string, any>): Array<{ id: number; key: string }> {
|
|
2039
|
+
const usedKeys = collectExistingSDKGenFieldKeys(entity)
|
|
2040
|
+
const updates: Array<{ id: number; key: string }> = []
|
|
2041
|
+
walkSDKGenFields(entity.fields, (field) => {
|
|
2042
|
+
if (typeof field.id !== 'number') {
|
|
2043
|
+
return
|
|
2044
|
+
}
|
|
2045
|
+
if (typeof field.key === 'string' && field.key.trim() !== '') {
|
|
2046
|
+
field.key = field.key.trim()
|
|
2047
|
+
return
|
|
2048
|
+
}
|
|
2049
|
+
const key = nextSDKGenFallbackFieldKey(field.id, usedKeys)
|
|
2050
|
+
field.key = key
|
|
2051
|
+
updates.push({ id: field.id, key })
|
|
2052
|
+
})
|
|
2053
|
+
return updates
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
async function executeDevSDKGen(
|
|
2057
|
+
runtimeEnv: RuntimeEnv,
|
|
2058
|
+
flags: Record<string, string | boolean>,
|
|
2059
|
+
fetchImpl: typeof fetch,
|
|
2060
|
+
): Promise<any> {
|
|
2061
|
+
validateDevSDKGenFlags(flags)
|
|
2062
|
+
const appId = flagValue(flags, 'app-id')
|
|
2063
|
+
const outDir = flagValue(flags, 'out')
|
|
2064
|
+
if (!appId) {
|
|
2065
|
+
throw new CLIError('MISSING_PATH_FLAG', 'missing required flag --app-id', 2)
|
|
2066
|
+
}
|
|
2067
|
+
if (!outDir) {
|
|
2068
|
+
throw new CLIError('MISSING_FLAG', 'missing required flag --out', 2, {
|
|
2069
|
+
operation: 'dev sdk-gen',
|
|
2070
|
+
hint: 'retry with --out src/arcubase',
|
|
2071
|
+
})
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
const endpoint = `/api/apps/${encodeURIComponent(appId)}`
|
|
2075
|
+
const appDetail = await requestJSON(runtimeEnv, 'GET', endpoint, undefined, fetchImpl)
|
|
2076
|
+
const appPayload = extractSDKGenAppPayload(appDetail.data)
|
|
2077
|
+
let entityRefs = extractSDKGenEntityRefs(appPayload)
|
|
2078
|
+
if (entityRefs.length === 0) {
|
|
2079
|
+
const entitiesEndpoint = `/api/apps/${encodeURIComponent(appId)}/entities`
|
|
2080
|
+
const entityList = await requestJSON(runtimeEnv, 'GET', entitiesEndpoint, undefined, fetchImpl)
|
|
2081
|
+
const entityListPayload = unwrapResponseData(entityList.data)
|
|
2082
|
+
entityRefs = extractSDKGenEntityRefs({ entities: entityListPayload })
|
|
2083
|
+
}
|
|
2084
|
+
if (entityRefs.length === 0) {
|
|
2085
|
+
throw new CLIError('SDK_GEN_NO_ENTITIES', 'sdk-gen could not find entities in app detail response', 2)
|
|
2086
|
+
}
|
|
2087
|
+
const entities = []
|
|
2088
|
+
const initializedFieldKeys: SDKGenInitializedFieldKeys[] = []
|
|
2089
|
+
for (const entityRef of entityRefs) {
|
|
2090
|
+
const entityEndpoint = `/api/apps/${encodeURIComponent(appId)}/entity/${encodeURIComponent(entityRef.id)}`
|
|
2091
|
+
const entityDetail = await requestJSON(runtimeEnv, 'GET', entityEndpoint, undefined, fetchImpl)
|
|
2092
|
+
const entityPayload = unwrapResponseData(entityDetail.data)
|
|
2093
|
+
if (!isRecord(entityPayload)) {
|
|
2094
|
+
throw new CLIError('SDK_GEN_INVALID_SCHEMA', 'sdk-gen expected entity detail response data', 2)
|
|
2095
|
+
}
|
|
2096
|
+
const customKeyUpdates = initializeMissingSDKGenFieldKeys(entityPayload)
|
|
2097
|
+
if (customKeyUpdates.length > 0) {
|
|
2098
|
+
const customKeysEndpoint = `/api/apps/${encodeURIComponent(appId)}/entity/${encodeURIComponent(entityRef.id)}/custom-keys`
|
|
2099
|
+
await requestJSON(runtimeEnv, 'PUT', customKeysEndpoint, customKeyUpdates, fetchImpl)
|
|
2100
|
+
initializedFieldKeys.push({
|
|
2101
|
+
entityId: typeof entityPayload.id === 'number' ? entityPayload.id : Number(entityRef.id),
|
|
2102
|
+
fields: customKeyUpdates.map((item) => item.key),
|
|
2103
|
+
})
|
|
2104
|
+
}
|
|
2105
|
+
entities.push(entityPayload)
|
|
2106
|
+
}
|
|
2107
|
+
const generated = generateArcubaseProjectSDK({
|
|
2108
|
+
id: resolveSDKGenAppId(appId, appPayload),
|
|
2109
|
+
name: typeof appPayload.name === 'string' ? appPayload.name : undefined,
|
|
2110
|
+
entities,
|
|
2111
|
+
})
|
|
2112
|
+
const absoluteOutDir = path.resolve(process.cwd(), outDir)
|
|
2113
|
+
|
|
2114
|
+
for (const file of generated.files) {
|
|
2115
|
+
const targetPath = path.resolve(absoluteOutDir, file.path)
|
|
2116
|
+
if (!targetPath.startsWith(`${absoluteOutDir}${path.sep}`)) {
|
|
2117
|
+
throw new CLIError('SDK_GEN_INVALID_OUTPUT_PATH', `invalid generated file path: ${file.path}`, 2)
|
|
2118
|
+
}
|
|
2119
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true })
|
|
2120
|
+
fs.writeFileSync(targetPath, file.contents)
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
return {
|
|
2124
|
+
kind: 'result',
|
|
2125
|
+
commandPath: ['dev', 'sdk-gen'],
|
|
2126
|
+
status: appDetail.status,
|
|
2127
|
+
data: {
|
|
2128
|
+
out: absoluteOutDir,
|
|
2129
|
+
files: generated.files.map((file) => file.path),
|
|
2130
|
+
initializedFieldKeys,
|
|
2131
|
+
},
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
|
|
1713
2135
|
function requiredStringFlag(flags: Record<string, string | boolean>, flag: string): string {
|
|
1714
2136
|
const value = flagValue(flags, flag)
|
|
1715
2137
|
if (!value) {
|
|
@@ -2078,6 +2500,16 @@ export async function executeCLI(scope: CommandScope, argv: string[], env?: Runt
|
|
|
2078
2500
|
}
|
|
2079
2501
|
return { kind: 'help', text: renderRootHelp(scope) }
|
|
2080
2502
|
}
|
|
2503
|
+
if (isDevSDKGenCommand(scope, moduleName, commandName)) {
|
|
2504
|
+
if (hasFlag(parsed.flags, 'help')) {
|
|
2505
|
+
return { kind: 'help', text: renderDevSDKGenHelp() }
|
|
2506
|
+
}
|
|
2507
|
+
const runtimeEnv = env ?? loadRuntimeEnv(scope)
|
|
2508
|
+
return executeDevSDKGen(runtimeEnv, parsed.flags, fetchImpl)
|
|
2509
|
+
}
|
|
2510
|
+
if (scope === 'admin' && moduleName === 'dev' && !commandName) {
|
|
2511
|
+
return { kind: 'help', text: renderDevModuleHelp() }
|
|
2512
|
+
}
|
|
2081
2513
|
const singleTokenCommand = !commandName ? findCommand(scope, moduleName) : null
|
|
2082
2514
|
if (!commandName && !singleTokenCommand) {
|
|
2083
2515
|
if (hasFlag(parsed.flags, 'help-examples')) {
|