@carthooks/arcubase-cli 0.1.25 → 0.1.26

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.
@@ -1,20 +1,666 @@
1
- import {
2
- ArcubaseCodeGenError,
3
- generateArcubaseProjectSDK as generateArcubaseProjectSDKCore,
4
- type GeneratedSDK,
5
- } from '@arcubase/code-gen'
6
-
7
1
  import { CLIError } from './errors.js'
8
2
 
9
- export type { GeneratedSDK, GeneratedSDKFile } from '@arcubase/code-gen'
3
+ export type GeneratedSDKFile = {
4
+ path: string
5
+ contents: string
6
+ }
10
7
 
11
- export function generateArcubaseProjectSDK(payload: unknown): GeneratedSDK {
12
- try {
13
- return generateArcubaseProjectSDKCore(payload)
14
- } catch (error) {
15
- if (error instanceof ArcubaseCodeGenError) {
16
- throw new CLIError(error.code, error.message, 2)
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
+ children?: ArcubaseField[]
20
+ }
21
+
22
+ type ArcubaseEntity = {
23
+ appId: number
24
+ id: number
25
+ name: string
26
+ fields: ArcubaseField[]
27
+ }
28
+
29
+ type ArcubaseIngress = {
30
+ appId: number
31
+ id?: number
32
+ hashId: string
33
+ key: string
34
+ name: string
35
+ actions: string[]
36
+ entity: ArcubaseEntity
37
+ listOptions?: Record<string, unknown>
38
+ }
39
+
40
+ const identifierPattern = /^[A-Za-z][A-Za-z0-9_]*$/
41
+
42
+ function isRecord(value: unknown): value is Record<string, any> {
43
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
44
+ }
45
+
46
+ function unwrapData(payload: unknown): unknown {
47
+ return isRecord(payload) && 'data' in payload ? payload.data : payload
48
+ }
49
+
50
+ function toPascalCase(value: string): string {
51
+ const words = value
52
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
53
+ .split(/[^A-Za-z0-9]+/)
54
+ .filter(Boolean)
55
+ if (words.length === 0) return 'Ingress'
56
+ return words.map((word) => `${word[0].toUpperCase()}${word.slice(1)}`).join('')
57
+ }
58
+
59
+ function normalizeFieldType(type: string): string {
60
+ switch (type) {
61
+ case 'number':
62
+ return 'number'
63
+ case 'boolean':
64
+ return 'boolean'
65
+ case 'select':
66
+ case 'radio':
67
+ case 'checkbox':
68
+ case 'status':
69
+ return 'number | string'
70
+ case 'file':
71
+ case 'image':
72
+ case 'member':
73
+ case 'members':
74
+ case 'department':
75
+ case 'departments':
76
+ case 'linkto':
77
+ case 'relation':
78
+ case 'relationfield':
79
+ case 'subform':
80
+ return 'unknown'
81
+ default:
82
+ return 'string'
83
+ }
84
+ }
85
+
86
+ function normalizeField(field: Record<string, unknown>): ArcubaseField | undefined {
87
+ if (typeof field.id !== 'number' || typeof field.label !== 'string' || typeof field.type !== 'string') {
88
+ return undefined
89
+ }
90
+ const children = Array.isArray(field.children)
91
+ ? field.children
92
+ .filter((child): child is Record<string, unknown> => isRecord(child))
93
+ .map(normalizeField)
94
+ .filter((child): child is ArcubaseField => Boolean(child))
95
+ : undefined
96
+ return {
97
+ id: field.id,
98
+ label: field.label,
99
+ key: typeof field.key === 'string' ? field.key.trim() : '',
100
+ type: field.type,
101
+ required: field.required === true,
102
+ options: isRecord(field.options) ? field.options : undefined,
103
+ children,
104
+ }
105
+ }
106
+
107
+ function normalizeEntity(payload: unknown, appId: number): ArcubaseEntity {
108
+ if (!isRecord(payload) || typeof payload.id !== 'number' || typeof payload.name !== 'string') {
109
+ throw new CLIError('SDK_GEN_INVALID_SCHEMA', 'sdk-gen expected ingress.entity with id and name', 2)
110
+ }
111
+ const fields = Array.isArray(payload.fields)
112
+ ? payload.fields
113
+ .filter((field): field is Record<string, unknown> => isRecord(field))
114
+ .map(normalizeField)
115
+ .filter((field): field is ArcubaseField => Boolean(field))
116
+ : []
117
+ return {
118
+ appId,
119
+ id: payload.id,
120
+ name: payload.name,
121
+ fields,
122
+ }
123
+ }
124
+
125
+ function normalizeActions(ingress: Record<string, any>): string[] {
126
+ const policy = isRecord(ingress.policy)
127
+ ? ingress.policy
128
+ : isRecord(ingress.options) && isRecord(ingress.options.policy)
129
+ ? ingress.options.policy
130
+ : undefined
131
+ const source = Array.isArray(ingress.actions) ? ingress.actions : Array.isArray(policy?.actions) ? policy.actions : []
132
+ return source.filter((action): action is string => typeof action === 'string' && action.trim() !== '').map((action) => action.trim())
133
+ }
134
+
135
+ function normalizeIngresses(payload: unknown): ArcubaseIngress[] {
136
+ const root = unwrapData(payload)
137
+ if (!isRecord(root)) {
138
+ throw new CLIError('SDK_GEN_INVALID_SCHEMA', 'sdk-gen expected an app ingress schema object', 2)
139
+ }
140
+ const appId = typeof root.id === 'number' ? root.id : typeof root.app_id === 'number' ? root.app_id : undefined
141
+ if (!appId) {
142
+ throw new CLIError('SDK_GEN_APP_ID_REQUIRED', 'sdk-gen expected app id in schema payload', 2)
143
+ }
144
+ const sourceIngresses = Array.isArray(root.ingresses) ? root.ingresses : []
145
+ if (sourceIngresses.length === 0) {
146
+ throw new CLIError('SDK_GEN_NO_INGRESSES', 'sdk-gen could not find ingresses in schema payload', 2)
147
+ }
148
+ const ingresses = sourceIngresses
149
+ .filter((item): item is Record<string, any> => isRecord(item))
150
+ .map((item) => {
151
+ const entity = normalizeEntity(item.entity, appId)
152
+ const options = isRecord(item.options) ? item.options : {}
153
+ const hashId = typeof item.hash_id === 'string'
154
+ ? item.hash_id.trim()
155
+ : typeof item.hashId === 'string'
156
+ ? item.hashId.trim()
157
+ : ''
158
+ return {
159
+ appId,
160
+ id: typeof item.id === 'number' ? item.id : undefined,
161
+ hashId,
162
+ key: typeof item.key === 'string' ? item.key.trim() : '',
163
+ name: typeof item.name === 'string' && item.name.trim() ? item.name.trim() : String(item.key ?? ''),
164
+ actions: normalizeActions(item),
165
+ entity,
166
+ listOptions: isRecord(options.list_options) ? options.list_options : isRecord(item.list_options) ? item.list_options : undefined,
167
+ }
168
+ })
169
+ validateIngresses(ingresses)
170
+ return ingresses
171
+ }
172
+
173
+ function walkFields(fields: ArcubaseField[], visit: (field: ArcubaseField) => void) {
174
+ for (const field of fields) {
175
+ visit(field)
176
+ if (field.children && field.children.length > 0) {
177
+ walkFields(field.children, visit)
178
+ }
179
+ }
180
+ }
181
+
182
+ function allFields(entity: ArcubaseEntity): ArcubaseField[] {
183
+ const out: ArcubaseField[] = []
184
+ walkFields(entity.fields, (field) => out.push(field))
185
+ return out
186
+ }
187
+
188
+ function validateIngresses(ingresses: ArcubaseIngress[]) {
189
+ const ingressKeys = new Set<string>()
190
+ for (const ingress of ingresses) {
191
+ if (!ingress.key) {
192
+ throw new CLIError('SDK_GEN_INGRESS_KEY_REQUIRED', `ingress.key is required for ${ingress.name || 'unnamed ingress'}`, 2)
193
+ }
194
+ if (!identifierPattern.test(ingress.key)) {
195
+ throw new CLIError('SDK_GEN_INVALID_INGRESS_KEY', `ingress.key must be a TypeScript identifier: ${ingress.key}`, 2)
196
+ }
197
+ if (ingressKeys.has(ingress.key)) {
198
+ throw new CLIError('SDK_GEN_DUPLICATE_INGRESS_KEY', `duplicate ingress.key: ${ingress.key}`, 2)
199
+ }
200
+ ingressKeys.add(ingress.key)
201
+ if (!ingress.hashId) {
202
+ throw new CLIError('SDK_GEN_INGRESS_HASH_ID_REQUIRED', `ingress.hash_id is required for ${ingress.key}`, 2)
203
+ }
204
+
205
+ const fieldKeys = new Set<string>()
206
+ for (const field of allFields(ingress.entity)) {
207
+ if (!field.key) {
208
+ throw new CLIError('SDK_GEN_FIELD_KEY_REQUIRED', `field.key is required for ${ingress.entity.name}.${field.label} (${field.id})`, 2)
209
+ }
210
+ if (!identifierPattern.test(field.key)) {
211
+ throw new CLIError('SDK_GEN_INVALID_FIELD_KEY', `field.key must be a TypeScript identifier: ${ingress.entity.name}.${field.key}`, 2)
212
+ }
213
+ if (fieldKeys.has(field.key)) {
214
+ throw new CLIError('SDK_GEN_DUPLICATE_FIELD_KEY', `duplicate field.key in ${ingress.entity.name}: ${field.key}`, 2)
215
+ }
216
+ fieldKeys.add(field.key)
217
+ }
218
+ }
219
+ }
220
+
221
+ function hasAction(ingress: ArcubaseIngress, action: string): boolean {
222
+ return ingress.actions.includes(action)
223
+ }
224
+
225
+ function methodLines(lines: string[]): string {
226
+ return lines.length > 0 ? `${lines.join('\n\n')}\n` : ''
227
+ }
228
+
229
+ function emitRuntime(): string {
230
+ return `export type ArcubaseClientConfig = {
231
+ baseURL: string
232
+ accessToken?: string
233
+ refreshToken?: string
234
+ }
235
+
236
+ export type ArcubaseRow<TFields> = {
237
+ id: number
238
+ title?: string
239
+ fields: TFields
240
+ raw: Record<string, any>
241
+ }
242
+
243
+ export type ArcubaseRuntime = {
244
+ request<T>(method: string, endpoint: string, body?: unknown): Promise<T>
245
+ }
246
+
247
+ type ArcubaseEnvelope<T> = {
248
+ data?: T
249
+ error?: { message?: string; key?: string; type?: string }
250
+ }
251
+
252
+ export function createRuntime(config: ArcubaseClientConfig): ArcubaseRuntime {
253
+ let accessToken = config.accessToken ?? ''
254
+ let refreshToken = config.refreshToken ?? ''
255
+
256
+ async function request<T>(method: string, endpoint: string, body?: unknown): Promise<T> {
257
+ const baseURL = config.baseURL.endsWith('/') ? config.baseURL : \`\${config.baseURL}/\`
258
+ const response = await fetch(new URL(endpoint.replace(/^\\/+/, ''), baseURL).toString(), {
259
+ method,
260
+ headers: {
261
+ 'Content-Type': 'application/json',
262
+ ...(accessToken ? { Authorization: \`Bearer \${accessToken}\` } : {}),
263
+ ...(refreshToken ? { 'X-Arcubase-Refresh-Token': refreshToken } : {}),
264
+ },
265
+ body: body === undefined ? undefined : JSON.stringify(body),
266
+ })
267
+ const payload = await response.json().catch(() => null) as ArcubaseEnvelope<T> | null
268
+ if (!response.ok || payload?.error) {
269
+ throw new Error(payload?.error?.message ?? \`Arcubase request failed: \${response.status}\`)
270
+ }
271
+ return payload?.data as T
272
+ }
273
+
274
+ return { request }
275
+ }
276
+
277
+ export function toArcubaseFields<T extends Record<string, any>>(fields: Partial<T>, fieldIds: Record<string, number>): Record<string, any> {
278
+ const out: Record<string, any> = {}
279
+ for (const [key, value] of Object.entries(fields)) {
280
+ if (value === undefined) continue
281
+ const fieldId = fieldIds[key]
282
+ if (!fieldId) {
283
+ throw new Error(\`Unknown Arcubase field: \${key}\`)
284
+ }
285
+ out[String(fieldId)] = value
286
+ }
287
+ return out
288
+ }
289
+
290
+ export function fromArcubaseFields<T>(raw: Record<string, any> | undefined, fieldIds: Record<string, number>): T {
291
+ const out: Record<string, any> = {}
292
+ const source = raw ?? {}
293
+ for (const [key, fieldId] of Object.entries(fieldIds)) {
294
+ if (String(fieldId) in source) {
295
+ out[key] = source[String(fieldId)]
296
+ }
297
+ }
298
+ return out as T
299
+ }
300
+
301
+ export function toArcubaseColumn(column: string, fieldIds: Record<string, number>): string {
302
+ return fieldIds[column] ? \`:\${fieldIds[column]}\` : column
303
+ }
304
+
305
+ export function mapArcubaseCondition(value: unknown, fieldIds: Record<string, number>): unknown {
306
+ if (Array.isArray(value)) {
307
+ return value.map((item) => mapArcubaseCondition(item, fieldIds))
308
+ }
309
+ if (!value || typeof value !== 'object') {
310
+ return value
311
+ }
312
+ const out: Record<string, unknown> = {}
313
+ for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
314
+ if (key === 'column' && typeof child === 'string') {
315
+ out[key] = toArcubaseColumn(child, fieldIds)
316
+ } else {
317
+ const mappedKey = fieldIds[key] ? \`:\${fieldIds[key]}\` : key
318
+ out[mappedKey] = mapArcubaseCondition(child, fieldIds)
17
319
  }
18
- throw error
320
+ }
321
+ return out
322
+ }
323
+
324
+ export function mapArcubaseSorts<T extends { column: string; order?: string }>(sorts: T[] | undefined, fieldIds: Record<string, number>): T[] | undefined {
325
+ return sorts?.map((sort) => ({ ...sort, column: toArcubaseColumn(sort.column, fieldIds) }))
326
+ }
327
+ `
328
+ }
329
+
330
+ function emitSchema(ingresses: ArcubaseIngress[]): string {
331
+ const schema = {
332
+ ingresses: Object.fromEntries(ingresses.map((ingress) => [
333
+ ingress.key,
334
+ {
335
+ appId: ingress.appId,
336
+ ingressId: ingress.hashId,
337
+ entityId: ingress.entity.id,
338
+ name: ingress.name,
339
+ actions: ingress.actions,
340
+ fields: Object.fromEntries(allFields(ingress.entity).map((field) => [
341
+ field.key,
342
+ {
343
+ fieldId: field.id,
344
+ label: field.label,
345
+ type: field.type,
346
+ required: field.required,
347
+ },
348
+ ])),
349
+ },
350
+ ])),
351
+ }
352
+ return `export const arcubaseSchema = ${JSON.stringify(schema, null, 2)} as const
353
+
354
+ export type ArcubaseIngressKey = keyof typeof arcubaseSchema.ingresses
355
+ `
356
+ }
357
+
358
+ function emitIngress(ingress: ArcubaseIngress): string {
359
+ const typeName = toPascalCase(ingress.key)
360
+ const fields = allFields(ingress.entity)
361
+ const fieldLines = fields.map((field) => {
362
+ const optional = field.required ? '' : '?'
363
+ return ` ${field.key}${optional}: ${normalizeFieldType(field.type)}`
364
+ })
365
+ const fieldMapLines = fields.map((field) => ` ${field.key}: ${field.id},`)
366
+ const methodChunks: string[] = []
367
+
368
+ if (hasAction(ingress, 'view')) {
369
+ methodChunks.push(` async query(params: ${typeName}QueryParams = {}): Promise<{ items: ArcubaseRow<${typeName}Fields>[]; total?: number; offset?: number; limit?: number }> {
370
+ const data = await runtime.request<{ items?: Record<string, any>[]; total?: number; offset?: number; limit?: number }>('POST', \`/entity/${ingress.appId}/${ingress.entity.id}/data-query\`, {
371
+ ...params,
372
+ policy_id: ingressId,
373
+ search: mapArcubaseCondition(params.search, fieldIds),
374
+ sorts: mapArcubaseSorts(params.sorts, fieldIds),
375
+ })
376
+ return {
377
+ items: (data.items ?? []).map(mapRow),
378
+ total: data.total,
379
+ offset: data.offset,
380
+ limit: data.limit,
381
+ }
382
+ },
383
+
384
+ async get(rowId: string | number): Promise<ArcubaseRow<${typeName}Fields>> {
385
+ const data = await runtime.request<{ record?: Record<string, any> }>('GET', \`/entity/${ingress.appId}/${ingress.entity.id}/row/\${rowId}?policy_id=\${encodeURIComponent(ingressId)}\`)
386
+ return mapRow(data.record ?? {})
387
+ },`)
388
+ }
389
+
390
+ if (hasAction(ingress, 'add')) {
391
+ methodChunks.push(` async prepareCreate(): Promise<Record<string, any>> {
392
+ return runtime.request<Record<string, any>>('POST', \`/entity/${ingress.appId}/${ingress.entity.id}/prepare\`, {
393
+ policy_id: ingressId,
394
+ })
395
+ },
396
+
397
+ async create(fields: ${typeName}CreateInput): Promise<number> {
398
+ return runtime.request<number>('POST', \`/entity/${ingress.appId}/${ingress.entity.id}/insert\`, {
399
+ fields: toArcubaseFields(fields, fieldIds),
400
+ policy_id: ingressId,
401
+ })
402
+ },`)
403
+ }
404
+
405
+ if (hasAction(ingress, 'edit')) {
406
+ methodChunks.push(` async update(rowId: string | number, fields: ${typeName}UpdateInput): Promise<boolean> {
407
+ const data = toArcubaseFields(fields, fieldIds)
408
+ return runtime.request<boolean>('PUT', \`/entity/${ingress.appId}/${ingress.entity.id}/row/\${rowId}\`, {
409
+ data,
410
+ changed_fields: Object.keys(data).map((fieldId) => Number(fieldId)),
411
+ policy_id: ingressId,
412
+ })
413
+ },`)
414
+ }
415
+
416
+ if (hasAction(ingress, 'delete')) {
417
+ methodChunks.push(` async delete(selection: ${typeName}Selection): Promise<{ deleted?: number } | boolean> {
418
+ return runtime.request<{ deleted?: number } | boolean>('POST', \`/entity/${ingress.appId}/${ingress.entity.id}/delete-item\`, {
419
+ policy_id: ingressId,
420
+ selection: toArcubaseSelection(selection),
421
+ })
422
+ },`)
423
+ }
424
+
425
+ if (hasAction(ingress, 'bulk_update')) {
426
+ methodChunks.push(` async bulkUpdate(selection: ${typeName}Selection, fields: ${typeName}BulkUpdateField[]): Promise<{ updated: number }> {
427
+ return runtime.request<{ updated: number }>('POST', \`/entity/${ingress.appId}/${ingress.entity.id}/bulk-update\`, {
428
+ policy_id: ingressId,
429
+ selection: toArcubaseSelection(selection),
430
+ fields: fields.map((field) => ({
431
+ id: fieldIds[field.key],
432
+ action: field.action,
433
+ })),
434
+ })
435
+ },
436
+
437
+ async saveBulkUpdate(input: { name: string; subject: string; fields: ${typeName}BulkUpdateField[] }): Promise<string> {
438
+ return runtime.request<string>('POST', \`/entity/${ingress.appId}/${ingress.entity.id}/bulk-update-save-as\`, {
439
+ name: input.name,
440
+ subject: input.subject,
441
+ fields: input.fields.map((field) => ({
442
+ id: fieldIds[field.key],
443
+ action: field.action,
444
+ })),
445
+ })
446
+ },`)
447
+ }
448
+
449
+ if (hasAction(ingress, 'export')) {
450
+ methodChunks.push(` async exportRows(options: ${typeName}ExportOptions = {}): Promise<{ task_id: string }> {
451
+ const fieldKeys = options.fields ?? Object.keys(fieldIds) as ${typeName}FieldKey[]
452
+ return runtime.request<{ task_id: string }>('POST', \`/entity/${ingress.appId}/${ingress.entity.id}/export\`, {
453
+ policy_id: ingressId,
454
+ props: options.props ?? [],
455
+ fields: fieldKeys.map((key) => fieldIds[key]),
456
+ propnames: options.propNames ?? {},
457
+ options: { has_id: options.hasIdField ?? false },
458
+ })
459
+ },
460
+
461
+ async exportTask(taskId: string): Promise<Record<string, any>> {
462
+ return runtime.request<Record<string, any>>('GET', \`/export-task/\${taskId}\`)
463
+ },`)
464
+ }
465
+
466
+ if (hasAction(ingress, 'batch_print')) {
467
+ methodChunks.push(` async batchPrint(selection: ${typeName}Selection, params: { limit?: number; offset?: number } = {}): Promise<{ items?: Record<string, any>[]; total?: number }> {
468
+ return runtime.request<{ items?: Record<string, any>[]; total?: number }>('POST', \`/entity/${ingress.appId}/${ingress.entity.id}/selection-query/batch_print\`, {
469
+ ...params,
470
+ policy_id: ingressId,
471
+ selection: toArcubaseSelection(selection),
472
+ })
473
+ },`)
474
+ }
475
+
476
+ if (hasAction(ingress, 'archive')) {
477
+ methodChunks.push(` async archive(selection: ${typeName}Selection): Promise<{ archived?: number } | boolean> {
478
+ return runtime.request<{ archived?: number } | boolean>('POST', \`/entity/${ingress.appId}/${ingress.entity.id}/selection-query/archive\`, {
479
+ policy_id: ingressId,
480
+ selection: toArcubaseSelection(selection),
481
+ })
482
+ },`)
483
+ }
484
+
485
+ if (hasAction(ingress, 'tag_manage')) {
486
+ methodChunks.push(` tags: {
487
+ async list(): Promise<string[]> {
488
+ return runtime.request<string[]>('GET', \`/user-action-entity/${ingress.appId}/${ingress.entity.id}/tag-names\`)
489
+ },
490
+ async add(selection: ${typeName}Selection, tagNames: string[]): Promise<Record<string, any>> {
491
+ return runtime.request<Record<string, any>>('POST', \`/user-action-entity/${ingress.appId}/${ingress.entity.id}/batch-add-tags\`, {
492
+ policy_id: ingressId,
493
+ selection: toArcubaseSelection(selection),
494
+ tag_names: tagNames,
495
+ })
496
+ },
497
+ async remove(selection: ${typeName}Selection, tagNames: string[]): Promise<boolean> {
498
+ return runtime.request<boolean>('POST', \`/user-action-entity/${ingress.appId}/${ingress.entity.id}/batch-remove-tags\`, {
499
+ policy_id: ingressId,
500
+ selection: toArcubaseSelection(selection),
501
+ tag_names: tagNames,
502
+ })
503
+ },
504
+ async selection(selection: ${typeName}Selection): Promise<string[]> {
505
+ return runtime.request<string[]>('POST', \`/user-action-entity/${ingress.appId}/${ingress.entity.id}/get-selection-tags\`, {
506
+ policy_id: ingressId,
507
+ selection: toArcubaseSelection(selection),
508
+ })
509
+ },
510
+ },`)
511
+ }
512
+
513
+ const customActions = ingress.actions.filter((action) => !['view', 'add', 'edit', 'delete', 'bulk_update', 'export', 'batch_print', 'archive', 'tag_manage'].includes(action))
514
+ if (customActions.length > 0) {
515
+ methodChunks.push(` async action(action: ${typeName}CustomAction, payload: Record<string, any> = {}): Promise<Record<string, any>> {
516
+ if (!allowedActions.includes(action)) {
517
+ throw new Error(\`Action is not allowed for ${ingress.key}: \${action}\`)
518
+ }
519
+ return runtime.request<Record<string, any>>('POST', \`/entity/${ingress.appId}/${ingress.entity.id}/selection-query/\${encodeURIComponent(action)}\`, {
520
+ ...payload,
521
+ policy_id: ingressId,
522
+ selection: payload.selection ? toArcubaseSelection(payload.selection) : undefined,
523
+ })
524
+ },`)
525
+ }
526
+
527
+ return `import {
528
+ fromArcubaseFields,
529
+ mapArcubaseCondition,
530
+ mapArcubaseSorts,
531
+ toArcubaseFields,
532
+ type ArcubaseRow,
533
+ type ArcubaseRuntime,
534
+ } from '../runtime.js'
535
+
536
+ export type ${typeName}FieldKey = ${fields.map((field) => `'${field.key}'`).join(' | ')}
537
+
538
+ export type ${typeName}Fields = {
539
+ ${fieldLines.join('\n')}
540
+ }
541
+
542
+ export type ${typeName}CreateInput = ${typeName}Fields
543
+ export type ${typeName}UpdateInput = Partial<${typeName}Fields>
544
+ export type ${typeName}Action = ${ingress.actions.map((action) => `'${action}'`).join(' | ') || 'never'}
545
+ export type ${typeName}CustomAction = ${customActions.map((action) => `'${action}'`).join(' | ') || 'never'}
546
+
547
+ export type ${typeName}Sort = {
548
+ column: ${typeName}FieldKey | 'id' | 'created_at' | 'updated_at' | 'creator' | string
549
+ order: 'asc' | 'desc'
550
+ }
551
+
552
+ export type ${typeName}QueryParams = {
553
+ limit?: number
554
+ offset?: number
555
+ search?: Record<string, any>
556
+ sorts?: ${typeName}Sort[]
557
+ view_mode?: 'normal' | 'deleted' | 'archived'
558
+ withSubForm?: boolean
559
+ }
560
+
561
+ export type ${typeName}Selection = {
562
+ type: 'ids' | 'condition'
563
+ length?: number
564
+ ids?: Array<number | string>
565
+ condition?: Record<string, any>
566
+ sorts?: ${typeName}Sort[]
567
+ }
568
+
569
+ export type ${typeName}BulkUpdateField = {
570
+ key: ${typeName}FieldKey
571
+ action: Record<string, any>
572
+ }
573
+
574
+ export type ${typeName}ExportOptions = {
575
+ fields?: ${typeName}FieldKey[]
576
+ props?: string[]
577
+ propNames?: Record<string, string>
578
+ hasIdField?: boolean
579
+ }
580
+
581
+ const ingressId = ${JSON.stringify(ingress.hashId)}
582
+ const fieldIds = {
583
+ ${fieldMapLines.join('\n')}
584
+ } as const
585
+ const allowedActions = ${JSON.stringify(customActions)} as const
586
+
587
+ function mapRow(raw: Record<string, any>): ArcubaseRow<${typeName}Fields> {
588
+ return {
589
+ id: Number(raw.id),
590
+ title: typeof raw.title === 'string' ? raw.title : undefined,
591
+ fields: fromArcubaseFields<${typeName}Fields>(raw.fields, fieldIds),
592
+ raw,
593
+ }
594
+ }
595
+
596
+ function toArcubaseSelection(selection: ${typeName}Selection): Record<string, any> {
597
+ return {
598
+ ...selection,
599
+ condition: mapArcubaseCondition(selection.condition ?? {}, fieldIds),
600
+ sorts: mapArcubaseSorts(selection.sorts, fieldIds),
601
+ }
602
+ }
603
+
604
+ export function create${typeName}Ingress(runtime: ArcubaseRuntime) {
605
+ return {
606
+ ${methodLines(methodChunks)} }
607
+ }
608
+ `
609
+ }
610
+
611
+ function emitClient(ingresses: ArcubaseIngress[]): string {
612
+ const imports = ingresses.map((ingress) => {
613
+ const typeName = toPascalCase(ingress.key)
614
+ return `import { create${typeName}Ingress } from './ingresses/${ingress.key}.js'`
615
+ })
616
+ const mapLines = ingresses.map((ingress) => ` ${JSON.stringify(ingress.key)}: ReturnType<typeof create${toPascalCase(ingress.key)}Ingress>`)
617
+ const cases = ingresses.map((ingress) => ` case ${JSON.stringify(ingress.key)}:
618
+ return create${toPascalCase(ingress.key)}Ingress(runtime) as ArcubaseIngressMap[K]`)
619
+ return `import { createRuntime, type ArcubaseClientConfig } from './runtime.js'
620
+ ${imports.join('\n')}
621
+
622
+ export type ArcubaseIngressMap = {
623
+ ${mapLines.join('\n')}
624
+ }
625
+
626
+ export type ArcubaseIngressKey = keyof ArcubaseIngressMap
627
+
628
+ export function createArcubaseClient(config: ArcubaseClientConfig) {
629
+ const runtime = createRuntime(config)
630
+ return {
631
+ ingress<K extends ArcubaseIngressKey>(key: K): ArcubaseIngressMap[K] {
632
+ switch (key) {
633
+ ${cases.join('\n')}
634
+ default:
635
+ throw new Error(\`Unknown Arcubase ingress: \${String(key)}\`)
636
+ }
637
+ },
638
+ }
639
+ }
640
+ `
641
+ }
642
+
643
+ function emitIndex(ingresses: ArcubaseIngress[]): string {
644
+ return `export { createArcubaseClient } from './client.js'
645
+ export type { ArcubaseClientConfig, ArcubaseRow } from './runtime.js'
646
+ export { arcubaseSchema } from './schema.js'
647
+ export type { ArcubaseIngressKey, ArcubaseIngressMap } from './client.js'
648
+ ${ingresses.map((ingress) => {
649
+ const typeName = toPascalCase(ingress.key)
650
+ return `export type { ${typeName}Fields, ${typeName}CreateInput, ${typeName}UpdateInput, ${typeName}FieldKey, ${typeName}Selection } from './ingresses/${ingress.key}.js'`
651
+ }).join('\n')}
652
+ `
653
+ }
654
+
655
+ export function generateArcubaseProjectSDK(payload: unknown): GeneratedSDK {
656
+ const ingresses = normalizeIngresses(payload)
657
+ return {
658
+ files: [
659
+ { path: 'runtime.ts', contents: emitRuntime() },
660
+ { path: 'schema.ts', contents: emitSchema(ingresses) },
661
+ ...ingresses.map((ingress) => ({ path: `ingresses/${ingress.key}.ts`, contents: emitIngress(ingress) })),
662
+ { path: 'client.ts', contents: emitClient(ingresses) },
663
+ { path: 'index.ts', contents: emitIndex(ingresses) },
664
+ ],
19
665
  }
20
666
  }