@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.
Files changed (57) hide show
  1. package/bundle/arcubase-admin.mjs +1430 -83
  2. package/bundle/arcubase.mjs +1430 -83
  3. package/dist/generated/command_registry.generated.d.ts +300 -0
  4. package/dist/generated/command_registry.generated.d.ts.map +1 -1
  5. package/dist/generated/command_registry.generated.js +416 -0
  6. package/dist/generated/help_examples.generated.d.ts +116 -0
  7. package/dist/generated/help_examples.generated.d.ts.map +1 -1
  8. package/dist/generated/help_examples.generated.js +184 -0
  9. package/dist/generated/type_index.generated.d.ts +40 -1
  10. package/dist/generated/type_index.generated.d.ts.map +1 -1
  11. package/dist/generated/type_index.generated.js +42 -1
  12. package/dist/generated/zod_registry.generated.d.ts +29 -1
  13. package/dist/generated/zod_registry.generated.d.ts.map +1 -1
  14. package/dist/generated/zod_registry.generated.js +37 -0
  15. package/dist/runtime/dev_sdk_gen.d.ts +9 -0
  16. package/dist/runtime/dev_sdk_gen.d.ts.map +1 -0
  17. package/dist/runtime/dev_sdk_gen.js +319 -0
  18. package/dist/runtime/execute.d.ts.map +1 -1
  19. package/dist/runtime/execute.js +326 -1
  20. package/dist/runtime/zod_registry.d.ts.map +1 -1
  21. package/dist/runtime/zod_registry.js +58 -0
  22. package/package.json +1 -1
  23. package/sdk-dist/api/admin/index.ts +1 -0
  24. package/sdk-dist/api/admin/public-link.ts +48 -0
  25. package/sdk-dist/api/shared-link/form.ts +42 -1
  26. package/sdk-dist/api/user/index.ts +0 -1
  27. package/sdk-dist/docs/runtime-reference/dashboard.md +22 -0
  28. package/sdk-dist/docs/runtime-reference/dev-sdk-gen.md +41 -0
  29. package/sdk-dist/docs/runtime-reference/help-examples/admin/dashboard/update-layout.json +16 -0
  30. package/sdk-dist/docs/runtime-reference/help-examples/admin/widget/create.json +33 -0
  31. package/sdk-dist/docs/runtime-reference/help-examples/admin/widget/preview-data.json +66 -0
  32. package/sdk-dist/docs/runtime-reference/help-examples/admin/widget/update-ingress-options.json +21 -0
  33. package/sdk-dist/docs/runtime-reference/widgets.md +64 -0
  34. package/sdk-dist/generated/command_registry.generated.ts +416 -0
  35. package/sdk-dist/generated/help_examples.generated.ts +184 -0
  36. package/sdk-dist/generated/type_index.generated.ts +42 -1
  37. package/sdk-dist/generated/zod_registry.generated.ts +56 -0
  38. package/sdk-dist/types/app.ts +37 -1
  39. package/sdk-dist/types/common.ts +77 -35
  40. package/sdk-dist/types/index.ts +1 -1
  41. package/sdk-dist/types/public-link.ts +10 -0
  42. package/sdk-dist/types/shared-link.ts +16 -1
  43. package/sdk-dist/types/user-action.ts +1 -43
  44. package/src/generated/command_registry.generated.ts +416 -0
  45. package/src/generated/help_examples.generated.ts +184 -0
  46. package/src/generated/type_index.generated.ts +42 -1
  47. package/src/generated/zod_registry.generated.ts +56 -0
  48. package/src/runtime/dev_sdk_gen.ts +360 -0
  49. package/src/runtime/execute.ts +371 -1
  50. package/src/runtime/zod_registry.ts +58 -0
  51. package/src/tests/command_registry.test.ts +21 -2
  52. package/src/tests/dev_sdk_gen.test.ts +226 -0
  53. package/src/tests/docs_readability.test.ts +15 -0
  54. package/src/tests/execute_validation.test.ts +226 -0
  55. package/src/tests/help.test.ts +86 -0
  56. package/sdk-dist/api/user/copilot.ts +0 -20
  57. package/sdk-dist/types/copilot.ts +0 -34
@@ -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 items = listModules(scope).map((item) => ` - ${item}`)
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`)