@carthooks/arcubase-cli 0.1.20 → 0.1.22

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 (60) hide show
  1. package/bundle/arcubase-admin.mjs +1669 -163
  2. package/bundle/arcubase.mjs +1669 -163
  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 +191 -0
  7. package/dist/generated/help_examples.generated.d.ts.map +1 -1
  8. package/dist/generated/help_examples.generated.js +277 -0
  9. package/dist/generated/type_index.generated.d.ts +44 -1
  10. package/dist/generated/type_index.generated.d.ts.map +1 -1
  11. package/dist/generated/type_index.generated.js +46 -1
  12. package/dist/generated/zod_registry.generated.d.ts +32 -1
  13. package/dist/generated/zod_registry.generated.d.ts.map +1 -1
  14. package/dist/generated/zod_registry.generated.js +41 -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 +384 -32
  20. package/dist/runtime/zod_registry.d.ts.map +1 -1
  21. package/dist/runtime/zod_registry.js +66 -0
  22. package/package.json +1 -1
  23. package/sdk-dist/api/admin/index.ts +1 -0
  24. package/sdk-dist/api/admin/ingress.ts +11 -0
  25. package/sdk-dist/api/admin/public-link.ts +48 -0
  26. package/sdk-dist/api/shared-link/form.ts +42 -1
  27. package/sdk-dist/api/user/index.ts +0 -1
  28. package/sdk-dist/docs/runtime-reference/access-rule.md +19 -0
  29. package/sdk-dist/docs/runtime-reference/dashboard.md +15 -0
  30. package/sdk-dist/docs/runtime-reference/dev-sdk-gen.md +41 -0
  31. package/sdk-dist/docs/runtime-reference/help-examples/admin/access-rule/bulk-apply.json +105 -0
  32. package/sdk-dist/docs/runtime-reference/help-examples/admin/dashboard/update-layout.json +16 -0
  33. package/sdk-dist/docs/runtime-reference/help-examples/admin/widget/create.json +33 -0
  34. package/sdk-dist/docs/runtime-reference/help-examples/admin/widget/preview-data.json +42 -0
  35. package/sdk-dist/docs/runtime-reference/widgets.md +40 -0
  36. package/sdk-dist/generated/command_registry.generated.ts +416 -0
  37. package/sdk-dist/generated/help_examples.generated.ts +277 -0
  38. package/sdk-dist/generated/type_index.generated.ts +46 -1
  39. package/sdk-dist/generated/zod_registry.generated.ts +62 -0
  40. package/sdk-dist/types/app.ts +37 -1
  41. package/sdk-dist/types/common.ts +77 -35
  42. package/sdk-dist/types/index.ts +1 -1
  43. package/sdk-dist/types/ingress.ts +31 -0
  44. package/sdk-dist/types/public-link.ts +10 -0
  45. package/sdk-dist/types/shared-link.ts +16 -1
  46. package/sdk-dist/types/user-action.ts +1 -43
  47. package/src/generated/command_registry.generated.ts +416 -0
  48. package/src/generated/help_examples.generated.ts +277 -0
  49. package/src/generated/type_index.generated.ts +46 -1
  50. package/src/generated/zod_registry.generated.ts +62 -0
  51. package/src/runtime/dev_sdk_gen.ts +360 -0
  52. package/src/runtime/execute.ts +484 -52
  53. package/src/runtime/zod_registry.ts +66 -0
  54. package/src/tests/command_registry.test.ts +21 -2
  55. package/src/tests/dev_sdk_gen.test.ts +226 -0
  56. package/src/tests/docs_readability.test.ts +15 -0
  57. package/src/tests/execute_validation.test.ts +215 -0
  58. package/src/tests/help.test.ts +83 -0
  59. package/sdk-dist/api/user/copilot.ts +0 -20
  60. package/sdk-dist/types/copilot.ts +0 -34
@@ -0,0 +1,360 @@
1
+ import { CLIError } from './errors.js'
2
+
3
+ export type GeneratedSDKFile = {
4
+ path: string
5
+ contents: string
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 ?? ''
184
+
185
+ async function request<T>(method: string, endpoint: string, body?: unknown): Promise<T> {
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
+ }
340
+
341
+ function emitIndex(entities: ArcubaseEntity[]): string {
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
+ }
348
+
349
+ export function generateArcubaseProjectSDK(payload: unknown): GeneratedSDK {
350
+ const entities = normalizeEntities(payload)
351
+ return {
352
+ files: [
353
+ { path: 'runtime.ts', contents: emitRuntime() },
354
+ { path: 'schema.ts', contents: emitSchema(entities) },
355
+ ...entities.map((entity) => ({ path: `entities/${entity.sdkName}.ts`, contents: emitEntity(entity) })),
356
+ { path: 'client.ts', contents: emitClient(entities) },
357
+ { path: 'index.ts', contents: emitIndex(entities) },
358
+ ],
359
+ }
360
+ }