@carthooks/arcubase-cli 0.1.21 → 0.1.23
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 +1430 -83
- package/bundle/arcubase.mjs +1430 -83
- 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 +116 -0
- package/dist/generated/help_examples.generated.d.ts.map +1 -1
- package/dist/generated/help_examples.generated.js +184 -0
- package/dist/generated/type_index.generated.d.ts +40 -1
- package/dist/generated/type_index.generated.d.ts.map +1 -1
- package/dist/generated/type_index.generated.js +42 -1
- package/dist/generated/zod_registry.generated.d.ts +29 -1
- package/dist/generated/zod_registry.generated.d.ts.map +1 -1
- package/dist/generated/zod_registry.generated.js +37 -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 +326 -1
- package/dist/runtime/zod_registry.d.ts.map +1 -1
- package/dist/runtime/zod_registry.js +58 -0
- package/package.json +1 -1
- package/sdk-dist/api/admin/index.ts +1 -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/dashboard.md +22 -0
- package/sdk-dist/docs/runtime-reference/dev-sdk-gen.md +41 -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 +66 -0
- package/sdk-dist/docs/runtime-reference/help-examples/admin/widget/update-ingress-options.json +21 -0
- package/sdk-dist/docs/runtime-reference/widgets.md +64 -0
- package/sdk-dist/generated/command_registry.generated.ts +416 -0
- package/sdk-dist/generated/help_examples.generated.ts +184 -0
- package/sdk-dist/generated/type_index.generated.ts +42 -1
- package/sdk-dist/generated/zod_registry.generated.ts +56 -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/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 +184 -0
- package/src/generated/type_index.generated.ts +42 -1
- package/src/generated/zod_registry.generated.ts +56 -0
- package/src/runtime/dev_sdk_gen.ts +360 -0
- package/src/runtime/execute.ts +371 -1
- package/src/runtime/zod_registry.ts +58 -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 +226 -0
- package/src/tests/help.test.ts +86 -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) {
|
|
@@ -657,6 +708,14 @@ function isAccessRuleCommand(scope: CommandScope, command: NonNullable<ReturnTyp
|
|
|
657
708
|
return scope === 'admin' && command.commandPath[0] === 'access-rule'
|
|
658
709
|
}
|
|
659
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
|
+
|
|
660
719
|
function isAccessRuleUpdateCommand(scope: CommandScope, command: NonNullable<ReturnType<typeof findCommand>>): boolean {
|
|
661
720
|
return scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'update'
|
|
662
721
|
}
|
|
@@ -966,6 +1025,91 @@ function buildBodyGuidance(requestType: string): Pick<CLIErrorDetails, 'retryToo
|
|
|
966
1025
|
],
|
|
967
1026
|
}
|
|
968
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
|
+
conditions: { mode: 'and', conditions: [{ column: ':1006', operator: '$eq', value: 'yes' }] },
|
|
1036
|
+
},
|
|
1037
|
+
commonMistakes: [
|
|
1038
|
+
'widget preview-data body is the chart query itself',
|
|
1039
|
+
'do not send a saved widget object with id, entity_id, or options',
|
|
1040
|
+
'field dimensions with FieldID must set IsField:true',
|
|
1041
|
+
'use conditions, not filters, for chart data filtering',
|
|
1042
|
+
'do not retry without conditions and then hand-calculate filtered statistics',
|
|
1043
|
+
'unsupported aggregates are invalid; use sum, avg, max, min, count, or distinct_count',
|
|
1044
|
+
],
|
|
1045
|
+
suggestions: [
|
|
1046
|
+
'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"}]}\'',
|
|
1047
|
+
'conditional stats: arcubase-admin widget preview-data --app-id <app_id> --body-json \'{"dataSource":<table_id>,"dataSourceType":"entity","dimensions":[{"IsField":true,"FieldID":<source_field_id>,"Name":"Source"}],"conditions":{"mode":"and","conditions":[{"column":":1006","operator":"$eq","value":"yes"}]},"measures":[{"FieldID":<customer_field_id>,"Aggregate":"distinct_count","Name":"Distinct Customers"}]}\'',
|
|
1048
|
+
],
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
if (requestType === 'AppNewWidgetsReqVO') {
|
|
1052
|
+
return {
|
|
1053
|
+
shapeHint: {
|
|
1054
|
+
type: 'list',
|
|
1055
|
+
options: { ingress_id: 2188893001 },
|
|
1056
|
+
},
|
|
1057
|
+
commonMistakes: [
|
|
1058
|
+
'dashboard widgets use options.dashboard_id',
|
|
1059
|
+
'ingress widgets use options.ingress_id',
|
|
1060
|
+
'do not put entity_id at body top level or under options when creating a widget',
|
|
1061
|
+
'after creating an ingress widget, use widget update-ingress-options to adjust display configuration',
|
|
1062
|
+
],
|
|
1063
|
+
suggestions: [
|
|
1064
|
+
'dashboard chart: arcubase-admin widget create --app-id <app_id> --body-json \'{"type":"chart","options":{"dashboard_id":<dashboard_id>}}\'',
|
|
1065
|
+
'ingress list: arcubase-admin widget create --app-id <app_id> --body-json \'{"type":"list","options":{"ingress_id":<ingress_id>}}\'',
|
|
1066
|
+
],
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
if (requestType === 'AppUpdateWidgetsReqVO') {
|
|
1070
|
+
return {
|
|
1071
|
+
shapeHint: {
|
|
1072
|
+
entity_id: 2188891001,
|
|
1073
|
+
options: {
|
|
1074
|
+
charts: {
|
|
1075
|
+
entity_id: 2188891001,
|
|
1076
|
+
dimensions: [{ IsField: true, FieldID: 1004, Name: 'Source' }],
|
|
1077
|
+
measures: [{ FieldID: 1005, Aggregate: 'sum', Name: 'Total Amount' }],
|
|
1078
|
+
conditions: { mode: 'and', conditions: [{ column: ':1006', operator: '$eq', value: 'yes' }] },
|
|
1079
|
+
type: 'bar',
|
|
1080
|
+
},
|
|
1081
|
+
},
|
|
1082
|
+
},
|
|
1083
|
+
commonMistakes: [
|
|
1084
|
+
'dashboard chart config belongs in body.options.charts',
|
|
1085
|
+
'ingress/list display settings use widget update-ingress-options, not widget update',
|
|
1086
|
+
'do not put ingress_id, list, default_cols, page_size, or entry display settings in widget update',
|
|
1087
|
+
'field dimensions with FieldID must set IsField:true',
|
|
1088
|
+
'use options.charts.conditions, not filters, for chart data filtering',
|
|
1089
|
+
'do not retry without conditions and then hand-calculate filtered statistics',
|
|
1090
|
+
'verify persistence with widget get or widget list after update',
|
|
1091
|
+
],
|
|
1092
|
+
suggestions: [
|
|
1093
|
+
'dashboard chart: arcubase-admin widget update --app-id <app_id> --widget-id <widget_id> --body-json \'{"entity_id":<table_id>,"options":{"charts":{"entity_id":<table_id>,"dimensions":[{"IsField":true,"FieldID":<field_id>,"Name":"Source"}],"measures":[{"FieldID":<number_field_id>,"Aggregate":"sum","Name":"Total Amount"}],"type":"bar"}}}\'',
|
|
1094
|
+
'ingress list display: arcubase-admin widget update-ingress-options --app-id <app_id> --widget-id <widget_id> --body-json \'{"options":{"cond_follow":true,"cond_follow_tab":true,"cond_follow_cols":[":1001",":1002"]}}\'',
|
|
1095
|
+
],
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
if (requestType === 'AppUpdateWidgetsIngressOptionsReqVO') {
|
|
1099
|
+
return {
|
|
1100
|
+
shapeHint: {
|
|
1101
|
+
options: { cond_follow: true, cond_follow_tab: true, cond_follow_cols: [':1001', ':1002'] },
|
|
1102
|
+
},
|
|
1103
|
+
commonMistakes: [
|
|
1104
|
+
'widget update-ingress-options is for ingress widgets, not dashboard chart widgets',
|
|
1105
|
+
'put display settings under body.options',
|
|
1106
|
+
'verify the ingress widget with widget get after update',
|
|
1107
|
+
],
|
|
1108
|
+
suggestions: [
|
|
1109
|
+
'retry with: arcubase-admin widget update-ingress-options --app-id <app_id> --widget-id <widget_id> --body-json \'{"options":{"cond_follow":true,"cond_follow_tab":true,"cond_follow_cols":[":1001",":1002"]}}\'',
|
|
1110
|
+
],
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
969
1113
|
return {}
|
|
970
1114
|
}
|
|
971
1115
|
|
|
@@ -1228,6 +1372,32 @@ function requireUpdateContains(
|
|
|
1228
1372
|
}
|
|
1229
1373
|
}
|
|
1230
1374
|
|
|
1375
|
+
function validateChartFieldDimensions(
|
|
1376
|
+
scope: CommandScope,
|
|
1377
|
+
command: NonNullable<ReturnType<typeof findCommand>>,
|
|
1378
|
+
requestType: string,
|
|
1379
|
+
dimensions: unknown,
|
|
1380
|
+
pathPrefix: string,
|
|
1381
|
+
env: EnvInput,
|
|
1382
|
+
) {
|
|
1383
|
+
if (!Array.isArray(dimensions)) return
|
|
1384
|
+
for (const [index, dimension] of dimensions.entries()) {
|
|
1385
|
+
if (!isRecord(dimension) || typeof dimension.FieldID !== 'number') {
|
|
1386
|
+
continue
|
|
1387
|
+
}
|
|
1388
|
+
if (dimension.IsField !== true) {
|
|
1389
|
+
throwBodyValidationFailure(
|
|
1390
|
+
'widget chart field dimensions require IsField:true when FieldID is present; otherwise grouped statistics collapse into total rows',
|
|
1391
|
+
requestType,
|
|
1392
|
+
`${pathPrefix}.${index}.IsField`,
|
|
1393
|
+
scope,
|
|
1394
|
+
command,
|
|
1395
|
+
env,
|
|
1396
|
+
)
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1231
1401
|
function validateActionSpecificRawBody(
|
|
1232
1402
|
scope: CommandScope,
|
|
1233
1403
|
command: NonNullable<ReturnType<typeof findCommand>>,
|
|
@@ -1361,6 +1531,16 @@ function validateActionSpecificRawBody(
|
|
|
1361
1531
|
validateAccessRuleUserScope(scope, command, command.requestType, options?.user_scope, env)
|
|
1362
1532
|
validateAccessRulePolicyShape(scope, command, command.requestType, policy, env, 'body.options.policy')
|
|
1363
1533
|
}
|
|
1534
|
+
|
|
1535
|
+
if (isWidgetPreviewDataCommand(scope, command)) {
|
|
1536
|
+
validateChartFieldDimensions(scope, command, command.requestType, body.dimensions, 'body.dimensions', env)
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
if (isWidgetUpdateCommand(scope, command)) {
|
|
1540
|
+
const options = isRecord(body.options) ? body.options : undefined
|
|
1541
|
+
const charts = options && isRecord(options.charts) ? options.charts : undefined
|
|
1542
|
+
validateChartFieldDimensions(scope, command, command.requestType, charts?.dimensions, 'body.options.charts.dimensions', env)
|
|
1543
|
+
}
|
|
1364
1544
|
}
|
|
1365
1545
|
|
|
1366
1546
|
function validateActionSpecificBody(
|
|
@@ -1818,6 +1998,186 @@ async function requestJSON(
|
|
|
1818
1998
|
return { status: response.status, data: parsedResponse }
|
|
1819
1999
|
}
|
|
1820
2000
|
|
|
2001
|
+
function unwrapResponseData(payload: unknown): unknown {
|
|
2002
|
+
return isRecord(payload) && 'data' in payload ? payload.data : payload
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
type SDKGenEntityRef = {
|
|
2006
|
+
id: string
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
type SDKGenInitializedFieldKeys = {
|
|
2010
|
+
entityId: number
|
|
2011
|
+
fields: string[]
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
function extractSDKGenAppPayload(payload: unknown): Record<string, any> {
|
|
2015
|
+
const appPayload = unwrapResponseData(payload)
|
|
2016
|
+
if (!isRecord(appPayload)) {
|
|
2017
|
+
throw new CLIError('SDK_GEN_INVALID_SCHEMA', 'sdk-gen expected app detail response data', 2)
|
|
2018
|
+
}
|
|
2019
|
+
return appPayload
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
function extractSDKGenEntityRefs(appPayload: Record<string, any>): SDKGenEntityRef[] {
|
|
2023
|
+
const sourceEntities = Array.isArray(appPayload.entities)
|
|
2024
|
+
? appPayload.entities
|
|
2025
|
+
: Array.isArray(appPayload.tables)
|
|
2026
|
+
? appPayload.tables
|
|
2027
|
+
: Array.isArray(appPayload.entitys)
|
|
2028
|
+
? appPayload.entitys
|
|
2029
|
+
: []
|
|
2030
|
+
const refs = sourceEntities
|
|
2031
|
+
.filter((item): item is Record<string, unknown> => isRecord(item))
|
|
2032
|
+
.map((item) => item.id)
|
|
2033
|
+
.filter((id): id is string | number => typeof id === 'string' || typeof id === 'number')
|
|
2034
|
+
.map((id) => ({ id: String(id) }))
|
|
2035
|
+
return refs
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
function resolveSDKGenAppId(flagAppId: string, appPayload: Record<string, any>): number {
|
|
2039
|
+
const fromPayload = typeof appPayload.id === 'number' ? appPayload.id : typeof appPayload.app_id === 'number' ? appPayload.app_id : undefined
|
|
2040
|
+
const fromFlag = Number(flagAppId)
|
|
2041
|
+
if (Number.isFinite(fromFlag)) return fromFlag
|
|
2042
|
+
if (fromPayload) return fromPayload
|
|
2043
|
+
throw new CLIError('SDK_GEN_APP_ID_REQUIRED', 'sdk-gen expected numeric app id', 2)
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
function walkSDKGenFields(fields: unknown, visit: (field: Record<string, any>) => void) {
|
|
2047
|
+
if (!Array.isArray(fields)) {
|
|
2048
|
+
return
|
|
2049
|
+
}
|
|
2050
|
+
for (const field of fields) {
|
|
2051
|
+
if (!isRecord(field)) {
|
|
2052
|
+
continue
|
|
2053
|
+
}
|
|
2054
|
+
visit(field)
|
|
2055
|
+
walkSDKGenFields(field.children, visit)
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
function collectExistingSDKGenFieldKeys(entity: Record<string, any>): Set<string> {
|
|
2060
|
+
const keys = new Set<string>()
|
|
2061
|
+
walkSDKGenFields(entity.fields, (field) => {
|
|
2062
|
+
if (typeof field.key === 'string' && field.key.trim() !== '') {
|
|
2063
|
+
keys.add(field.key.trim())
|
|
2064
|
+
}
|
|
2065
|
+
})
|
|
2066
|
+
return keys
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
function nextSDKGenFallbackFieldKey(fieldId: number, usedKeys: Set<string>): string {
|
|
2070
|
+
const base = `field${fieldId}`
|
|
2071
|
+
if (!usedKeys.has(base)) {
|
|
2072
|
+
usedKeys.add(base)
|
|
2073
|
+
return base
|
|
2074
|
+
}
|
|
2075
|
+
let index = 2
|
|
2076
|
+
while (usedKeys.has(`${base}_${index}`)) {
|
|
2077
|
+
index++
|
|
2078
|
+
}
|
|
2079
|
+
const key = `${base}_${index}`
|
|
2080
|
+
usedKeys.add(key)
|
|
2081
|
+
return key
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
function initializeMissingSDKGenFieldKeys(entity: Record<string, any>): Array<{ id: number; key: string }> {
|
|
2085
|
+
const usedKeys = collectExistingSDKGenFieldKeys(entity)
|
|
2086
|
+
const updates: Array<{ id: number; key: string }> = []
|
|
2087
|
+
walkSDKGenFields(entity.fields, (field) => {
|
|
2088
|
+
if (typeof field.id !== 'number') {
|
|
2089
|
+
return
|
|
2090
|
+
}
|
|
2091
|
+
if (typeof field.key === 'string' && field.key.trim() !== '') {
|
|
2092
|
+
field.key = field.key.trim()
|
|
2093
|
+
return
|
|
2094
|
+
}
|
|
2095
|
+
const key = nextSDKGenFallbackFieldKey(field.id, usedKeys)
|
|
2096
|
+
field.key = key
|
|
2097
|
+
updates.push({ id: field.id, key })
|
|
2098
|
+
})
|
|
2099
|
+
return updates
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
async function executeDevSDKGen(
|
|
2103
|
+
runtimeEnv: RuntimeEnv,
|
|
2104
|
+
flags: Record<string, string | boolean>,
|
|
2105
|
+
fetchImpl: typeof fetch,
|
|
2106
|
+
): Promise<any> {
|
|
2107
|
+
validateDevSDKGenFlags(flags)
|
|
2108
|
+
const appId = flagValue(flags, 'app-id')
|
|
2109
|
+
const outDir = flagValue(flags, 'out')
|
|
2110
|
+
if (!appId) {
|
|
2111
|
+
throw new CLIError('MISSING_PATH_FLAG', 'missing required flag --app-id', 2)
|
|
2112
|
+
}
|
|
2113
|
+
if (!outDir) {
|
|
2114
|
+
throw new CLIError('MISSING_FLAG', 'missing required flag --out', 2, {
|
|
2115
|
+
operation: 'dev sdk-gen',
|
|
2116
|
+
hint: 'retry with --out src/arcubase',
|
|
2117
|
+
})
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
const endpoint = `/api/apps/${encodeURIComponent(appId)}`
|
|
2121
|
+
const appDetail = await requestJSON(runtimeEnv, 'GET', endpoint, undefined, fetchImpl)
|
|
2122
|
+
const appPayload = extractSDKGenAppPayload(appDetail.data)
|
|
2123
|
+
let entityRefs = extractSDKGenEntityRefs(appPayload)
|
|
2124
|
+
if (entityRefs.length === 0) {
|
|
2125
|
+
const entitiesEndpoint = `/api/apps/${encodeURIComponent(appId)}/entities`
|
|
2126
|
+
const entityList = await requestJSON(runtimeEnv, 'GET', entitiesEndpoint, undefined, fetchImpl)
|
|
2127
|
+
const entityListPayload = unwrapResponseData(entityList.data)
|
|
2128
|
+
entityRefs = extractSDKGenEntityRefs({ entities: entityListPayload })
|
|
2129
|
+
}
|
|
2130
|
+
if (entityRefs.length === 0) {
|
|
2131
|
+
throw new CLIError('SDK_GEN_NO_ENTITIES', 'sdk-gen could not find entities in app detail response', 2)
|
|
2132
|
+
}
|
|
2133
|
+
const entities = []
|
|
2134
|
+
const initializedFieldKeys: SDKGenInitializedFieldKeys[] = []
|
|
2135
|
+
for (const entityRef of entityRefs) {
|
|
2136
|
+
const entityEndpoint = `/api/apps/${encodeURIComponent(appId)}/entity/${encodeURIComponent(entityRef.id)}`
|
|
2137
|
+
const entityDetail = await requestJSON(runtimeEnv, 'GET', entityEndpoint, undefined, fetchImpl)
|
|
2138
|
+
const entityPayload = unwrapResponseData(entityDetail.data)
|
|
2139
|
+
if (!isRecord(entityPayload)) {
|
|
2140
|
+
throw new CLIError('SDK_GEN_INVALID_SCHEMA', 'sdk-gen expected entity detail response data', 2)
|
|
2141
|
+
}
|
|
2142
|
+
const customKeyUpdates = initializeMissingSDKGenFieldKeys(entityPayload)
|
|
2143
|
+
if (customKeyUpdates.length > 0) {
|
|
2144
|
+
const customKeysEndpoint = `/api/apps/${encodeURIComponent(appId)}/entity/${encodeURIComponent(entityRef.id)}/custom-keys`
|
|
2145
|
+
await requestJSON(runtimeEnv, 'PUT', customKeysEndpoint, customKeyUpdates, fetchImpl)
|
|
2146
|
+
initializedFieldKeys.push({
|
|
2147
|
+
entityId: typeof entityPayload.id === 'number' ? entityPayload.id : Number(entityRef.id),
|
|
2148
|
+
fields: customKeyUpdates.map((item) => item.key),
|
|
2149
|
+
})
|
|
2150
|
+
}
|
|
2151
|
+
entities.push(entityPayload)
|
|
2152
|
+
}
|
|
2153
|
+
const generated = generateArcubaseProjectSDK({
|
|
2154
|
+
id: resolveSDKGenAppId(appId, appPayload),
|
|
2155
|
+
name: typeof appPayload.name === 'string' ? appPayload.name : undefined,
|
|
2156
|
+
entities,
|
|
2157
|
+
})
|
|
2158
|
+
const absoluteOutDir = path.resolve(process.cwd(), outDir)
|
|
2159
|
+
|
|
2160
|
+
for (const file of generated.files) {
|
|
2161
|
+
const targetPath = path.resolve(absoluteOutDir, file.path)
|
|
2162
|
+
if (!targetPath.startsWith(`${absoluteOutDir}${path.sep}`)) {
|
|
2163
|
+
throw new CLIError('SDK_GEN_INVALID_OUTPUT_PATH', `invalid generated file path: ${file.path}`, 2)
|
|
2164
|
+
}
|
|
2165
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true })
|
|
2166
|
+
fs.writeFileSync(targetPath, file.contents)
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
return {
|
|
2170
|
+
kind: 'result',
|
|
2171
|
+
commandPath: ['dev', 'sdk-gen'],
|
|
2172
|
+
status: appDetail.status,
|
|
2173
|
+
data: {
|
|
2174
|
+
out: absoluteOutDir,
|
|
2175
|
+
files: generated.files.map((file) => file.path),
|
|
2176
|
+
initializedFieldKeys,
|
|
2177
|
+
},
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
|
|
1821
2181
|
function requiredStringFlag(flags: Record<string, string | boolean>, flag: string): string {
|
|
1822
2182
|
const value = flagValue(flags, flag)
|
|
1823
2183
|
if (!value) {
|
|
@@ -2186,6 +2546,16 @@ export async function executeCLI(scope: CommandScope, argv: string[], env?: Runt
|
|
|
2186
2546
|
}
|
|
2187
2547
|
return { kind: 'help', text: renderRootHelp(scope) }
|
|
2188
2548
|
}
|
|
2549
|
+
if (isDevSDKGenCommand(scope, moduleName, commandName)) {
|
|
2550
|
+
if (hasFlag(parsed.flags, 'help')) {
|
|
2551
|
+
return { kind: 'help', text: renderDevSDKGenHelp() }
|
|
2552
|
+
}
|
|
2553
|
+
const runtimeEnv = env ?? loadRuntimeEnv(scope)
|
|
2554
|
+
return executeDevSDKGen(runtimeEnv, parsed.flags, fetchImpl)
|
|
2555
|
+
}
|
|
2556
|
+
if (scope === 'admin' && moduleName === 'dev' && !commandName) {
|
|
2557
|
+
return { kind: 'help', text: renderDevModuleHelp() }
|
|
2558
|
+
}
|
|
2189
2559
|
const singleTokenCommand = !commandName ? findCommand(scope, moduleName) : null
|
|
2190
2560
|
if (!commandName && !singleTokenCommand) {
|
|
2191
2561
|
if (hasFlag(parsed.flags, 'help-examples')) {
|
|
@@ -89,12 +89,32 @@ const conditionDocHint: RuntimeDocHint = {
|
|
|
89
89
|
title: 'Condition',
|
|
90
90
|
file: 'docs/runtime-reference/condition.md',
|
|
91
91
|
}
|
|
92
|
+
const dashboardDocHint: RuntimeDocHint = {
|
|
93
|
+
title: 'Dashboard',
|
|
94
|
+
file: 'docs/runtime-reference/dashboard.md',
|
|
95
|
+
}
|
|
96
|
+
const widgetsDocHint: RuntimeDocHint = {
|
|
97
|
+
title: 'Widgets',
|
|
98
|
+
file: 'docs/runtime-reference/widgets.md',
|
|
99
|
+
}
|
|
92
100
|
|
|
93
101
|
const docHintIndex: Record<string, RuntimeDocHint[]> = {
|
|
94
102
|
AppCreateByTenantsReqVO: [
|
|
95
103
|
{ title: 'Table lifecycle', file: 'docs/runtime-reference/table-lifecycle.md' },
|
|
96
104
|
examplesIndexDocHint,
|
|
97
105
|
],
|
|
106
|
+
AppNewWidgetsReqVO: [
|
|
107
|
+
widgetsDocHint,
|
|
108
|
+
],
|
|
109
|
+
AppPreviewWidgetsDataReqVO: [
|
|
110
|
+
widgetsDocHint,
|
|
111
|
+
],
|
|
112
|
+
AppUpdateWidgetsIngressOptionsReqVO: [
|
|
113
|
+
widgetsDocHint,
|
|
114
|
+
],
|
|
115
|
+
AppUpdateWidgetsReqVO: [
|
|
116
|
+
widgetsDocHint,
|
|
117
|
+
],
|
|
98
118
|
AppCreateEntityReqVO: [
|
|
99
119
|
{ title: 'Table lifecycle', file: 'docs/runtime-reference/table-lifecycle.md' },
|
|
100
120
|
{ title: 'Table schema cheatsheet', file: 'docs/runtime-reference/entity-schema.md' },
|
|
@@ -113,6 +133,19 @@ const docHintIndex: Record<string, RuntimeDocHint[]> = {
|
|
|
113
133
|
{ title: 'Access rule', file: 'docs/runtime-reference/access-rule.md' },
|
|
114
134
|
examplesIndexDocHint,
|
|
115
135
|
],
|
|
136
|
+
DashboardCreateReqVO: [
|
|
137
|
+
dashboardDocHint,
|
|
138
|
+
],
|
|
139
|
+
DashboardUpdateLayoutReqVO: [
|
|
140
|
+
dashboardDocHint,
|
|
141
|
+
widgetsDocHint,
|
|
142
|
+
],
|
|
143
|
+
DashboardUpdateOptionsReqVO: [
|
|
144
|
+
dashboardDocHint,
|
|
145
|
+
],
|
|
146
|
+
DashboardUpdateTitleReqVO: [
|
|
147
|
+
dashboardDocHint,
|
|
148
|
+
],
|
|
116
149
|
EntitySaveReqVO: [
|
|
117
150
|
{ title: 'Table schema cheatsheet', file: 'docs/runtime-reference/entity-schema.md' },
|
|
118
151
|
{ title: 'Field type index', file: 'docs/runtime-reference/entity-schema/README.md' },
|
|
@@ -152,6 +185,9 @@ const docHintIndex: Record<string, RuntimeDocHint[]> = {
|
|
|
152
185
|
{ title: 'Search and bulk actions', file: 'docs/runtime-reference/search-and-bulk-actions.md' },
|
|
153
186
|
examplesIndexDocHint,
|
|
154
187
|
],
|
|
188
|
+
FetchWidgetsDataReqVO: [
|
|
189
|
+
widgetsDocHint,
|
|
190
|
+
],
|
|
155
191
|
'EntityInvokeBatchOperatorReqVO & InvokeRequestVO': [
|
|
156
192
|
selectionDocHint,
|
|
157
193
|
conditionDocHint,
|
|
@@ -206,6 +242,21 @@ const commandDocHintIndex: Record<string, RuntimeDocHint[]> = {
|
|
|
206
242
|
{ title: 'Table lifecycle', file: 'docs/runtime-reference/table-lifecycle.md' },
|
|
207
243
|
examplesIndexDocHint,
|
|
208
244
|
],
|
|
245
|
+
'admin.dashboard.getDashboardList': [
|
|
246
|
+
dashboardDocHint,
|
|
247
|
+
],
|
|
248
|
+
'admin.dashboard.getDashboardOptions': [
|
|
249
|
+
dashboardDocHint,
|
|
250
|
+
],
|
|
251
|
+
'admin.widget.widgetsList': [
|
|
252
|
+
widgetsDocHint,
|
|
253
|
+
],
|
|
254
|
+
'admin.widget.getWidgets': [
|
|
255
|
+
widgetsDocHint,
|
|
256
|
+
],
|
|
257
|
+
'admin.widget.deleteWidgets': [
|
|
258
|
+
widgetsDocHint,
|
|
259
|
+
],
|
|
209
260
|
'admin.app.createEntity': [
|
|
210
261
|
{ title: 'Table lifecycle', file: 'docs/runtime-reference/table-lifecycle.md' },
|
|
211
262
|
examplesIndexDocHint,
|
|
@@ -273,6 +324,13 @@ const commandDocHintIndex: Record<string, RuntimeDocHint[]> = {
|
|
|
273
324
|
{ title: 'Search and bulk actions', file: 'docs/runtime-reference/search-and-bulk-actions.md' },
|
|
274
325
|
examplesIndexDocHint,
|
|
275
326
|
],
|
|
327
|
+
'user.dashboard.getDashboard': [
|
|
328
|
+
dashboardDocHint,
|
|
329
|
+
],
|
|
330
|
+
'user.dashboard.getDashboardWidgets': [
|
|
331
|
+
dashboardDocHint,
|
|
332
|
+
widgetsDocHint,
|
|
333
|
+
],
|
|
276
334
|
}
|
|
277
335
|
|
|
278
336
|
export function getCommandDocHints(operation: string, env: EnvInput = process.env): RuntimeDocHint[] {
|
|
@@ -17,6 +17,12 @@ test('admin surface is restricted to reset white list', () => {
|
|
|
17
17
|
'app list',
|
|
18
18
|
'app update',
|
|
19
19
|
'app watch-import-task',
|
|
20
|
+
'dashboard create',
|
|
21
|
+
'dashboard get-options',
|
|
22
|
+
'dashboard list',
|
|
23
|
+
'dashboard rename',
|
|
24
|
+
'dashboard update-layout',
|
|
25
|
+
'dashboard update-options',
|
|
20
26
|
'table create',
|
|
21
27
|
'table delete',
|
|
22
28
|
'table get',
|
|
@@ -30,6 +36,13 @@ test('admin surface is restricted to reset white list', () => {
|
|
|
30
36
|
'access-rule get',
|
|
31
37
|
'access-rule list',
|
|
32
38
|
'access-rule update',
|
|
39
|
+
'widget create',
|
|
40
|
+
'widget delete',
|
|
41
|
+
'widget get',
|
|
42
|
+
'widget list',
|
|
43
|
+
'widget preview-data',
|
|
44
|
+
'widget update',
|
|
45
|
+
'widget update-ingress-options',
|
|
33
46
|
'workflow create-version',
|
|
34
47
|
'workflow deploy',
|
|
35
48
|
'workflow get',
|
|
@@ -41,6 +54,9 @@ test('admin surface is restricted to reset white list', () => {
|
|
|
41
54
|
test('user surface is restricted to reset white list', () => {
|
|
42
55
|
const actual = commandPaths(userCommands)
|
|
43
56
|
const expected = [
|
|
57
|
+
'app get',
|
|
58
|
+
'dashboard get',
|
|
59
|
+
'dashboard widgets',
|
|
44
60
|
'entry',
|
|
45
61
|
'profile get',
|
|
46
62
|
'profile update',
|
|
@@ -60,6 +76,7 @@ test('user surface is restricted to reset white list', () => {
|
|
|
60
76
|
'upload mobile',
|
|
61
77
|
'upload preview',
|
|
62
78
|
'upload token',
|
|
79
|
+
'widget data',
|
|
63
80
|
'workflow approve',
|
|
64
81
|
'workflow countersign',
|
|
65
82
|
'workflow forward',
|
|
@@ -76,8 +93,8 @@ test('user surface is restricted to reset white list', () => {
|
|
|
76
93
|
test('generated modules use CLI namespaces only', () => {
|
|
77
94
|
const adminModules = [...new Set(adminCommands.map((item) => item.module))].sort()
|
|
78
95
|
const userModules = [...new Set(userCommands.map((item) => item.module))].sort()
|
|
79
|
-
assert.deepEqual(adminModules, ['access-rule', 'app', 'table', 'workflow'])
|
|
80
|
-
assert.deepEqual(userModules, ['entry', 'profile', 'row', 'table', 'upload', 'workflow'])
|
|
96
|
+
assert.deepEqual(adminModules, ['access-rule', 'app', 'dashboard', 'table', 'widget', 'workflow'])
|
|
97
|
+
assert.deepEqual(userModules, ['app', 'dashboard', 'entry', 'profile', 'row', 'table', 'upload', 'widget', 'workflow'])
|
|
81
98
|
})
|
|
82
99
|
|
|
83
100
|
test('black list commands are removed from registry', () => {
|
|
@@ -99,6 +116,8 @@ test('black list commands are removed from registry', () => {
|
|
|
99
116
|
'workflow query-entity-selection',
|
|
100
117
|
'profile change-password',
|
|
101
118
|
'profile auth-channels',
|
|
119
|
+
'dashboard delete',
|
|
120
|
+
'dashboard clone',
|
|
102
121
|
]
|
|
103
122
|
for (const item of forbidden) {
|
|
104
123
|
assert.equal(all.has(item), false, `${item} should not exist`)
|