@carthooks/arcubase-cli 0.1.23 → 0.1.25
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 +84 -54
- package/bundle/arcubase.mjs +84 -54
- package/dist/generated/help_examples.generated.d.ts +14 -0
- package/dist/generated/help_examples.generated.d.ts.map +1 -1
- package/dist/generated/help_examples.generated.js +27 -0
- package/dist/runtime/dev_sdk_gen.d.ts +2 -7
- package/dist/runtime/dev_sdk_gen.d.ts.map +1 -1
- package/dist/runtime/dev_sdk_gen.js +8 -314
- package/dist/runtime/execute.d.ts.map +1 -1
- package/dist/runtime/execute.js +25 -43
- package/package.json +4 -3
- package/sdk-dist/docs/runtime-reference/dashboard.md +8 -0
- package/sdk-dist/docs/runtime-reference/dev-sdk-gen.md +14 -3
- package/sdk-dist/docs/runtime-reference/help-examples/admin/dashboard/update-options.json +23 -0
- package/sdk-dist/generated/help_examples.generated.ts +27 -0
- package/src/generated/help_examples.generated.ts +27 -0
- package/src/runtime/dev_sdk_gen.ts +14 -354
- package/src/runtime/execute.ts +26 -50
- package/src/tests/dev_sdk_gen.test.ts +12 -28
- package/src/tests/help.test.ts +11 -0
|
@@ -1,360 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export type GeneratedSDK = {
|
|
9
|
-
files: GeneratedSDKFile[]
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
type ArcubaseField = {
|
|
13
|
-
id: number
|
|
14
|
-
label: string
|
|
15
|
-
key: string
|
|
16
|
-
type: string
|
|
17
|
-
required: boolean
|
|
18
|
-
options?: Record<string, unknown>
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
type ArcubaseEntity = {
|
|
22
|
-
appId: number
|
|
23
|
-
id: number
|
|
24
|
-
name: string
|
|
25
|
-
sdkName: string
|
|
26
|
-
fields: ArcubaseField[]
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function isRecord(value: unknown): value is Record<string, any> {
|
|
30
|
-
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function unwrapData(payload: unknown): unknown {
|
|
34
|
-
return isRecord(payload) && 'data' in payload ? payload.data : payload
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function toPascalCase(value: string): string {
|
|
38
|
-
const words = value
|
|
39
|
-
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
40
|
-
.split(/[^A-Za-z0-9]+/)
|
|
41
|
-
.filter(Boolean)
|
|
42
|
-
if (words.length === 0) return 'Entity'
|
|
43
|
-
return words.map((word) => `${word[0].toUpperCase()}${word.slice(1)}`).join('')
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function toCamelCase(value: string): string {
|
|
47
|
-
const pascal = toPascalCase(value)
|
|
48
|
-
return `${pascal[0].toLowerCase()}${pascal.slice(1)}`
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function sanitizeEntityKey(entity: ArcubaseEntity): string {
|
|
52
|
-
const fallback = entity.sdkName || `entity_${entity.id}`
|
|
53
|
-
return toCamelCase(fallback) === 'entity' ? `entity${entity.id}` : toCamelCase(fallback)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function normalizeFieldType(type: string): string {
|
|
57
|
-
switch (type) {
|
|
58
|
-
case 'number':
|
|
59
|
-
return 'number'
|
|
60
|
-
case 'boolean':
|
|
61
|
-
return 'boolean'
|
|
62
|
-
case 'select':
|
|
63
|
-
case 'radio':
|
|
64
|
-
case 'checkbox':
|
|
65
|
-
case 'status':
|
|
66
|
-
return 'number | string'
|
|
67
|
-
case 'file':
|
|
68
|
-
case 'image':
|
|
69
|
-
case 'member':
|
|
70
|
-
case 'members':
|
|
71
|
-
case 'department':
|
|
72
|
-
case 'departments':
|
|
73
|
-
case 'linkto':
|
|
74
|
-
case 'relation':
|
|
75
|
-
case 'relationfield':
|
|
76
|
-
case 'subform':
|
|
77
|
-
return 'unknown'
|
|
78
|
-
default:
|
|
79
|
-
return 'string'
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function normalizeEntities(payload: unknown): ArcubaseEntity[] {
|
|
84
|
-
const root = unwrapData(payload)
|
|
85
|
-
if (!isRecord(root)) {
|
|
86
|
-
throw new CLIError('SDK_GEN_INVALID_SCHEMA', 'sdk-gen expected an app detail object', 2)
|
|
87
|
-
}
|
|
88
|
-
const appId = typeof root.id === 'number' ? root.id : typeof root.app_id === 'number' ? root.app_id : undefined
|
|
89
|
-
if (!appId) {
|
|
90
|
-
throw new CLIError('SDK_GEN_APP_ID_REQUIRED', 'sdk-gen expected app id in app detail response', 2)
|
|
91
|
-
}
|
|
92
|
-
const sourceEntities = Array.isArray(root.entities)
|
|
93
|
-
? root.entities
|
|
94
|
-
: Array.isArray(root.tables)
|
|
95
|
-
? root.tables
|
|
96
|
-
: Array.isArray(root.entitys)
|
|
97
|
-
? root.entitys
|
|
98
|
-
: []
|
|
99
|
-
const entities = sourceEntities
|
|
100
|
-
.filter((item): item is Record<string, unknown> => isRecord(item))
|
|
101
|
-
.filter((item) => typeof item.id === 'number' && typeof item.name === 'string')
|
|
102
|
-
.map((item) => {
|
|
103
|
-
const fields = Array.isArray(item.fields)
|
|
104
|
-
? item.fields
|
|
105
|
-
.filter((field): field is Record<string, unknown> => isRecord(field))
|
|
106
|
-
.filter((field) => typeof field.id === 'number' && typeof field.label === 'string' && typeof field.type === 'string')
|
|
107
|
-
.map((field) => ({
|
|
108
|
-
id: field.id as number,
|
|
109
|
-
label: field.label as string,
|
|
110
|
-
key: typeof field.key === 'string' ? field.key.trim() : '',
|
|
111
|
-
type: field.type as string,
|
|
112
|
-
required: field.required === true,
|
|
113
|
-
options: isRecord(field.options) ? field.options : undefined,
|
|
114
|
-
}))
|
|
115
|
-
: []
|
|
116
|
-
const entity: ArcubaseEntity = {
|
|
117
|
-
appId,
|
|
118
|
-
id: item.id as number,
|
|
119
|
-
name: item.name as string,
|
|
120
|
-
sdkName: typeof item.key === 'string' && item.key.trim() ? item.key.trim() : '',
|
|
121
|
-
fields,
|
|
122
|
-
}
|
|
123
|
-
entity.sdkName = sanitizeEntityKey(entity)
|
|
124
|
-
return entity
|
|
125
|
-
})
|
|
126
|
-
if (entities.length === 0) {
|
|
127
|
-
throw new CLIError('SDK_GEN_NO_ENTITIES', 'sdk-gen could not find entities in app detail response', 2)
|
|
128
|
-
}
|
|
129
|
-
validateEntities(entities)
|
|
130
|
-
return entities
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function validateEntities(entities: ArcubaseEntity[]) {
|
|
134
|
-
const entityKeys = new Set<string>()
|
|
135
|
-
for (const entity of entities) {
|
|
136
|
-
if (entityKeys.has(entity.sdkName)) {
|
|
137
|
-
throw new CLIError('SDK_GEN_DUPLICATE_ENTITY_KEY', `duplicate generated entity key: ${entity.sdkName}`, 2)
|
|
138
|
-
}
|
|
139
|
-
entityKeys.add(entity.sdkName)
|
|
140
|
-
|
|
141
|
-
const fieldKeys = new Set<string>()
|
|
142
|
-
for (const field of entity.fields) {
|
|
143
|
-
if (!field.key) {
|
|
144
|
-
throw new CLIError('SDK_GEN_FIELD_KEY_REQUIRED', `field.key is required for ${entity.name}.${field.label} (${field.id})`, 2)
|
|
145
|
-
}
|
|
146
|
-
if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(field.key)) {
|
|
147
|
-
throw new CLIError('SDK_GEN_INVALID_FIELD_KEY', `field.key must be a TypeScript identifier: ${entity.name}.${field.key}`, 2)
|
|
148
|
-
}
|
|
149
|
-
if (fieldKeys.has(field.key)) {
|
|
150
|
-
throw new CLIError('SDK_GEN_DUPLICATE_FIELD_KEY', `duplicate field.key in ${entity.name}: ${field.key}`, 2)
|
|
151
|
-
}
|
|
152
|
-
fieldKeys.add(field.key)
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function emitRuntime(): string {
|
|
158
|
-
return `export type ArcubaseSdkConfig = {
|
|
159
|
-
baseURL: string
|
|
160
|
-
accessToken?: string
|
|
161
|
-
refreshToken?: string
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export type ArcubaseRow<TFields> = {
|
|
165
|
-
id: number
|
|
166
|
-
title?: string
|
|
167
|
-
fields: TFields
|
|
168
|
-
raw: Record<string, any>
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
export type ArcubaseRuntime = {
|
|
172
|
-
config: ArcubaseSdkConfig
|
|
173
|
-
request<T>(method: string, endpoint: string, body?: unknown): Promise<T>
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
type ArcubaseEnvelope<T> = {
|
|
177
|
-
data?: T
|
|
178
|
-
error?: { message?: string; key?: string; type?: string }
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export function createRuntime(config: ArcubaseSdkConfig): ArcubaseRuntime {
|
|
182
|
-
let accessToken = config.accessToken ?? ''
|
|
183
|
-
let refreshToken = config.refreshToken ?? ''
|
|
1
|
+
import {
|
|
2
|
+
ArcubaseCodeGenError,
|
|
3
|
+
generateArcubaseProjectSDK as generateArcubaseProjectSDKCore,
|
|
4
|
+
type GeneratedSDK,
|
|
5
|
+
} from '@arcubase/code-gen'
|
|
184
6
|
|
|
185
|
-
|
|
186
|
-
const baseURL = config.baseURL.endsWith('/') ? config.baseURL : \`\${config.baseURL}/\`
|
|
187
|
-
const response = await fetch(new URL(endpoint.replace(/^\\/+/, ''), baseURL).toString(), {
|
|
188
|
-
method,
|
|
189
|
-
headers: {
|
|
190
|
-
'Content-Type': 'application/json',
|
|
191
|
-
...(accessToken ? { Authorization: \`Bearer \${accessToken}\` } : {}),
|
|
192
|
-
},
|
|
193
|
-
body: body === undefined ? undefined : JSON.stringify(body),
|
|
194
|
-
})
|
|
195
|
-
const payload = await response.json().catch(() => null) as ArcubaseEnvelope<T> | null
|
|
196
|
-
if (!response.ok || payload?.error) {
|
|
197
|
-
throw new Error(payload?.error?.message ?? \`Arcubase request failed: \${response.status}\`)
|
|
198
|
-
}
|
|
199
|
-
return payload?.data as T
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
return {
|
|
203
|
-
config: { ...config, accessToken, refreshToken },
|
|
204
|
-
request,
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
`
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function emitSchema(entities: ArcubaseEntity[]): string {
|
|
211
|
-
const schema = {
|
|
212
|
-
entities: Object.fromEntries(entities.map((entity) => [
|
|
213
|
-
entity.sdkName,
|
|
214
|
-
{
|
|
215
|
-
entityId: entity.id,
|
|
216
|
-
name: entity.name,
|
|
217
|
-
fields: Object.fromEntries(entity.fields.map((field) => [
|
|
218
|
-
field.key,
|
|
219
|
-
{
|
|
220
|
-
fieldId: field.id,
|
|
221
|
-
label: field.label,
|
|
222
|
-
type: field.type,
|
|
223
|
-
required: field.required,
|
|
224
|
-
},
|
|
225
|
-
])),
|
|
226
|
-
},
|
|
227
|
-
])),
|
|
228
|
-
}
|
|
229
|
-
return `export const arcubaseSchema = ${JSON.stringify(schema, null, 2)} as const
|
|
230
|
-
|
|
231
|
-
export type ArcubaseEntityKey = keyof typeof arcubaseSchema.entities
|
|
232
|
-
`
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function emitEntity(entity: ArcubaseEntity): string {
|
|
236
|
-
const typeName = toPascalCase(entity.sdkName)
|
|
237
|
-
const fieldLines = entity.fields.map((field) => {
|
|
238
|
-
const optional = field.required ? '' : '?'
|
|
239
|
-
return ` ${field.key}${optional}: ${normalizeFieldType(field.type)}`
|
|
240
|
-
})
|
|
241
|
-
const fieldMapLines = entity.fields.map((field) => ` ${field.key}: ${field.id},`)
|
|
242
|
-
|
|
243
|
-
return `import type { ArcubaseRuntime, ArcubaseRow } from '../runtime.js'
|
|
244
|
-
|
|
245
|
-
export type ${typeName}Fields = {
|
|
246
|
-
${fieldLines.join('\n')}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
export type ${typeName}CreateInput = ${typeName}Fields
|
|
250
|
-
export type ${typeName}UpdateInput = Partial<${typeName}Fields>
|
|
251
|
-
|
|
252
|
-
const entityId = ${entity.id}
|
|
253
|
-
const fieldIds = {
|
|
254
|
-
${fieldMapLines.join('\n')}
|
|
255
|
-
} as const
|
|
256
|
-
|
|
257
|
-
function toArcubaseFields(fields: Partial<${typeName}Fields>): Record<string, any> {
|
|
258
|
-
const out: Record<string, any> = {}
|
|
259
|
-
for (const [key, value] of Object.entries(fields)) {
|
|
260
|
-
if (value === undefined) continue
|
|
261
|
-
const fieldId = fieldIds[key as keyof typeof fieldIds]
|
|
262
|
-
if (!fieldId) {
|
|
263
|
-
throw new Error(\`Unknown ${entity.sdkName} field: \${key}\`)
|
|
264
|
-
}
|
|
265
|
-
out[String(fieldId)] = value
|
|
266
|
-
}
|
|
267
|
-
return out
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function fromArcubaseFields(raw: Record<string, any> | undefined): ${typeName}Fields {
|
|
271
|
-
const out: Record<string, any> = {}
|
|
272
|
-
const source = raw ?? {}
|
|
273
|
-
for (const [key, fieldId] of Object.entries(fieldIds)) {
|
|
274
|
-
if (String(fieldId) in source) {
|
|
275
|
-
out[key] = source[String(fieldId)]
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
return out as ${typeName}Fields
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function mapRow(raw: Record<string, any>): ArcubaseRow<${typeName}Fields> {
|
|
282
|
-
return {
|
|
283
|
-
id: Number(raw.id),
|
|
284
|
-
title: typeof raw.title === 'string' ? raw.title : undefined,
|
|
285
|
-
fields: fromArcubaseFields(raw.fields),
|
|
286
|
-
raw,
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
export function create${typeName}SDK(runtime: ArcubaseRuntime) {
|
|
291
|
-
return {
|
|
292
|
-
async create(fields: ${typeName}CreateInput): Promise<number> {
|
|
293
|
-
return runtime.request<number>('POST', \`/entity/${entity.appId}/\${entityId}/insert\`, {
|
|
294
|
-
fields: toArcubaseFields(fields),
|
|
295
|
-
})
|
|
296
|
-
},
|
|
297
|
-
|
|
298
|
-
async update(rowId: string | number, fields: ${typeName}UpdateInput): Promise<boolean> {
|
|
299
|
-
const data = toArcubaseFields(fields)
|
|
300
|
-
return runtime.request<boolean>('PUT', \`/entity/${entity.appId}/\${entityId}/row/\${rowId}\`, {
|
|
301
|
-
data,
|
|
302
|
-
changed_fields: Object.keys(data).map((fieldId) => Number(fieldId)),
|
|
303
|
-
})
|
|
304
|
-
},
|
|
305
|
-
|
|
306
|
-
async get(rowId: string | number): Promise<ArcubaseRow<${typeName}Fields>> {
|
|
307
|
-
const data = await runtime.request<{ record?: Record<string, any> }>('GET', \`/entity/${entity.appId}/\${entityId}/row/\${rowId}\`)
|
|
308
|
-
return mapRow(data.record ?? {})
|
|
309
|
-
},
|
|
310
|
-
|
|
311
|
-
async query(params: { limit?: number; offset?: number; search?: Record<string, any> } = {}): Promise<{ items: ArcubaseRow<${typeName}Fields>[]; total?: number }> {
|
|
312
|
-
const data = await runtime.request<{ items?: Record<string, any>[]; total?: number }>('POST', \`/entity/${entity.appId}/\${entityId}/data-query\`, params)
|
|
313
|
-
return {
|
|
314
|
-
items: (data.items ?? []).map(mapRow),
|
|
315
|
-
total: data.total,
|
|
316
|
-
}
|
|
317
|
-
},
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
`
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function emitClient(entities: ArcubaseEntity[]): string {
|
|
324
|
-
const imports = entities.map((entity) => {
|
|
325
|
-
const typeName = toPascalCase(entity.sdkName)
|
|
326
|
-
return `import { create${typeName}SDK } from './entities/${entity.sdkName}.js'`
|
|
327
|
-
})
|
|
328
|
-
const properties = entities.map((entity) => ` ${entity.sdkName}: create${toPascalCase(entity.sdkName)}SDK(runtime),`)
|
|
329
|
-
return `import { createRuntime, type ArcubaseSdkConfig } from './runtime.js'
|
|
330
|
-
${imports.join('\n')}
|
|
331
|
-
|
|
332
|
-
export function createArcubaseSdk(config: ArcubaseSdkConfig) {
|
|
333
|
-
const runtime = createRuntime(config)
|
|
334
|
-
return {
|
|
335
|
-
${properties.join('\n')}
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
`
|
|
339
|
-
}
|
|
7
|
+
import { CLIError } from './errors.js'
|
|
340
8
|
|
|
341
|
-
|
|
342
|
-
return `export { createArcubaseSdk } from './client.js'
|
|
343
|
-
export type { ArcubaseSdkConfig, ArcubaseRow } from './runtime.js'
|
|
344
|
-
export { arcubaseSchema } from './schema.js'
|
|
345
|
-
${entities.map((entity) => `export type { ${toPascalCase(entity.sdkName)}Fields, ${toPascalCase(entity.sdkName)}CreateInput, ${toPascalCase(entity.sdkName)}UpdateInput } from './entities/${entity.sdkName}.js'`).join('\n')}
|
|
346
|
-
`
|
|
347
|
-
}
|
|
9
|
+
export type { GeneratedSDK, GeneratedSDKFile } from '@arcubase/code-gen'
|
|
348
10
|
|
|
349
11
|
export function generateArcubaseProjectSDK(payload: unknown): GeneratedSDK {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
{ path: 'index.ts', contents: emitIndex(entities) },
|
|
358
|
-
],
|
|
12
|
+
try {
|
|
13
|
+
return generateArcubaseProjectSDKCore(payload)
|
|
14
|
+
} catch (error) {
|
|
15
|
+
if (error instanceof ArcubaseCodeGenError) {
|
|
16
|
+
throw new CLIError(error.code, error.message, 2)
|
|
17
|
+
}
|
|
18
|
+
throw error
|
|
359
19
|
}
|
|
360
20
|
}
|
package/src/runtime/execute.ts
CHANGED
|
@@ -295,7 +295,7 @@ function renderDevSDKGenHelp(): string {
|
|
|
295
295
|
' - arcubase-admin dev sdk-gen --app-id <app_id> --out src/arcubase',
|
|
296
296
|
'',
|
|
297
297
|
'requirements:',
|
|
298
|
-
' -
|
|
298
|
+
' - every generated field must have a configured field.key; sdk-gen fails fast when keys are missing',
|
|
299
299
|
' - existing field.key values must be stable unique TypeScript identifiers',
|
|
300
300
|
' - entity property names use entity.key when available, otherwise a stable entity<id> fallback',
|
|
301
301
|
' - generated code accepts createArcubaseSdk({ baseURL, accessToken, refreshToken })',
|
|
@@ -2006,11 +2006,6 @@ type SDKGenEntityRef = {
|
|
|
2006
2006
|
id: string
|
|
2007
2007
|
}
|
|
2008
2008
|
|
|
2009
|
-
type SDKGenInitializedFieldKeys = {
|
|
2010
|
-
entityId: number
|
|
2011
|
-
fields: string[]
|
|
2012
|
-
}
|
|
2013
|
-
|
|
2014
2009
|
function extractSDKGenAppPayload(payload: unknown): Record<string, any> {
|
|
2015
2010
|
const appPayload = unwrapResponseData(payload)
|
|
2016
2011
|
if (!isRecord(appPayload)) {
|
|
@@ -2056,47 +2051,38 @@ function walkSDKGenFields(fields: unknown, visit: (field: Record<string, any>) =
|
|
|
2056
2051
|
}
|
|
2057
2052
|
}
|
|
2058
2053
|
|
|
2059
|
-
function
|
|
2060
|
-
const
|
|
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 }> = []
|
|
2054
|
+
function assertSDKGenFieldKeysPresent(entity: Record<string, any>) {
|
|
2055
|
+
const missing: Array<{ id: number; label: string }> = []
|
|
2087
2056
|
walkSDKGenFields(entity.fields, (field) => {
|
|
2088
2057
|
if (typeof field.id !== 'number') {
|
|
2089
2058
|
return
|
|
2090
2059
|
}
|
|
2091
2060
|
if (typeof field.key === 'string' && field.key.trim() !== '') {
|
|
2092
|
-
field.key = field.key.trim()
|
|
2093
2061
|
return
|
|
2094
2062
|
}
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2063
|
+
missing.push({
|
|
2064
|
+
id: field.id,
|
|
2065
|
+
label: typeof field.label === 'string' && field.label.trim() ? field.label.trim() : `field ${field.id}`,
|
|
2066
|
+
})
|
|
2067
|
+
})
|
|
2068
|
+
if (missing.length === 0) {
|
|
2069
|
+
return
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
const entityName = typeof entity.name === 'string' && entity.name.trim() ? entity.name.trim() : `entity ${String(entity.id ?? '')}`.trim()
|
|
2073
|
+
const preview = missing.slice(0, 8).map((field) => `${entityName}.${field.label} (${field.id})`)
|
|
2074
|
+
const suffix = missing.length > preview.length ? `, and ${missing.length - preview.length} more` : ''
|
|
2075
|
+
throw new CLIError('SDK_GEN_FIELD_KEY_REQUIRED', `missing field.key values: ${preview.join(', ')}${suffix}`, 2, {
|
|
2076
|
+
operation: 'dev sdk-gen',
|
|
2077
|
+
issues: missing.map((field) => ({
|
|
2078
|
+
path: `entity.${String(entity.id ?? 'unknown')}.field.${field.id}.key`,
|
|
2079
|
+
message: `field.key is required for ${entityName}.${field.label}`,
|
|
2080
|
+
})),
|
|
2081
|
+
suggestions: [
|
|
2082
|
+
'set stable field.key values in Arcubase admin before running sdk-gen',
|
|
2083
|
+
'rerun arcubase-admin dev sdk-gen after the schema keys are complete',
|
|
2084
|
+
],
|
|
2098
2085
|
})
|
|
2099
|
-
return updates
|
|
2100
2086
|
}
|
|
2101
2087
|
|
|
2102
2088
|
async function executeDevSDKGen(
|
|
@@ -2131,7 +2117,6 @@ async function executeDevSDKGen(
|
|
|
2131
2117
|
throw new CLIError('SDK_GEN_NO_ENTITIES', 'sdk-gen could not find entities in app detail response', 2)
|
|
2132
2118
|
}
|
|
2133
2119
|
const entities = []
|
|
2134
|
-
const initializedFieldKeys: SDKGenInitializedFieldKeys[] = []
|
|
2135
2120
|
for (const entityRef of entityRefs) {
|
|
2136
2121
|
const entityEndpoint = `/api/apps/${encodeURIComponent(appId)}/entity/${encodeURIComponent(entityRef.id)}`
|
|
2137
2122
|
const entityDetail = await requestJSON(runtimeEnv, 'GET', entityEndpoint, undefined, fetchImpl)
|
|
@@ -2139,15 +2124,7 @@ async function executeDevSDKGen(
|
|
|
2139
2124
|
if (!isRecord(entityPayload)) {
|
|
2140
2125
|
throw new CLIError('SDK_GEN_INVALID_SCHEMA', 'sdk-gen expected entity detail response data', 2)
|
|
2141
2126
|
}
|
|
2142
|
-
|
|
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
|
-
}
|
|
2127
|
+
assertSDKGenFieldKeysPresent(entityPayload)
|
|
2151
2128
|
entities.push(entityPayload)
|
|
2152
2129
|
}
|
|
2153
2130
|
const generated = generateArcubaseProjectSDK({
|
|
@@ -2173,7 +2150,6 @@ async function executeDevSDKGen(
|
|
|
2173
2150
|
data: {
|
|
2174
2151
|
out: absoluteOutDir,
|
|
2175
2152
|
files: generated.files.map((file) => file.path),
|
|
2176
|
-
initializedFieldKeys,
|
|
2177
2153
|
},
|
|
2178
2154
|
}
|
|
2179
2155
|
}
|
|
@@ -177,24 +177,16 @@ test('arcubase-admin dev sdk-gen fetches app schema and writes typed sdk files',
|
|
|
177
177
|
assert.match(fs.readFileSync(path.join(outDir, 'client.ts'), 'utf8'), /createArcubaseSdk/)
|
|
178
178
|
})
|
|
179
179
|
|
|
180
|
-
test('arcubase-admin dev sdk-gen
|
|
180
|
+
test('arcubase-admin dev sdk-gen fails before generation when field keys are missing', async () => {
|
|
181
181
|
const outDir = fs.mkdtempSync(path.join(os.tmpdir(), 'arcubase-sdk-gen-'))
|
|
182
|
-
const calls: Array<{ url: string; method?: string
|
|
182
|
+
const calls: Array<{ url: string; method?: string }> = []
|
|
183
183
|
|
|
184
|
-
|
|
184
|
+
await assert.rejects(() => executeCLI(
|
|
185
185
|
'admin',
|
|
186
186
|
['dev', 'sdk-gen', '--app-id', '3882856425', '--out', outDir],
|
|
187
187
|
env as any,
|
|
188
188
|
async (url, init) => {
|
|
189
|
-
|
|
190
|
-
calls.push({
|
|
191
|
-
url: String(url),
|
|
192
|
-
method: init?.method,
|
|
193
|
-
authorization: init?.headers instanceof Headers
|
|
194
|
-
? init.headers.get('Authorization') ?? undefined
|
|
195
|
-
: (init?.headers as Record<string, string> | undefined)?.Authorization,
|
|
196
|
-
body: bodyText ? JSON.parse(bodyText) : undefined,
|
|
197
|
-
})
|
|
189
|
+
calls.push({ url: String(url), method: init?.method })
|
|
198
190
|
if (String(url).endsWith('/api/apps/3882856425')) {
|
|
199
191
|
return new Response(JSON.stringify({ data: appDetail() }), { status: 200 })
|
|
200
192
|
}
|
|
@@ -204,23 +196,15 @@ test('arcubase-admin dev sdk-gen initializes missing field keys through admin au
|
|
|
204
196
|
if (String(url).endsWith('/api/apps/3882856425/entity/3883547182')) {
|
|
205
197
|
return new Response(JSON.stringify({ data: entityDetailWithoutFieldKeys() }), { status: 200 })
|
|
206
198
|
}
|
|
207
|
-
if (String(url).endsWith('/api/apps/3882856425/entity/3883547182/custom-keys')) {
|
|
208
|
-
return new Response(JSON.stringify({ data: entityDetail() }), { status: 200 })
|
|
209
|
-
}
|
|
210
199
|
return new Response(JSON.stringify({ error: { message: `unexpected url ${url}` } }), { status: 404 })
|
|
211
200
|
},
|
|
212
|
-
)
|
|
201
|
+
), (error: unknown) => {
|
|
202
|
+
assert.ok(error instanceof CLIError)
|
|
203
|
+
assert.equal(error.code, 'SDK_GEN_FIELD_KEY_REQUIRED')
|
|
204
|
+
assert.match(error.message, /编故事\.时代 \(1006\)/)
|
|
205
|
+
return true
|
|
206
|
+
})
|
|
213
207
|
|
|
214
|
-
assert.equal(
|
|
215
|
-
|
|
216
|
-
assert.ok(customKeysCall)
|
|
217
|
-
assert.equal(customKeysCall.method, 'PUT')
|
|
218
|
-
assert.equal(customKeysCall.authorization, 'Bearer tok')
|
|
219
|
-
assert.deepEqual(customKeysCall.body, [
|
|
220
|
-
{ id: 1006, key: 'field1006' },
|
|
221
|
-
{ id: 1013, key: 'field1013' },
|
|
222
|
-
{ id: 1011, key: 'field1011' },
|
|
223
|
-
])
|
|
224
|
-
assert.match(fs.readFileSync(path.join(outDir, 'entities/story.ts'), 'utf8'), /field1011: string/)
|
|
225
|
-
assert.deepEqual(result.data.initializedFieldKeys, [{ entityId: 3883547182, fields: ['field1006', 'field1013', 'field1011'] }])
|
|
208
|
+
assert.equal(calls.some((call) => call.url.endsWith('/custom-keys')), false)
|
|
209
|
+
assert.equal(fs.existsSync(path.join(outDir, 'index.ts')), false)
|
|
226
210
|
})
|
package/src/tests/help.test.ts
CHANGED
|
@@ -220,6 +220,17 @@ test('admin dashboard update-layout help-examples show top-level layout array',
|
|
|
220
220
|
assert.match(out.text, /do not wrap the array/)
|
|
221
221
|
})
|
|
222
222
|
|
|
223
|
+
test('admin dashboard update-options help-examples show user visibility scope', async () => {
|
|
224
|
+
const out = await executeCLI('admin', ['dashboard', 'update-options', '--help-examples'], env as any, async () => new Response('{}'))
|
|
225
|
+
assert.equal(out.kind, 'help')
|
|
226
|
+
assert.match(out.text, /arcubase-admin dashboard update-options --help-examples/)
|
|
227
|
+
assert.match(out.text, /"changed":\["users"\]/)
|
|
228
|
+
assert.match(out.text, /"users":\[\{"type":"user","id":2188889901\}\]/)
|
|
229
|
+
assert.match(out.text, /currentSpeaker\.arcubaseUserId/)
|
|
230
|
+
assert.match(out.text, /verify from the user side with arcubase app get/)
|
|
231
|
+
assert.doesNotMatch(out.text, /"type":"member"/)
|
|
232
|
+
})
|
|
233
|
+
|
|
223
234
|
test('admin widget preview-data help-examples show chart query body without widget id', async () => {
|
|
224
235
|
const out = await executeCLI('admin', ['widget', 'preview-data', '--help-examples'], env as any, async () => new Response('{}'))
|
|
225
236
|
assert.equal(out.kind, 'help')
|