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