@carthooks/arcubase-cli 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/arcubase-admin.mjs +1042 -5056
- package/bundle/arcubase.mjs +1042 -5056
- package/dist/generated/command_registry.generated.d.ts +693 -2812
- package/dist/generated/command_registry.generated.d.ts.map +1 -1
- package/dist/generated/command_registry.generated.js +812 -4100
- package/dist/generated/type_index.generated.d.ts +8 -397
- package/dist/generated/type_index.generated.d.ts.map +1 -1
- package/dist/generated/type_index.generated.js +8 -403
- package/dist/generated/zod_registry.generated.d.ts +33 -261
- package/dist/generated/zod_registry.generated.d.ts.map +1 -1
- package/dist/generated/zod_registry.generated.js +36 -365
- package/dist/runtime/body_loader.d.ts.map +1 -1
- package/dist/runtime/body_loader.js +12 -0
- package/dist/runtime/command_registry.d.ts +3 -21
- package/dist/runtime/command_registry.d.ts.map +1 -1
- package/dist/runtime/command_registry.js +17 -23
- package/dist/runtime/execute.d.ts +3 -3
- package/dist/runtime/execute.d.ts.map +1 -1
- package/dist/runtime/execute.js +53 -157
- package/dist/runtime/http.js +1 -1
- package/dist/runtime/upload.js +3 -3
- package/dist/runtime/zod_registry.d.ts.map +1 -1
- package/dist/runtime/zod_registry.js +7 -24
- package/dist/tests/command_registry.test.js +81 -28
- package/dist/tests/execute_validation.test.js +175 -510
- package/dist/tests/help.test.js +39 -62
- package/dist/tests/upload.test.js +4 -4
- package/dist/tests/zod_registry.test.js +0 -10
- package/package.json +1 -1
- package/sdk-dist/docs/runtime-reference/README.md +20 -30
- package/sdk-dist/docs/runtime-reference/entity-schema/README.md +4 -5
- package/sdk-dist/docs/runtime-reference/entity-schema/linkto.md +3 -3
- package/sdk-dist/docs/runtime-reference/entity-schema/query.md +3 -3
- package/sdk-dist/docs/runtime-reference/entity-schema/relation.md +3 -3
- package/sdk-dist/docs/runtime-reference/entity-schema/relationfield.md +3 -3
- package/sdk-dist/docs/runtime-reference/entity-schema/subform.md +2 -2
- package/sdk-dist/docs/runtime-reference/entity-schema.md +4 -4
- package/sdk-dist/docs/runtime-reference/examples/README.md +2 -2
- package/sdk-dist/docs/runtime-reference/examples/oms-01/README.md +12 -12
- package/sdk-dist/docs/runtime-reference/examples/oms-01/app-overview.md +1 -1
- package/sdk-dist/docs/runtime-reference/row-crud.md +11 -32
- package/sdk-dist/docs/runtime-reference/search-and-bulk-actions.md +16 -47
- package/sdk-dist/docs/runtime-reference/table-lifecycle.md +11 -27
- package/sdk-dist/docs/runtime-reference/uploads.md +1 -1
- package/sdk-dist/docs/runtime-reference/workflow/README.md +19 -0
- package/sdk-dist/generated/command_registry.generated.ts +814 -4102
- package/sdk-dist/generated/type_index.generated.ts +8 -403
- package/sdk-dist/generated/zod_registry.generated.ts +44 -500
- package/sdk-dist/types/app.ts +3 -10
- package/sdk-dist/types/common.ts +7 -0
- package/sdk-dist/types/global-action.ts +2 -1
- package/src/generated/command_registry.generated.ts +814 -4102
- package/src/generated/type_index.generated.ts +8 -403
- package/src/generated/zod_registry.generated.ts +44 -500
- package/src/runtime/body_loader.ts +11 -0
- package/src/runtime/command_registry.ts +22 -44
- package/src/runtime/execute.ts +157 -165
- package/src/runtime/http.ts +1 -1
- package/src/runtime/upload.ts +3 -3
- package/src/runtime/zod_registry.ts +7 -25
- package/src/tests/command_registry.test.ts +81 -31
- package/src/tests/execute_validation.test.ts +236 -603
- package/src/tests/help.test.ts +47 -66
- package/src/tests/upload.test.ts +4 -4
- package/src/tests/zod_registry.test.ts +0 -13
- package/sdk-dist/docs/runtime-reference/app-discovery.md +0 -68
|
@@ -11,6 +11,17 @@ function parseJSONFile(filePath: string, label: string): unknown {
|
|
|
11
11
|
|
|
12
12
|
export function loadBodyFromFlags(flags: Record<string, string | boolean>): unknown | undefined {
|
|
13
13
|
const bodyFile = typeof flags['body-file'] === 'string' ? flags['body-file'] : undefined
|
|
14
|
+
const bodyJSON = typeof flags['body-json'] === 'string' ? flags['body-json'] : undefined
|
|
15
|
+
if (bodyFile && bodyJSON) {
|
|
16
|
+
throw new CLIError('BODY_JSON_CONFLICT', 'use either --body-file or --body-json, not both', 2)
|
|
17
|
+
}
|
|
18
|
+
if (bodyJSON !== undefined) {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(bodyJSON)
|
|
21
|
+
} catch {
|
|
22
|
+
throw new CLIError('INVALID_BODY_JSON', 'body json is invalid', 2)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
14
25
|
if (!bodyFile) return undefined
|
|
15
26
|
return parseJSONFile(bodyFile, 'body')
|
|
16
27
|
}
|
|
@@ -3,50 +3,22 @@ import { adminCommands, userCommands } from '../generated/command_registry.gener
|
|
|
3
3
|
export { adminCommands, userCommands }
|
|
4
4
|
|
|
5
5
|
export type CommandScope = 'admin' | 'user'
|
|
6
|
-
type
|
|
7
|
-
|
|
8
|
-
type CustomCommandDef = {
|
|
9
|
-
scope: CommandScope
|
|
10
|
-
module: string
|
|
11
|
-
functionName: string
|
|
12
|
-
commandPath: readonly [string, string]
|
|
13
|
-
method: string
|
|
14
|
-
endpoint: string
|
|
15
|
-
endpointParams: readonly string[]
|
|
16
|
-
controller: string
|
|
17
|
-
requestType: string | null
|
|
18
|
-
scalarParams: readonly { name: string; type: string; hasDefault: boolean }[]
|
|
19
|
-
responseType: string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export type CommandDef = GeneratedCommandDef | CustomCommandDef
|
|
23
|
-
|
|
24
|
-
const customAdminCommands: readonly CustomCommandDef[] = [
|
|
25
|
-
{
|
|
26
|
-
scope: 'admin',
|
|
27
|
-
module: 'app',
|
|
28
|
-
functionName: 'inventory',
|
|
29
|
-
commandPath: ['app', 'inventory'],
|
|
30
|
-
method: 'GET',
|
|
31
|
-
endpoint: '/apps',
|
|
32
|
-
endpointParams: [],
|
|
33
|
-
controller: 'Runtime.AppInventory',
|
|
34
|
-
requestType: null,
|
|
35
|
-
scalarParams: [],
|
|
36
|
-
responseType: 'AppInventoryRespVO',
|
|
37
|
-
},
|
|
38
|
-
] as const
|
|
39
|
-
|
|
40
|
-
const customUserCommands: readonly CustomCommandDef[] = []
|
|
6
|
+
export type CommandDef = (typeof adminCommands)[number] | (typeof userCommands)[number]
|
|
41
7
|
|
|
42
8
|
export function listCommands(scope: CommandScope): readonly CommandDef[] {
|
|
43
|
-
return scope === 'admin'
|
|
44
|
-
? [...adminCommands, ...customAdminCommands]
|
|
45
|
-
: [...userCommands, ...customUserCommands]
|
|
9
|
+
return scope === 'admin' ? adminCommands : userCommands
|
|
46
10
|
}
|
|
47
11
|
|
|
48
|
-
export function findCommand(scope: CommandScope, moduleName: string, commandName
|
|
49
|
-
return listCommands(scope).find((item) =>
|
|
12
|
+
export function findCommand(scope: CommandScope, moduleName: string, commandName?: string | null): CommandDef | null {
|
|
13
|
+
return listCommands(scope).find((item) => {
|
|
14
|
+
if (item.commandPath[0] !== moduleName) {
|
|
15
|
+
return false
|
|
16
|
+
}
|
|
17
|
+
if (item.commandPath.length === 1) {
|
|
18
|
+
return !commandName
|
|
19
|
+
}
|
|
20
|
+
return item.commandPath[1] === commandName
|
|
21
|
+
}) ?? null
|
|
50
22
|
}
|
|
51
23
|
|
|
52
24
|
export function listModules(scope: CommandScope): string[] {
|
|
@@ -77,7 +49,7 @@ function levenshtein(a: string, b: string): number {
|
|
|
77
49
|
export type CommandSuggestion = {
|
|
78
50
|
scope: CommandScope
|
|
79
51
|
moduleName: string
|
|
80
|
-
commandName: string
|
|
52
|
+
commandName: string | null
|
|
81
53
|
binary: 'arcubase-admin' | 'arcubase'
|
|
82
54
|
score: number
|
|
83
55
|
}
|
|
@@ -86,9 +58,10 @@ export function findCommandSuggestions(moduleName: string, commandName: string,
|
|
|
86
58
|
const suggestions: CommandSuggestion[] = []
|
|
87
59
|
for (const scope of ['admin', 'user'] as const) {
|
|
88
60
|
for (const command of listCommands(scope)) {
|
|
89
|
-
const
|
|
61
|
+
const candidateModule = command.commandPath[0]
|
|
62
|
+
const candidateCommand: string | null = command.commandPath.length > 1 ? command.commandPath[1] ?? null : null
|
|
90
63
|
const moduleScore = candidateModule === moduleName ? 0 : levenshtein(candidateModule, moduleName || '')
|
|
91
|
-
const commandScore = candidateCommand === commandName ? 0 : levenshtein(candidateCommand, commandName || '')
|
|
64
|
+
const commandScore = candidateCommand === commandName ? 0 : levenshtein(candidateCommand ?? '', commandName || '')
|
|
92
65
|
const score = commandScore * 3 + moduleScore
|
|
93
66
|
suggestions.push({
|
|
94
67
|
scope,
|
|
@@ -100,6 +73,11 @@ export function findCommandSuggestions(moduleName: string, commandName: string,
|
|
|
100
73
|
}
|
|
101
74
|
}
|
|
102
75
|
return suggestions
|
|
103
|
-
.sort((a, b) =>
|
|
76
|
+
.sort((a, b) =>
|
|
77
|
+
a.score - b.score ||
|
|
78
|
+
a.binary.localeCompare(b.binary) ||
|
|
79
|
+
a.moduleName.localeCompare(b.moduleName) ||
|
|
80
|
+
(a.commandName ?? '').localeCompare(b.commandName ?? ''),
|
|
81
|
+
)
|
|
104
82
|
.slice(0, limit)
|
|
105
83
|
}
|
package/src/runtime/execute.ts
CHANGED
|
@@ -9,12 +9,12 @@ import { renderUploadHelp, uploadLocalFile } from './upload.js'
|
|
|
9
9
|
|
|
10
10
|
export type CommandExecutionSummary = {
|
|
11
11
|
scope: CommandScope
|
|
12
|
-
commandPath: readonly [
|
|
12
|
+
commandPath: readonly string[]
|
|
13
13
|
method: string
|
|
14
14
|
endpoint: string
|
|
15
15
|
requestType: string | null
|
|
16
16
|
pathFlags: readonly string[]
|
|
17
|
-
|
|
17
|
+
queryFlags: readonly string[]
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
type CustomUploadSummary = {
|
|
@@ -23,127 +23,19 @@ type CustomUploadSummary = {
|
|
|
23
23
|
data: ReturnType<typeof summarizeUploadResult>
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
type AppInventoryItem = {
|
|
27
|
-
id: string
|
|
28
|
-
name: string
|
|
29
|
-
entities: Array<{ id: string; name: string }>
|
|
30
|
-
}
|
|
31
|
-
|
|
32
26
|
function summarizeUploadResult(value: Awaited<ReturnType<typeof uploadLocalFile>>) {
|
|
33
27
|
return value
|
|
34
28
|
}
|
|
35
29
|
|
|
36
|
-
function parseJSONResponse(raw: string): unknown {
|
|
37
|
-
if (!raw) return null
|
|
38
|
-
try {
|
|
39
|
-
return JSON.parse(raw)
|
|
40
|
-
} catch {
|
|
41
|
-
return raw
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function unwrapResponseData(payload: unknown): unknown {
|
|
46
|
-
return isRecord(payload) && 'data' in payload ? payload.data : payload
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function formatUpstreamError(payload: unknown): string {
|
|
50
|
-
if (typeof payload === 'string') {
|
|
51
|
-
return payload
|
|
52
|
-
}
|
|
53
|
-
if (isRecord(payload) && isRecord(payload.error) && typeof payload.error.message === 'string') {
|
|
54
|
-
return payload.error.message
|
|
55
|
-
}
|
|
56
|
-
return JSON.stringify(payload)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function extractApps(payload: unknown): Array<{ id: string; name: string }> {
|
|
60
|
-
const data = unwrapResponseData(payload)
|
|
61
|
-
if (!Array.isArray(data)) return []
|
|
62
|
-
return data
|
|
63
|
-
.filter((item): item is Record<string, unknown> => isRecord(item))
|
|
64
|
-
.filter((item) => item.id !== undefined && typeof item.name === 'string')
|
|
65
|
-
.map((item) => ({
|
|
66
|
-
id: String(item.id),
|
|
67
|
-
name: item.name as string,
|
|
68
|
-
}))
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function extractEntities(payload: unknown): Array<{ id: string; name: string }> {
|
|
72
|
-
const data = unwrapResponseData(payload)
|
|
73
|
-
if (!Array.isArray(data)) return []
|
|
74
|
-
return data
|
|
75
|
-
.filter((item): item is Record<string, unknown> => isRecord(item))
|
|
76
|
-
.filter((item) => item.id !== undefined && typeof item.name === 'string')
|
|
77
|
-
.map((item) => ({
|
|
78
|
-
id: String(item.id),
|
|
79
|
-
name: item.name as string,
|
|
80
|
-
}))
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async function executeAppInventory(runtimeEnv: RuntimeEnv, fetchImpl: typeof fetch) {
|
|
84
|
-
const appsResponse = await fetchImpl(buildURL(runtimeEnv.ARCUBASE_BASE_URL, '/apps'), {
|
|
85
|
-
method: 'GET',
|
|
86
|
-
headers: buildRequestHeaders(runtimeEnv),
|
|
87
|
-
})
|
|
88
|
-
const appsRaw = await appsResponse.text()
|
|
89
|
-
const appsPayload = parseJSONResponse(appsRaw)
|
|
90
|
-
if (!appsResponse.ok) {
|
|
91
|
-
throw new CLIError('UPSTREAM_REQUEST_FAILED', formatUpstreamError(appsPayload), 1)
|
|
92
|
-
}
|
|
93
|
-
if (isRecord(appsPayload) && appsPayload.error) {
|
|
94
|
-
throw new CLIError('UPSTREAM_BODY_ERROR', formatUpstreamError(appsPayload), 1, {
|
|
95
|
-
endpoint: '/apps',
|
|
96
|
-
method: 'GET',
|
|
97
|
-
upstreamError: appsPayload.error,
|
|
98
|
-
})
|
|
99
|
-
}
|
|
100
|
-
const apps = extractApps(appsPayload)
|
|
101
|
-
const items: AppInventoryItem[] = []
|
|
102
|
-
for (const app of apps) {
|
|
103
|
-
const entitiesResponse = await fetchImpl(buildURL(runtimeEnv.ARCUBASE_BASE_URL, `/apps/${encodeURIComponent(app.id)}/entities`), {
|
|
104
|
-
method: 'POST',
|
|
105
|
-
headers: {
|
|
106
|
-
...buildRequestHeaders(runtimeEnv),
|
|
107
|
-
'Content-Type': 'application/json',
|
|
108
|
-
},
|
|
109
|
-
body: JSON.stringify({}),
|
|
110
|
-
})
|
|
111
|
-
const entitiesRaw = await entitiesResponse.text()
|
|
112
|
-
const entitiesPayload = parseJSONResponse(entitiesRaw)
|
|
113
|
-
if (!entitiesResponse.ok) {
|
|
114
|
-
throw new CLIError('UPSTREAM_REQUEST_FAILED', formatUpstreamError(entitiesPayload), 1)
|
|
115
|
-
}
|
|
116
|
-
if (isRecord(entitiesPayload) && entitiesPayload.error) {
|
|
117
|
-
throw new CLIError('UPSTREAM_BODY_ERROR', formatUpstreamError(entitiesPayload), 1, {
|
|
118
|
-
endpoint: `/apps/${app.id}/entities`,
|
|
119
|
-
method: 'POST',
|
|
120
|
-
upstreamError: entitiesPayload.error,
|
|
121
|
-
})
|
|
122
|
-
}
|
|
123
|
-
items.push({
|
|
124
|
-
...app,
|
|
125
|
-
entities: extractEntities(entitiesPayload),
|
|
126
|
-
})
|
|
127
|
-
}
|
|
128
|
-
return {
|
|
129
|
-
kind: 'result',
|
|
130
|
-
commandPath: ['app', 'inventory'] as const,
|
|
131
|
-
status: 200,
|
|
132
|
-
data: {
|
|
133
|
-
apps: items,
|
|
134
|
-
},
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
30
|
export function renderRootHelp(scope: CommandScope): string {
|
|
139
31
|
const binary = scope === 'admin' ? 'arcubase-admin' : 'arcubase'
|
|
140
32
|
const items = listModules(scope).map((item) => ` - ${item}`)
|
|
141
33
|
return [
|
|
142
|
-
`${binary} <
|
|
34
|
+
`${binary} <command> [subcommand] [flags]`,
|
|
143
35
|
'',
|
|
144
36
|
...(scope === 'admin'
|
|
145
|
-
? ['use arcubase-admin for app,
|
|
146
|
-
: ['use arcubase for
|
|
37
|
+
? ['use arcubase-admin for app, table, access-rule, and workflow management', '']
|
|
38
|
+
: ['use arcubase for entry, table, row, upload, profile, and workflow actions', '']),
|
|
147
39
|
'modules:',
|
|
148
40
|
...items,
|
|
149
41
|
...(scope === 'user' ? ['','special commands:',' - upload <local-file>'] : []),
|
|
@@ -153,62 +45,69 @@ export function renderRootHelp(scope: CommandScope): string {
|
|
|
153
45
|
export function renderModuleHelp(scope: CommandScope, moduleName: string): string {
|
|
154
46
|
const items = listModuleCommands(scope, moduleName)
|
|
155
47
|
if (items.length === 0) {
|
|
156
|
-
const suggestions = findCommandSuggestions(moduleName, '').map((item) => `${item.binary} ${item.moduleName} ${item.commandName}`)
|
|
48
|
+
const suggestions = findCommandSuggestions(moduleName, '').map((item) => `${item.binary} ${item.moduleName}${item.commandName ? ` ${item.commandName}` : ''}`)
|
|
157
49
|
throw new CLIError('UNKNOWN_MODULE', `unknown module: ${moduleName}`, 2, suggestions.length > 0 ? { suggestions } : undefined)
|
|
158
50
|
}
|
|
51
|
+
if (items.length === 1 && items[0].commandPath.length === 1) {
|
|
52
|
+
return renderCommandHelp(scope, moduleName)
|
|
53
|
+
}
|
|
159
54
|
return [
|
|
160
55
|
`${scope === 'admin' ? 'arcubase-admin' : 'arcubase'} ${moduleName} <command> [flags]`,
|
|
161
56
|
'',
|
|
162
57
|
...(scope === 'admin'
|
|
163
|
-
? ['use arcubase-admin for app,
|
|
164
|
-
: ['use arcubase for
|
|
58
|
+
? ['use arcubase-admin for app, table, access-rule, and workflow management', '']
|
|
59
|
+
: ['use arcubase for entry, table, row, upload, profile, and workflow actions', '']),
|
|
165
60
|
'commands:',
|
|
166
|
-
...items.map((item) => ` - ${item.commandPath[1]}`),
|
|
61
|
+
...items.filter((item) => item.commandPath.length > 1).map((item) => ` - ${item.commandPath[1]}`),
|
|
167
62
|
].join('\n')
|
|
168
63
|
}
|
|
169
64
|
|
|
170
65
|
function buildUnknownCommandDetails(scope: CommandScope, moduleName: string, commandName: string): { suggestions?: string[]; reason?: string; hint?: string } | undefined {
|
|
171
|
-
const suggestions = findCommandSuggestions(moduleName, commandName).map((item) => `${item.binary} ${item.moduleName} ${item.commandName}`)
|
|
172
|
-
if (scope === 'admin' && moduleName === 'workflow' && suggestions.some((item) => item.startsWith('arcubase rows '))) {
|
|
173
|
-
return {
|
|
174
|
-
suggestions,
|
|
175
|
-
reason: `${moduleName} ${commandName} is not a valid command group; use arcubase rows for row operations`,
|
|
176
|
-
hint: `use: arcubase rows ${commandName}`,
|
|
177
|
-
}
|
|
178
|
-
}
|
|
66
|
+
const suggestions = findCommandSuggestions(moduleName, commandName).map((item) => `${item.binary} ${item.moduleName}${item.commandName ? ` ${item.commandName}` : ''}`)
|
|
179
67
|
return suggestions.length > 0 ? { suggestions } : undefined
|
|
180
68
|
}
|
|
181
69
|
|
|
182
|
-
export function renderCommandHelp(scope: CommandScope, moduleName: string, commandName
|
|
70
|
+
export function renderCommandHelp(scope: CommandScope, moduleName: string, commandName?: string): string {
|
|
183
71
|
const command = findCommand(scope, moduleName, commandName)
|
|
184
72
|
if (!command) {
|
|
185
|
-
throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName} ${commandName}`, 2, buildUnknownCommandDetails(scope, moduleName, commandName))
|
|
73
|
+
throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName}${commandName ? ` ${commandName}` : ''}`, 2, buildUnknownCommandDetails(scope, moduleName, commandName ?? ''))
|
|
186
74
|
}
|
|
187
75
|
const docHints = [
|
|
188
76
|
...getCommandDocHints(`${scope}.${command.module}.${command.functionName}`),
|
|
189
77
|
...(command.requestType ? getDocHints(command.requestType) : []),
|
|
190
78
|
].filter((item, index, list) => list.findIndex((candidate) => candidate.file === item.file) === index)
|
|
79
|
+
const pathFlags = command.pathParams.filter((item) => !getFixedValue(item)).map((item) => `--${item.flag}`)
|
|
80
|
+
const queryFlags = command.queryParams.filter((item) => !getFixedValue(item)).map((item) => `--${item.flag}`)
|
|
191
81
|
return [
|
|
192
|
-
`${scope === 'admin' ? 'arcubase-admin' : 'arcubase'} ${
|
|
82
|
+
`${scope === 'admin' ? 'arcubase-admin' : 'arcubase'} ${command.commandPath.join(' ')}`,
|
|
193
83
|
`method: ${command.method}`,
|
|
194
84
|
`endpoint: ${normalizeExternalEndpoint(command.endpoint)}`,
|
|
195
|
-
command.requestType ? `body: ${command.requestType} via --body-file` : 'body: none',
|
|
196
|
-
'query flags: --query-file',
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
...(scope === 'admin' && moduleName === 'entity' && commandName === 'admin-save-entity'
|
|
85
|
+
command.requestType ? `body: ${command.requestType} via --body-file or --body-json` : 'body: none',
|
|
86
|
+
queryFlags.length ? `query flags: ${queryFlags.join(', ')}, --query-file` : 'query flags: --query-file',
|
|
87
|
+
pathFlags.length ? `path flags: ${pathFlags.join(', ')}` : 'path flags: none',
|
|
88
|
+
...(scope === 'admin' && command.commandPath[0] === 'table' && command.commandPath[1] === 'update-schema'
|
|
200
89
|
? [
|
|
201
90
|
'next step:',
|
|
202
91
|
' - switch to arcubase for row operations',
|
|
203
|
-
' - row
|
|
204
|
-
' - row query: arcubase
|
|
92
|
+
' - row create: arcubase row create --app-id <app_id> --table-id <table_id> --body-file insert.json',
|
|
93
|
+
' - row query: arcubase row query --app-id <app_id> --table-id <table_id> --body-file query.json',
|
|
205
94
|
]
|
|
206
95
|
: []),
|
|
207
|
-
...(scope === 'admin' &&
|
|
96
|
+
...(scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'update'
|
|
208
97
|
? [
|
|
209
|
-
'
|
|
210
|
-
' -
|
|
211
|
-
' -
|
|
98
|
+
'body-json examples:',
|
|
99
|
+
' - enable: {"update":["enabled"],"enabled":true}',
|
|
100
|
+
' - rename: {"update":["name"],"name":"Sales read only"}',
|
|
101
|
+
' - update options: {"update":["options"],"options":{"user_scope":[{"type":"user","id":123}]}}',
|
|
102
|
+
]
|
|
103
|
+
: []),
|
|
104
|
+
...(scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'assign-users'
|
|
105
|
+
? [
|
|
106
|
+
'body-json example:',
|
|
107
|
+
' - {"update":["user_scope"],"options":{"user_scope":[{"type":"user","id":123}]}}',
|
|
108
|
+
'success requires:',
|
|
109
|
+
' - result.enabled must be true',
|
|
110
|
+
' - result.options.user_scope must contain the assigned user ids',
|
|
212
111
|
]
|
|
213
112
|
: []),
|
|
214
113
|
...(docHints.length > 0
|
|
@@ -234,15 +133,22 @@ function coerceScalar(value: string, typeText: string): any {
|
|
|
234
133
|
return value
|
|
235
134
|
}
|
|
236
135
|
|
|
237
|
-
function
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
136
|
+
function getFixedValue(value: object): string | undefined {
|
|
137
|
+
if (!('fixedValue' in value)) {
|
|
138
|
+
return undefined
|
|
139
|
+
}
|
|
140
|
+
const fixedValue = (value as { fixedValue?: unknown }).fixedValue
|
|
141
|
+
return typeof fixedValue === 'string' ? fixedValue : undefined
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function resolveEndpoint(command: NonNullable<ReturnType<typeof findCommand>>, flags: Record<string, string | boolean>): string {
|
|
145
|
+
let resolved: string = command.endpoint
|
|
146
|
+
for (const mapping of command.pathParams) {
|
|
147
|
+
const value = getFixedValue(mapping) ?? flagValue(flags, mapping.flag)
|
|
242
148
|
if (!value) {
|
|
243
|
-
throw new CLIError('MISSING_PATH_FLAG', `missing required flag --${
|
|
149
|
+
throw new CLIError('MISSING_PATH_FLAG', `missing required flag --${mapping.flag}`, 2)
|
|
244
150
|
}
|
|
245
|
-
resolved = resolved.replace(`:${
|
|
151
|
+
resolved = resolved.replace(`:${mapping.param}`, encodeURIComponent(value))
|
|
246
152
|
}
|
|
247
153
|
return resolved
|
|
248
154
|
}
|
|
@@ -252,11 +158,10 @@ function buildScalarQuery(command: ReturnType<typeof findCommand>, flags: Record
|
|
|
252
158
|
return undefined
|
|
253
159
|
}
|
|
254
160
|
const out: Record<string, any> = {}
|
|
255
|
-
for (const scalar of command.
|
|
256
|
-
const
|
|
257
|
-
const value = flagValue(flags, flagName)
|
|
161
|
+
for (const scalar of command.queryParams) {
|
|
162
|
+
const value = getFixedValue(scalar) ?? flagValue(flags, scalar.flag)
|
|
258
163
|
if (value === undefined) continue
|
|
259
|
-
out[scalar.
|
|
164
|
+
out[scalar.key] = coerceScalar(value, scalar.type)
|
|
260
165
|
}
|
|
261
166
|
return Object.keys(out).length > 0 ? out : undefined
|
|
262
167
|
}
|
|
@@ -292,7 +197,7 @@ async function fetchEntityFields(
|
|
|
292
197
|
entityId: string,
|
|
293
198
|
fetchImpl: typeof fetch,
|
|
294
199
|
): Promise<Map<number, EntityFieldInfo>> {
|
|
295
|
-
const response = await fetchImpl(buildURL(runtimeEnv.ARCUBASE_BASE_URL, `/entity/${encodeURIComponent(appId)}/${encodeURIComponent(entityId)}`), {
|
|
200
|
+
const response = await fetchImpl(buildURL(runtimeEnv.ARCUBASE_BASE_URL, `/api/entity/${encodeURIComponent(appId)}/${encodeURIComponent(entityId)}`), {
|
|
296
201
|
method: 'GET',
|
|
297
202
|
headers: buildRequestHeaders(runtimeEnv),
|
|
298
203
|
})
|
|
@@ -315,7 +220,7 @@ async function fetchEntityFields(
|
|
|
315
220
|
? upstreamError
|
|
316
221
|
: JSON.stringify(upstreamError)
|
|
317
222
|
throw new CLIError('UPSTREAM_BODY_ERROR', upstreamMessage, 1, {
|
|
318
|
-
endpoint: `/entity/${appId}/${entityId}`,
|
|
223
|
+
endpoint: `/api/entity/${appId}/${entityId}`,
|
|
319
224
|
method: 'GET',
|
|
320
225
|
upstreamError,
|
|
321
226
|
})
|
|
@@ -359,7 +264,7 @@ async function validateFileFieldPayloads(
|
|
|
359
264
|
return
|
|
360
265
|
}
|
|
361
266
|
const appId = flagValue(flags, 'app-id')
|
|
362
|
-
const entityId = flagValue(flags, '
|
|
267
|
+
const entityId = flagValue(flags, 'table-id')
|
|
363
268
|
if (!appId || !entityId) {
|
|
364
269
|
return
|
|
365
270
|
}
|
|
@@ -445,6 +350,51 @@ function throwBodyValidationFailure(message: string, requestType: string, path:
|
|
|
445
350
|
})
|
|
446
351
|
}
|
|
447
352
|
|
|
353
|
+
function stringArray(value: unknown): string[] {
|
|
354
|
+
return Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : []
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function requireUpdateContains(
|
|
358
|
+
scope: CommandScope,
|
|
359
|
+
command: NonNullable<ReturnType<typeof findCommand>>,
|
|
360
|
+
body: Record<string, unknown>,
|
|
361
|
+
field: string,
|
|
362
|
+
) {
|
|
363
|
+
if (!(field in body)) return
|
|
364
|
+
const update = stringArray(body.update)
|
|
365
|
+
if (!update.includes(field)) {
|
|
366
|
+
throwBodyValidationFailure(
|
|
367
|
+
`body.update must include "${field}" when body.${field} is present. Example: {"update":["${field}"],"${field}":${field === 'enabled' ? 'true' : '"value"'}}`,
|
|
368
|
+
command.requestType ?? 'AppIngressUpdateReqVO',
|
|
369
|
+
'body.update',
|
|
370
|
+
scope,
|
|
371
|
+
command,
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function validateActionSpecificRawBody(
|
|
377
|
+
scope: CommandScope,
|
|
378
|
+
command: NonNullable<ReturnType<typeof findCommand>>,
|
|
379
|
+
body: unknown,
|
|
380
|
+
) {
|
|
381
|
+
if (!isRecord(body) || !command.requestType) {
|
|
382
|
+
return
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'assign-users') {
|
|
386
|
+
if ('users' in body || 'allowedUsers' in body) {
|
|
387
|
+
throwBodyValidationFailure(
|
|
388
|
+
'access-rule assign-users does not accept users or allowedUsers; use body.options.user_scope with {"update":["user_scope"],"options":{"user_scope":[{"type":"user","id":123}]}}',
|
|
389
|
+
command.requestType,
|
|
390
|
+
'body.options.user_scope',
|
|
391
|
+
scope,
|
|
392
|
+
command,
|
|
393
|
+
)
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
448
398
|
function validateActionSpecificBody(
|
|
449
399
|
scope: CommandScope,
|
|
450
400
|
command: NonNullable<ReturnType<typeof findCommand>>,
|
|
@@ -455,7 +405,46 @@ function validateActionSpecificBody(
|
|
|
455
405
|
return
|
|
456
406
|
}
|
|
457
407
|
|
|
458
|
-
if (command.
|
|
408
|
+
if (scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'update') {
|
|
409
|
+
requireUpdateContains(scope, command, body, 'enabled')
|
|
410
|
+
requireUpdateContains(scope, command, body, 'name')
|
|
411
|
+
requireUpdateContains(scope, command, body, 'options')
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'assign-users') {
|
|
415
|
+
const update = stringArray(body.update)
|
|
416
|
+
const options = isRecord(body.options) ? body.options : undefined
|
|
417
|
+
const userScope = options && Array.isArray(options.user_scope) ? options.user_scope : undefined
|
|
418
|
+
if ('users' in body || 'allowedUsers' in body) {
|
|
419
|
+
throwBodyValidationFailure(
|
|
420
|
+
'access-rule assign-users does not accept users or allowedUsers; use body.options.user_scope with {"update":["user_scope"],"options":{"user_scope":[{"type":"user","id":123}]}}',
|
|
421
|
+
command.requestType,
|
|
422
|
+
'body.options.user_scope',
|
|
423
|
+
scope,
|
|
424
|
+
command,
|
|
425
|
+
)
|
|
426
|
+
}
|
|
427
|
+
if (!update.includes('user_scope')) {
|
|
428
|
+
throwBodyValidationFailure(
|
|
429
|
+
'access-rule assign-users requires body.update to include "user_scope"',
|
|
430
|
+
command.requestType,
|
|
431
|
+
'body.update',
|
|
432
|
+
scope,
|
|
433
|
+
command,
|
|
434
|
+
)
|
|
435
|
+
}
|
|
436
|
+
if (!userScope || userScope.length === 0) {
|
|
437
|
+
throwBodyValidationFailure(
|
|
438
|
+
'access-rule assign-users requires non-empty body.options.user_scope',
|
|
439
|
+
command.requestType,
|
|
440
|
+
'body.options.user_scope',
|
|
441
|
+
scope,
|
|
442
|
+
command,
|
|
443
|
+
)
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (command.commandPath[0] === 'row' && command.commandPath[1] === 'selection-action') {
|
|
459
448
|
const action = flagValue(flags, 'action')
|
|
460
449
|
const selection = body.selection
|
|
461
450
|
if (!isRecord(selection) || typeof selection.type !== 'string') {
|
|
@@ -469,7 +458,7 @@ function validateActionSpecificBody(
|
|
|
469
458
|
if (action === 'query') {
|
|
470
459
|
if (type === 'condition') {
|
|
471
460
|
throwBodyValidationFailure(
|
|
472
|
-
'selection.type=condition is not supported for action=query; use selection.type=ids or selection.type=all, or use
|
|
461
|
+
'selection.type=condition is not supported for action=query; use selection.type=ids or selection.type=all, or use row query for search conditions',
|
|
473
462
|
command.requestType,
|
|
474
463
|
'body.selection.type',
|
|
475
464
|
scope,
|
|
@@ -510,10 +499,10 @@ export function summarizeCommand(scope: CommandScope, moduleName: string, comman
|
|
|
510
499
|
scope,
|
|
511
500
|
commandPath: command.commandPath,
|
|
512
501
|
method: command.method,
|
|
513
|
-
endpoint:
|
|
502
|
+
endpoint: command.endpoint,
|
|
514
503
|
requestType: command.requestType,
|
|
515
|
-
pathFlags: command.
|
|
516
|
-
|
|
504
|
+
pathFlags: command.pathParams.filter((item) => !getFixedValue(item)).map((item) => item.flag),
|
|
505
|
+
queryFlags: command.queryParams.filter((item) => !getFixedValue(item)).map((item) => item.flag),
|
|
517
506
|
}
|
|
518
507
|
}
|
|
519
508
|
|
|
@@ -551,7 +540,8 @@ export async function executeCLI(scope: CommandScope, argv: string[], env?: Runt
|
|
|
551
540
|
if (!moduleName) {
|
|
552
541
|
return { kind: 'help', text: renderRootHelp(scope) }
|
|
553
542
|
}
|
|
554
|
-
|
|
543
|
+
const singleTokenCommand = !commandName ? findCommand(scope, moduleName) : null
|
|
544
|
+
if (!commandName && !singleTokenCommand) {
|
|
555
545
|
return { kind: 'help', text: renderModuleHelp(scope, moduleName) }
|
|
556
546
|
}
|
|
557
547
|
if (hasFlag(parsed.flags, 'help')) {
|
|
@@ -562,23 +552,25 @@ export async function executeCLI(scope: CommandScope, argv: string[], env?: Runt
|
|
|
562
552
|
|
|
563
553
|
const command = findCommand(scope, moduleName, commandName)
|
|
564
554
|
if (!command) {
|
|
565
|
-
throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName} ${commandName}`, 2, buildUnknownCommandDetails(scope, moduleName, commandName))
|
|
566
|
-
}
|
|
567
|
-
if (scope === 'admin' && moduleName === 'app' && commandName === 'inventory') {
|
|
568
|
-
return executeAppInventory(runtimeEnv, fetchImpl)
|
|
555
|
+
throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName}${commandName ? ` ${commandName}` : ''}`, 2, buildUnknownCommandDetails(scope, moduleName, commandName ?? ''))
|
|
569
556
|
}
|
|
570
557
|
|
|
571
|
-
const endpoint =
|
|
558
|
+
const endpoint = resolveEndpoint(command, parsed.flags)
|
|
572
559
|
const scalarQuery = buildScalarQuery(command, parsed.flags)
|
|
573
560
|
const fileQuery = loadQueryFromFlags(parsed.flags)
|
|
574
561
|
const query = { ...(scalarQuery ?? {}), ...(fileQuery ?? {}) }
|
|
575
562
|
const body = loadBodyFromFlags(parsed.flags)
|
|
576
563
|
let validatedBody = body
|
|
577
564
|
|
|
565
|
+
if (!command.requestType && body !== undefined) {
|
|
566
|
+
throw new CLIError('BODY_JSON_NOT_SUPPORTED', `command does not accept a request body: ${moduleName} ${commandName}`, 2)
|
|
567
|
+
}
|
|
568
|
+
|
|
578
569
|
if (command.requestType) {
|
|
579
570
|
if (body === undefined) {
|
|
580
|
-
throw new CLIError('MISSING_BODY_FILE', `command requires --body-file for ${command.requestType}`, 2)
|
|
571
|
+
throw new CLIError('MISSING_BODY_FILE', `command requires --body-file or --body-json for ${command.requestType}`, 2)
|
|
581
572
|
}
|
|
573
|
+
validateActionSpecificRawBody(scope, command, body)
|
|
582
574
|
const schema = getBodySchema(command.requestType)
|
|
583
575
|
if (schema) {
|
|
584
576
|
const parsedBody = schema.safeParse(body)
|
package/src/runtime/http.ts
CHANGED
|
@@ -22,7 +22,7 @@ export function normalizeExternalEndpoint(endpoint: string): string {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export function buildURL(baseURL: string, endpoint: string, query?: Record<string, any>): string {
|
|
25
|
-
const url = new URL(
|
|
25
|
+
const url = new URL(endpoint, baseURL.endsWith('/') ? baseURL : `${baseURL}/`)
|
|
26
26
|
if (query) {
|
|
27
27
|
for (const [key, value] of Object.entries(query)) {
|
|
28
28
|
if (value === undefined || value === null) continue
|
package/src/runtime/upload.ts
CHANGED
|
@@ -64,7 +64,7 @@ function requireTokenPayload(payload: UploadTokenResponse): NonNullable<UploadTo
|
|
|
64
64
|
? upstreamError
|
|
65
65
|
: JSON.stringify(upstreamError)
|
|
66
66
|
throw new CLIError('UPSTREAM_BODY_ERROR', message, 1, {
|
|
67
|
-
endpoint: '/upload/token',
|
|
67
|
+
endpoint: '/api/upload/token',
|
|
68
68
|
method: 'POST',
|
|
69
69
|
traceId: typeof payload.trace_id === 'string' ? payload.trace_id : undefined,
|
|
70
70
|
upstreamError,
|
|
@@ -116,7 +116,7 @@ export async function uploadLocalFile(
|
|
|
116
116
|
const { absolutePath, bytes } = readUploadSource(sourcePath)
|
|
117
117
|
const filename = (options.filename && options.filename.trim()) || path.basename(absolutePath)
|
|
118
118
|
|
|
119
|
-
const tokenResponse = await fetchImpl(buildURL(env.ARCUBASE_BASE_URL, '/upload/token'), {
|
|
119
|
+
const tokenResponse = await fetchImpl(buildURL(env.ARCUBASE_BASE_URL, '/api/upload/token'), {
|
|
120
120
|
method: 'POST',
|
|
121
121
|
headers: {
|
|
122
122
|
...buildRequestHeaders(env),
|
|
@@ -131,7 +131,7 @@ export async function uploadLocalFile(
|
|
|
131
131
|
const tokenParsed = parseJSONResponse(tokenRaw)
|
|
132
132
|
if (!tokenResponse.ok) {
|
|
133
133
|
throw new CLIError('UPLOAD_TOKEN_REQUEST_FAILED', typeof tokenParsed === 'string' ? tokenParsed : JSON.stringify(tokenParsed), 1, {
|
|
134
|
-
endpoint: '/upload/token',
|
|
134
|
+
endpoint: '/api/upload/token',
|
|
135
135
|
method: 'POST',
|
|
136
136
|
})
|
|
137
137
|
}
|