@carthooks/arcubase-cli 0.1.24 → 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 +57 -54
- package/bundle/arcubase.mjs +57 -54
- 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 +2 -1
- package/sdk-dist/docs/runtime-reference/dev-sdk-gen.md +14 -3
- 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
|
@@ -1,319 +1,13 @@
|
|
|
1
|
+
import { ArcubaseCodeGenError, generateArcubaseProjectSDK as generateArcubaseProjectSDKCore, } from '@arcubase/code-gen';
|
|
1
2
|
import { CLIError } from './errors.js';
|
|
2
|
-
function
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
function unwrapData(payload) {
|
|
6
|
-
return isRecord(payload) && 'data' in payload ? payload.data : payload;
|
|
7
|
-
}
|
|
8
|
-
function toPascalCase(value) {
|
|
9
|
-
const words = value
|
|
10
|
-
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
11
|
-
.split(/[^A-Za-z0-9]+/)
|
|
12
|
-
.filter(Boolean);
|
|
13
|
-
if (words.length === 0)
|
|
14
|
-
return 'Entity';
|
|
15
|
-
return words.map((word) => `${word[0].toUpperCase()}${word.slice(1)}`).join('');
|
|
16
|
-
}
|
|
17
|
-
function toCamelCase(value) {
|
|
18
|
-
const pascal = toPascalCase(value);
|
|
19
|
-
return `${pascal[0].toLowerCase()}${pascal.slice(1)}`;
|
|
20
|
-
}
|
|
21
|
-
function sanitizeEntityKey(entity) {
|
|
22
|
-
const fallback = entity.sdkName || `entity_${entity.id}`;
|
|
23
|
-
return toCamelCase(fallback) === 'entity' ? `entity${entity.id}` : toCamelCase(fallback);
|
|
24
|
-
}
|
|
25
|
-
function normalizeFieldType(type) {
|
|
26
|
-
switch (type) {
|
|
27
|
-
case 'number':
|
|
28
|
-
return 'number';
|
|
29
|
-
case 'boolean':
|
|
30
|
-
return 'boolean';
|
|
31
|
-
case 'select':
|
|
32
|
-
case 'radio':
|
|
33
|
-
case 'checkbox':
|
|
34
|
-
case 'status':
|
|
35
|
-
return 'number | string';
|
|
36
|
-
case 'file':
|
|
37
|
-
case 'image':
|
|
38
|
-
case 'member':
|
|
39
|
-
case 'members':
|
|
40
|
-
case 'department':
|
|
41
|
-
case 'departments':
|
|
42
|
-
case 'linkto':
|
|
43
|
-
case 'relation':
|
|
44
|
-
case 'relationfield':
|
|
45
|
-
case 'subform':
|
|
46
|
-
return 'unknown';
|
|
47
|
-
default:
|
|
48
|
-
return 'string';
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
function normalizeEntities(payload) {
|
|
52
|
-
const root = unwrapData(payload);
|
|
53
|
-
if (!isRecord(root)) {
|
|
54
|
-
throw new CLIError('SDK_GEN_INVALID_SCHEMA', 'sdk-gen expected an app detail object', 2);
|
|
55
|
-
}
|
|
56
|
-
const appId = typeof root.id === 'number' ? root.id : typeof root.app_id === 'number' ? root.app_id : undefined;
|
|
57
|
-
if (!appId) {
|
|
58
|
-
throw new CLIError('SDK_GEN_APP_ID_REQUIRED', 'sdk-gen expected app id in app detail response', 2);
|
|
59
|
-
}
|
|
60
|
-
const sourceEntities = Array.isArray(root.entities)
|
|
61
|
-
? root.entities
|
|
62
|
-
: Array.isArray(root.tables)
|
|
63
|
-
? root.tables
|
|
64
|
-
: Array.isArray(root.entitys)
|
|
65
|
-
? root.entitys
|
|
66
|
-
: [];
|
|
67
|
-
const entities = sourceEntities
|
|
68
|
-
.filter((item) => isRecord(item))
|
|
69
|
-
.filter((item) => typeof item.id === 'number' && typeof item.name === 'string')
|
|
70
|
-
.map((item) => {
|
|
71
|
-
const fields = Array.isArray(item.fields)
|
|
72
|
-
? item.fields
|
|
73
|
-
.filter((field) => isRecord(field))
|
|
74
|
-
.filter((field) => typeof field.id === 'number' && typeof field.label === 'string' && typeof field.type === 'string')
|
|
75
|
-
.map((field) => ({
|
|
76
|
-
id: field.id,
|
|
77
|
-
label: field.label,
|
|
78
|
-
key: typeof field.key === 'string' ? field.key.trim() : '',
|
|
79
|
-
type: field.type,
|
|
80
|
-
required: field.required === true,
|
|
81
|
-
options: isRecord(field.options) ? field.options : undefined,
|
|
82
|
-
}))
|
|
83
|
-
: [];
|
|
84
|
-
const entity = {
|
|
85
|
-
appId,
|
|
86
|
-
id: item.id,
|
|
87
|
-
name: item.name,
|
|
88
|
-
sdkName: typeof item.key === 'string' && item.key.trim() ? item.key.trim() : '',
|
|
89
|
-
fields,
|
|
90
|
-
};
|
|
91
|
-
entity.sdkName = sanitizeEntityKey(entity);
|
|
92
|
-
return entity;
|
|
93
|
-
});
|
|
94
|
-
if (entities.length === 0) {
|
|
95
|
-
throw new CLIError('SDK_GEN_NO_ENTITIES', 'sdk-gen could not find entities in app detail response', 2);
|
|
3
|
+
export function generateArcubaseProjectSDK(payload) {
|
|
4
|
+
try {
|
|
5
|
+
return generateArcubaseProjectSDKCore(payload);
|
|
96
6
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
function validateEntities(entities) {
|
|
101
|
-
const entityKeys = new Set();
|
|
102
|
-
for (const entity of entities) {
|
|
103
|
-
if (entityKeys.has(entity.sdkName)) {
|
|
104
|
-
throw new CLIError('SDK_GEN_DUPLICATE_ENTITY_KEY', `duplicate generated entity key: ${entity.sdkName}`, 2);
|
|
105
|
-
}
|
|
106
|
-
entityKeys.add(entity.sdkName);
|
|
107
|
-
const fieldKeys = new Set();
|
|
108
|
-
for (const field of entity.fields) {
|
|
109
|
-
if (!field.key) {
|
|
110
|
-
throw new CLIError('SDK_GEN_FIELD_KEY_REQUIRED', `field.key is required for ${entity.name}.${field.label} (${field.id})`, 2);
|
|
111
|
-
}
|
|
112
|
-
if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(field.key)) {
|
|
113
|
-
throw new CLIError('SDK_GEN_INVALID_FIELD_KEY', `field.key must be a TypeScript identifier: ${entity.name}.${field.key}`, 2);
|
|
114
|
-
}
|
|
115
|
-
if (fieldKeys.has(field.key)) {
|
|
116
|
-
throw new CLIError('SDK_GEN_DUPLICATE_FIELD_KEY', `duplicate field.key in ${entity.name}: ${field.key}`, 2);
|
|
117
|
-
}
|
|
118
|
-
fieldKeys.add(field.key);
|
|
7
|
+
catch (error) {
|
|
8
|
+
if (error instanceof ArcubaseCodeGenError) {
|
|
9
|
+
throw new CLIError(error.code, error.message, 2);
|
|
119
10
|
}
|
|
11
|
+
throw error;
|
|
120
12
|
}
|
|
121
13
|
}
|
|
122
|
-
function emitRuntime() {
|
|
123
|
-
return `export type ArcubaseSdkConfig = {
|
|
124
|
-
baseURL: string
|
|
125
|
-
accessToken?: string
|
|
126
|
-
refreshToken?: string
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export type ArcubaseRow<TFields> = {
|
|
130
|
-
id: number
|
|
131
|
-
title?: string
|
|
132
|
-
fields: TFields
|
|
133
|
-
raw: Record<string, any>
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export type ArcubaseRuntime = {
|
|
137
|
-
config: ArcubaseSdkConfig
|
|
138
|
-
request<T>(method: string, endpoint: string, body?: unknown): Promise<T>
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
type ArcubaseEnvelope<T> = {
|
|
142
|
-
data?: T
|
|
143
|
-
error?: { message?: string; key?: string; type?: string }
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export function createRuntime(config: ArcubaseSdkConfig): ArcubaseRuntime {
|
|
147
|
-
let accessToken = config.accessToken ?? ''
|
|
148
|
-
let refreshToken = config.refreshToken ?? ''
|
|
149
|
-
|
|
150
|
-
async function request<T>(method: string, endpoint: string, body?: unknown): Promise<T> {
|
|
151
|
-
const baseURL = config.baseURL.endsWith('/') ? config.baseURL : \`\${config.baseURL}/\`
|
|
152
|
-
const response = await fetch(new URL(endpoint.replace(/^\\/+/, ''), baseURL).toString(), {
|
|
153
|
-
method,
|
|
154
|
-
headers: {
|
|
155
|
-
'Content-Type': 'application/json',
|
|
156
|
-
...(accessToken ? { Authorization: \`Bearer \${accessToken}\` } : {}),
|
|
157
|
-
},
|
|
158
|
-
body: body === undefined ? undefined : JSON.stringify(body),
|
|
159
|
-
})
|
|
160
|
-
const payload = await response.json().catch(() => null) as ArcubaseEnvelope<T> | null
|
|
161
|
-
if (!response.ok || payload?.error) {
|
|
162
|
-
throw new Error(payload?.error?.message ?? \`Arcubase request failed: \${response.status}\`)
|
|
163
|
-
}
|
|
164
|
-
return payload?.data as T
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return {
|
|
168
|
-
config: { ...config, accessToken, refreshToken },
|
|
169
|
-
request,
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
`;
|
|
173
|
-
}
|
|
174
|
-
function emitSchema(entities) {
|
|
175
|
-
const schema = {
|
|
176
|
-
entities: Object.fromEntries(entities.map((entity) => [
|
|
177
|
-
entity.sdkName,
|
|
178
|
-
{
|
|
179
|
-
entityId: entity.id,
|
|
180
|
-
name: entity.name,
|
|
181
|
-
fields: Object.fromEntries(entity.fields.map((field) => [
|
|
182
|
-
field.key,
|
|
183
|
-
{
|
|
184
|
-
fieldId: field.id,
|
|
185
|
-
label: field.label,
|
|
186
|
-
type: field.type,
|
|
187
|
-
required: field.required,
|
|
188
|
-
},
|
|
189
|
-
])),
|
|
190
|
-
},
|
|
191
|
-
])),
|
|
192
|
-
};
|
|
193
|
-
return `export const arcubaseSchema = ${JSON.stringify(schema, null, 2)} as const
|
|
194
|
-
|
|
195
|
-
export type ArcubaseEntityKey = keyof typeof arcubaseSchema.entities
|
|
196
|
-
`;
|
|
197
|
-
}
|
|
198
|
-
function emitEntity(entity) {
|
|
199
|
-
const typeName = toPascalCase(entity.sdkName);
|
|
200
|
-
const fieldLines = entity.fields.map((field) => {
|
|
201
|
-
const optional = field.required ? '' : '?';
|
|
202
|
-
return ` ${field.key}${optional}: ${normalizeFieldType(field.type)}`;
|
|
203
|
-
});
|
|
204
|
-
const fieldMapLines = entity.fields.map((field) => ` ${field.key}: ${field.id},`);
|
|
205
|
-
return `import type { ArcubaseRuntime, ArcubaseRow } from '../runtime.js'
|
|
206
|
-
|
|
207
|
-
export type ${typeName}Fields = {
|
|
208
|
-
${fieldLines.join('\n')}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
export type ${typeName}CreateInput = ${typeName}Fields
|
|
212
|
-
export type ${typeName}UpdateInput = Partial<${typeName}Fields>
|
|
213
|
-
|
|
214
|
-
const entityId = ${entity.id}
|
|
215
|
-
const fieldIds = {
|
|
216
|
-
${fieldMapLines.join('\n')}
|
|
217
|
-
} as const
|
|
218
|
-
|
|
219
|
-
function toArcubaseFields(fields: Partial<${typeName}Fields>): Record<string, any> {
|
|
220
|
-
const out: Record<string, any> = {}
|
|
221
|
-
for (const [key, value] of Object.entries(fields)) {
|
|
222
|
-
if (value === undefined) continue
|
|
223
|
-
const fieldId = fieldIds[key as keyof typeof fieldIds]
|
|
224
|
-
if (!fieldId) {
|
|
225
|
-
throw new Error(\`Unknown ${entity.sdkName} field: \${key}\`)
|
|
226
|
-
}
|
|
227
|
-
out[String(fieldId)] = value
|
|
228
|
-
}
|
|
229
|
-
return out
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function fromArcubaseFields(raw: Record<string, any> | undefined): ${typeName}Fields {
|
|
233
|
-
const out: Record<string, any> = {}
|
|
234
|
-
const source = raw ?? {}
|
|
235
|
-
for (const [key, fieldId] of Object.entries(fieldIds)) {
|
|
236
|
-
if (String(fieldId) in source) {
|
|
237
|
-
out[key] = source[String(fieldId)]
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
return out as ${typeName}Fields
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function mapRow(raw: Record<string, any>): ArcubaseRow<${typeName}Fields> {
|
|
244
|
-
return {
|
|
245
|
-
id: Number(raw.id),
|
|
246
|
-
title: typeof raw.title === 'string' ? raw.title : undefined,
|
|
247
|
-
fields: fromArcubaseFields(raw.fields),
|
|
248
|
-
raw,
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
export function create${typeName}SDK(runtime: ArcubaseRuntime) {
|
|
253
|
-
return {
|
|
254
|
-
async create(fields: ${typeName}CreateInput): Promise<number> {
|
|
255
|
-
return runtime.request<number>('POST', \`/entity/${entity.appId}/\${entityId}/insert\`, {
|
|
256
|
-
fields: toArcubaseFields(fields),
|
|
257
|
-
})
|
|
258
|
-
},
|
|
259
|
-
|
|
260
|
-
async update(rowId: string | number, fields: ${typeName}UpdateInput): Promise<boolean> {
|
|
261
|
-
const data = toArcubaseFields(fields)
|
|
262
|
-
return runtime.request<boolean>('PUT', \`/entity/${entity.appId}/\${entityId}/row/\${rowId}\`, {
|
|
263
|
-
data,
|
|
264
|
-
changed_fields: Object.keys(data).map((fieldId) => Number(fieldId)),
|
|
265
|
-
})
|
|
266
|
-
},
|
|
267
|
-
|
|
268
|
-
async get(rowId: string | number): Promise<ArcubaseRow<${typeName}Fields>> {
|
|
269
|
-
const data = await runtime.request<{ record?: Record<string, any> }>('GET', \`/entity/${entity.appId}/\${entityId}/row/\${rowId}\`)
|
|
270
|
-
return mapRow(data.record ?? {})
|
|
271
|
-
},
|
|
272
|
-
|
|
273
|
-
async query(params: { limit?: number; offset?: number; search?: Record<string, any> } = {}): Promise<{ items: ArcubaseRow<${typeName}Fields>[]; total?: number }> {
|
|
274
|
-
const data = await runtime.request<{ items?: Record<string, any>[]; total?: number }>('POST', \`/entity/${entity.appId}/\${entityId}/data-query\`, params)
|
|
275
|
-
return {
|
|
276
|
-
items: (data.items ?? []).map(mapRow),
|
|
277
|
-
total: data.total,
|
|
278
|
-
}
|
|
279
|
-
},
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
`;
|
|
283
|
-
}
|
|
284
|
-
function emitClient(entities) {
|
|
285
|
-
const imports = entities.map((entity) => {
|
|
286
|
-
const typeName = toPascalCase(entity.sdkName);
|
|
287
|
-
return `import { create${typeName}SDK } from './entities/${entity.sdkName}.js'`;
|
|
288
|
-
});
|
|
289
|
-
const properties = entities.map((entity) => ` ${entity.sdkName}: create${toPascalCase(entity.sdkName)}SDK(runtime),`);
|
|
290
|
-
return `import { createRuntime, type ArcubaseSdkConfig } from './runtime.js'
|
|
291
|
-
${imports.join('\n')}
|
|
292
|
-
|
|
293
|
-
export function createArcubaseSdk(config: ArcubaseSdkConfig) {
|
|
294
|
-
const runtime = createRuntime(config)
|
|
295
|
-
return {
|
|
296
|
-
${properties.join('\n')}
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
`;
|
|
300
|
-
}
|
|
301
|
-
function emitIndex(entities) {
|
|
302
|
-
return `export { createArcubaseSdk } from './client.js'
|
|
303
|
-
export type { ArcubaseSdkConfig, ArcubaseRow } from './runtime.js'
|
|
304
|
-
export { arcubaseSchema } from './schema.js'
|
|
305
|
-
${entities.map((entity) => `export type { ${toPascalCase(entity.sdkName)}Fields, ${toPascalCase(entity.sdkName)}CreateInput, ${toPascalCase(entity.sdkName)}UpdateInput } from './entities/${entity.sdkName}.js'`).join('\n')}
|
|
306
|
-
`;
|
|
307
|
-
}
|
|
308
|
-
export function generateArcubaseProjectSDK(payload) {
|
|
309
|
-
const entities = normalizeEntities(payload);
|
|
310
|
-
return {
|
|
311
|
-
files: [
|
|
312
|
-
{ path: 'runtime.ts', contents: emitRuntime() },
|
|
313
|
-
{ path: 'schema.ts', contents: emitSchema(entities) },
|
|
314
|
-
...entities.map((entity) => ({ path: `entities/${entity.sdkName}.ts`, contents: emitEntity(entity) })),
|
|
315
|
-
{ path: 'client.ts', contents: emitClient(entities) },
|
|
316
|
-
{ path: 'index.ts', contents: emitIndex(entities) },
|
|
317
|
-
],
|
|
318
|
-
};
|
|
319
|
-
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"execute.d.ts","sourceRoot":"","sources":["../../src/runtime/execute.ts"],"names":[],"mappings":"AAGA,OAAO,EAAkB,KAAK,UAAU,EAAE,MAAM,UAAU,CAAA;AAG1D,OAAO,EAAyF,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAA;AAShJ,MAAM,MAAM,uBAAuB,GAAG;IACpC,KAAK,EAAE,YAAY,CAAA;IACnB,WAAW,EAAE,SAAS,MAAM,EAAE,CAAA;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,SAAS,EAAE,SAAS,MAAM,EAAE,CAAA;IAC5B,UAAU,EAAE,SAAS,MAAM,EAAE,CAAA;CAC9B,CAAA;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAe1D;AAaD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,GAAE,QAAsB,GAAG,MAAM,CAoB7G;AA8BD,KAAK,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;AAUlD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,EAAE,GAAG,GAAE,QAAsB,GAAG,MAAM,CA8HpI;
|
|
1
|
+
{"version":3,"file":"execute.d.ts","sourceRoot":"","sources":["../../src/runtime/execute.ts"],"names":[],"mappings":"AAGA,OAAO,EAAkB,KAAK,UAAU,EAAE,MAAM,UAAU,CAAA;AAG1D,OAAO,EAAyF,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAA;AAShJ,MAAM,MAAM,uBAAuB,GAAG;IACpC,KAAK,EAAE,YAAY,CAAA;IACnB,WAAW,EAAE,SAAS,MAAM,EAAE,CAAA;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,SAAS,EAAE,SAAS,MAAM,EAAE,CAAA;IAC5B,UAAU,EAAE,SAAS,MAAM,EAAE,CAAA;CAC9B,CAAA;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAe1D;AAaD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,GAAE,QAAsB,GAAG,MAAM,CAoB7G;AA8BD,KAAK,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;AAUlD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,EAAE,GAAG,GAAE,QAAsB,GAAG,MAAM,CA8HpI;AA6sED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,uBAAuB,CActH;AAED,wBAAsB,UAAU,CAAC,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,CAAC,EAAE,UAAU,EAAE,SAAS,GAAE,OAAO,KAAa,GAAG,OAAO,CAAC,GAAG,CAAC,CAyOrI"}
|
package/dist/runtime/execute.js
CHANGED
|
@@ -267,7 +267,7 @@ function renderDevSDKGenHelp() {
|
|
|
267
267
|
' - arcubase-admin dev sdk-gen --app-id <app_id> --out src/arcubase',
|
|
268
268
|
'',
|
|
269
269
|
'requirements:',
|
|
270
|
-
' -
|
|
270
|
+
' - every generated field must have a configured field.key; sdk-gen fails fast when keys are missing',
|
|
271
271
|
' - existing field.key values must be stable unique TypeScript identifiers',
|
|
272
272
|
' - entity property names use entity.key when available, otherwise a stable entity<id> fallback',
|
|
273
273
|
' - generated code accepts createArcubaseSdk({ baseURL, accessToken, refreshToken })',
|
|
@@ -1647,45 +1647,37 @@ function walkSDKGenFields(fields, visit) {
|
|
|
1647
1647
|
walkSDKGenFields(field.children, visit);
|
|
1648
1648
|
}
|
|
1649
1649
|
}
|
|
1650
|
-
function
|
|
1651
|
-
const
|
|
1652
|
-
walkSDKGenFields(entity.fields, (field) => {
|
|
1653
|
-
if (typeof field.key === 'string' && field.key.trim() !== '') {
|
|
1654
|
-
keys.add(field.key.trim());
|
|
1655
|
-
}
|
|
1656
|
-
});
|
|
1657
|
-
return keys;
|
|
1658
|
-
}
|
|
1659
|
-
function nextSDKGenFallbackFieldKey(fieldId, usedKeys) {
|
|
1660
|
-
const base = `field${fieldId}`;
|
|
1661
|
-
if (!usedKeys.has(base)) {
|
|
1662
|
-
usedKeys.add(base);
|
|
1663
|
-
return base;
|
|
1664
|
-
}
|
|
1665
|
-
let index = 2;
|
|
1666
|
-
while (usedKeys.has(`${base}_${index}`)) {
|
|
1667
|
-
index++;
|
|
1668
|
-
}
|
|
1669
|
-
const key = `${base}_${index}`;
|
|
1670
|
-
usedKeys.add(key);
|
|
1671
|
-
return key;
|
|
1672
|
-
}
|
|
1673
|
-
function initializeMissingSDKGenFieldKeys(entity) {
|
|
1674
|
-
const usedKeys = collectExistingSDKGenFieldKeys(entity);
|
|
1675
|
-
const updates = [];
|
|
1650
|
+
function assertSDKGenFieldKeysPresent(entity) {
|
|
1651
|
+
const missing = [];
|
|
1676
1652
|
walkSDKGenFields(entity.fields, (field) => {
|
|
1677
1653
|
if (typeof field.id !== 'number') {
|
|
1678
1654
|
return;
|
|
1679
1655
|
}
|
|
1680
1656
|
if (typeof field.key === 'string' && field.key.trim() !== '') {
|
|
1681
|
-
field.key = field.key.trim();
|
|
1682
1657
|
return;
|
|
1683
1658
|
}
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1659
|
+
missing.push({
|
|
1660
|
+
id: field.id,
|
|
1661
|
+
label: typeof field.label === 'string' && field.label.trim() ? field.label.trim() : `field ${field.id}`,
|
|
1662
|
+
});
|
|
1663
|
+
});
|
|
1664
|
+
if (missing.length === 0) {
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
1667
|
+
const entityName = typeof entity.name === 'string' && entity.name.trim() ? entity.name.trim() : `entity ${String(entity.id ?? '')}`.trim();
|
|
1668
|
+
const preview = missing.slice(0, 8).map((field) => `${entityName}.${field.label} (${field.id})`);
|
|
1669
|
+
const suffix = missing.length > preview.length ? `, and ${missing.length - preview.length} more` : '';
|
|
1670
|
+
throw new CLIError('SDK_GEN_FIELD_KEY_REQUIRED', `missing field.key values: ${preview.join(', ')}${suffix}`, 2, {
|
|
1671
|
+
operation: 'dev sdk-gen',
|
|
1672
|
+
issues: missing.map((field) => ({
|
|
1673
|
+
path: `entity.${String(entity.id ?? 'unknown')}.field.${field.id}.key`,
|
|
1674
|
+
message: `field.key is required for ${entityName}.${field.label}`,
|
|
1675
|
+
})),
|
|
1676
|
+
suggestions: [
|
|
1677
|
+
'set stable field.key values in Arcubase admin before running sdk-gen',
|
|
1678
|
+
'rerun arcubase-admin dev sdk-gen after the schema keys are complete',
|
|
1679
|
+
],
|
|
1687
1680
|
});
|
|
1688
|
-
return updates;
|
|
1689
1681
|
}
|
|
1690
1682
|
async function executeDevSDKGen(runtimeEnv, flags, fetchImpl) {
|
|
1691
1683
|
validateDevSDKGenFlags(flags);
|
|
@@ -1714,7 +1706,6 @@ async function executeDevSDKGen(runtimeEnv, flags, fetchImpl) {
|
|
|
1714
1706
|
throw new CLIError('SDK_GEN_NO_ENTITIES', 'sdk-gen could not find entities in app detail response', 2);
|
|
1715
1707
|
}
|
|
1716
1708
|
const entities = [];
|
|
1717
|
-
const initializedFieldKeys = [];
|
|
1718
1709
|
for (const entityRef of entityRefs) {
|
|
1719
1710
|
const entityEndpoint = `/api/apps/${encodeURIComponent(appId)}/entity/${encodeURIComponent(entityRef.id)}`;
|
|
1720
1711
|
const entityDetail = await requestJSON(runtimeEnv, 'GET', entityEndpoint, undefined, fetchImpl);
|
|
@@ -1722,15 +1713,7 @@ async function executeDevSDKGen(runtimeEnv, flags, fetchImpl) {
|
|
|
1722
1713
|
if (!isRecord(entityPayload)) {
|
|
1723
1714
|
throw new CLIError('SDK_GEN_INVALID_SCHEMA', 'sdk-gen expected entity detail response data', 2);
|
|
1724
1715
|
}
|
|
1725
|
-
|
|
1726
|
-
if (customKeyUpdates.length > 0) {
|
|
1727
|
-
const customKeysEndpoint = `/api/apps/${encodeURIComponent(appId)}/entity/${encodeURIComponent(entityRef.id)}/custom-keys`;
|
|
1728
|
-
await requestJSON(runtimeEnv, 'PUT', customKeysEndpoint, customKeyUpdates, fetchImpl);
|
|
1729
|
-
initializedFieldKeys.push({
|
|
1730
|
-
entityId: typeof entityPayload.id === 'number' ? entityPayload.id : Number(entityRef.id),
|
|
1731
|
-
fields: customKeyUpdates.map((item) => item.key),
|
|
1732
|
-
});
|
|
1733
|
-
}
|
|
1716
|
+
assertSDKGenFieldKeysPresent(entityPayload);
|
|
1734
1717
|
entities.push(entityPayload);
|
|
1735
1718
|
}
|
|
1736
1719
|
const generated = generateArcubaseProjectSDK({
|
|
@@ -1754,7 +1737,6 @@ async function executeDevSDKGen(runtimeEnv, flags, fetchImpl) {
|
|
|
1754
1737
|
data: {
|
|
1755
1738
|
out: absoluteOutDir,
|
|
1756
1739
|
files: generated.files.map((file) => file.path),
|
|
1757
|
-
initializedFieldKeys,
|
|
1758
1740
|
},
|
|
1759
1741
|
};
|
|
1760
1742
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@carthooks/arcubase-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.25",
|
|
4
4
|
"description": "Arcubase runtime CLI for admin and user command execution",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"docs:llms": "node -e \"\""
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
+
"@arcubase/code-gen": "^0.1.0",
|
|
34
35
|
"@carthooks/client-ts": "0.8.0",
|
|
35
36
|
"zod": "^4.4.3"
|
|
36
37
|
},
|
|
@@ -8,10 +8,16 @@ arcubase-admin dev sdk-gen \
|
|
|
8
8
|
--out src/arcubase
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
The command reads the app schema from Arcubase through the normal
|
|
11
|
+
The command reads the app schema from Arcubase through the normal
|
|
12
|
+
`arcubase-admin` runtime auth and writes project-local source files under
|
|
13
|
+
`--out`. This is the direct Arcubase admin/runtime path.
|
|
14
|
+
|
|
15
|
+
Workbench local ordinary-user codegen must not receive Arcubase admin tokens.
|
|
16
|
+
It should use the BotWorks Host/JCode schema flow documented in
|
|
17
|
+
`docs/design/workbench/03-workbench-vibe-coding-arcubase-codegen.md`.
|
|
12
18
|
|
|
13
19
|
Generated code is key based. Application code uses stable `field.key` names, while generated runtime code converts those keys to internal numeric field ids.
|
|
14
|
-
If
|
|
20
|
+
If any field is missing `field.key`, the command fails before generation and prints the missing fields. Set stable keys in Arcubase admin, then rerun the command.
|
|
15
21
|
|
|
16
22
|
```ts
|
|
17
23
|
import { createArcubaseSdk } from './arcubase'
|
|
@@ -31,9 +37,14 @@ await sdk.story.update(rowId, {
|
|
|
31
37
|
})
|
|
32
38
|
```
|
|
33
39
|
|
|
40
|
+
The direct `baseURL` and token construction above is for direct Arcubase
|
|
41
|
+
runtime consumers. Workbench browser runtime should wrap generated SDK calls
|
|
42
|
+
with BotWorks Workbench auth and the Host Arcubase proxy instead of exposing
|
|
43
|
+
Arcubase runtime credentials in browser code.
|
|
44
|
+
|
|
34
45
|
Rules:
|
|
35
46
|
|
|
36
|
-
-
|
|
47
|
+
- every visible SDK field must have a configured `field.key`; sdk-gen does not invent fallback keys
|
|
37
48
|
- existing `field.key` values must be stable unique TypeScript identifiers
|
|
38
49
|
- `field.key` is a developer API contract and should not track display labels
|
|
39
50
|
- entity property names use `entity.key` when available, otherwise a stable `entity<id>` fallback
|