@carthooks/arcubase-cli 0.1.21 → 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.
- package/bundle/arcubase-admin.mjs +1297 -79
- package/bundle/arcubase.mjs +1297 -79
- package/dist/generated/command_registry.generated.d.ts +285 -0
- package/dist/generated/command_registry.generated.d.ts.map +1 -1
- package/dist/generated/command_registry.generated.js +395 -0
- package/dist/generated/help_examples.generated.d.ts +77 -0
- package/dist/generated/help_examples.generated.d.ts.map +1 -1
- package/dist/generated/help_examples.generated.js +122 -0
- package/dist/generated/type_index.generated.d.ts +40 -1
- package/dist/generated/type_index.generated.d.ts.map +1 -1
- package/dist/generated/type_index.generated.js +42 -1
- package/dist/generated/zod_registry.generated.d.ts +29 -1
- package/dist/generated/zod_registry.generated.d.ts.map +1 -1
- package/dist/generated/zod_registry.generated.js +37 -0
- package/dist/runtime/dev_sdk_gen.d.ts +9 -0
- package/dist/runtime/dev_sdk_gen.d.ts.map +1 -0
- package/dist/runtime/dev_sdk_gen.js +319 -0
- package/dist/runtime/execute.d.ts.map +1 -1
- package/dist/runtime/execute.js +280 -1
- package/dist/runtime/zod_registry.d.ts.map +1 -1
- package/dist/runtime/zod_registry.js +58 -0
- package/package.json +1 -1
- package/sdk-dist/api/admin/index.ts +1 -0
- package/sdk-dist/api/admin/public-link.ts +48 -0
- package/sdk-dist/api/shared-link/form.ts +42 -1
- package/sdk-dist/api/user/index.ts +0 -1
- package/sdk-dist/docs/runtime-reference/dashboard.md +15 -0
- package/sdk-dist/docs/runtime-reference/dev-sdk-gen.md +41 -0
- package/sdk-dist/docs/runtime-reference/help-examples/admin/dashboard/update-layout.json +16 -0
- package/sdk-dist/docs/runtime-reference/help-examples/admin/widget/create.json +33 -0
- package/sdk-dist/docs/runtime-reference/help-examples/admin/widget/preview-data.json +42 -0
- package/sdk-dist/docs/runtime-reference/widgets.md +40 -0
- package/sdk-dist/generated/command_registry.generated.ts +395 -0
- package/sdk-dist/generated/help_examples.generated.ts +122 -0
- package/sdk-dist/generated/type_index.generated.ts +42 -1
- package/sdk-dist/generated/zod_registry.generated.ts +56 -0
- package/sdk-dist/types/app.ts +37 -1
- package/sdk-dist/types/common.ts +77 -35
- package/sdk-dist/types/index.ts +1 -1
- package/sdk-dist/types/public-link.ts +10 -0
- package/sdk-dist/types/shared-link.ts +16 -1
- package/sdk-dist/types/user-action.ts +1 -43
- package/src/generated/command_registry.generated.ts +395 -0
- package/src/generated/help_examples.generated.ts +122 -0
- package/src/generated/type_index.generated.ts +42 -1
- package/src/generated/zod_registry.generated.ts +56 -0
- package/src/runtime/dev_sdk_gen.ts +360 -0
- package/src/runtime/execute.ts +325 -1
- package/src/runtime/zod_registry.ts +58 -0
- package/src/tests/command_registry.test.ts +20 -2
- package/src/tests/dev_sdk_gen.test.ts +226 -0
- package/src/tests/docs_readability.test.ts +15 -0
- package/src/tests/execute_validation.test.ts +129 -0
- package/src/tests/help.test.ts +70 -0
- package/sdk-dist/api/user/copilot.ts +0 -20
- package/sdk-dist/types/copilot.ts +0 -34
|
@@ -42,12 +42,46 @@ export const AppIngressCreateReqVOSchema: z.ZodTypeAny = z.lazy(() => z.object({
|
|
|
42
42
|
|
|
43
43
|
export const AppIngressUpdateReqVOSchema: z.ZodTypeAny = z.lazy(() => z.object({ "enabled": z.boolean().optional(), "name": z.string().optional(), "options": EntityIngressOptionsSchema.optional(), "update": z.array(z.string()).optional() }).strict())
|
|
44
44
|
|
|
45
|
+
export const WidgetsTypeCodeSchema: z.ZodTypeAny = z.lazy(() => z.union([z.literal(1), z.literal(2)]))
|
|
46
|
+
|
|
47
|
+
export const WidgetsTypeSchema: z.ZodTypeAny = z.lazy(() => z.object({ "dashboard_id": z.number().optional(), "ingress_id": z.number().optional(), "type": WidgetsTypeCodeSchema.optional() }).strict())
|
|
48
|
+
|
|
49
|
+
export const AppNewWidgetsReqVOSchema: z.ZodTypeAny = z.lazy(() => z.object({ "options": WidgetsTypeSchema.optional(), "type": z.string().optional() }).strict())
|
|
50
|
+
|
|
51
|
+
export const CubeDimensionSchema: z.ZodTypeAny = z.lazy(() => z.object({ "FieldID": z.number().optional(), "IsField": z.boolean().optional(), "Label": z.string().optional(), "Name": z.string().optional(), "Options": z.record(z.string(), z.any()).optional(), "OrderBy": z.string().optional(), "PropName": z.string().optional() }).strict())
|
|
52
|
+
|
|
53
|
+
export const CubeMeasureAggregateSchema: z.ZodTypeAny = z.lazy(() => z.union([z.literal("sum"), z.literal("avg"), z.literal("max"), z.literal("min"), z.literal("count"), z.literal("distinct_count")]))
|
|
54
|
+
|
|
55
|
+
export const CubeMeasureSchema: z.ZodTypeAny = z.lazy(() => z.object({ "Aggregate": CubeMeasureAggregateSchema.optional(), "FieldID": z.number().optional(), "Label": z.string().optional(), "Name": z.string().optional(), "Options": z.record(z.string(), z.any()).optional(), "OrderBy": z.string().optional() }).strict())
|
|
56
|
+
|
|
57
|
+
export const AppPreviewWidgetsDataReqVOSchema: z.ZodTypeAny = z.lazy(() => z.object({ "conditions": PermitConditionSetSchema.optional(), "dataSource": z.number().optional(), "dataSourceType": z.string().optional(), "dimensions": z.array(CubeDimensionSchema).optional(), "measures": z.array(CubeMeasureSchema).optional() }).strict())
|
|
58
|
+
|
|
45
59
|
export const GormDeletedAtSchema: z.ZodTypeAny = z.lazy(() => z.object({ "Time": z.string().optional(), "Valid": z.boolean().optional() }).strict())
|
|
46
60
|
|
|
47
61
|
export const AppOptionsSchema: z.ZodTypeAny = z.lazy(() => z.object({ "use_home_dashboard": z.boolean().optional() }).strict())
|
|
48
62
|
|
|
49
63
|
export const AppUpdateAppReqVOSchema: z.ZodTypeAny = z.lazy(() => z.object({ "CreatedAt": z.string().optional(), "DeletedAt": GormDeletedAtSchema.optional(), "ID": z.number().optional(), "Options": AppOptionsSchema.optional(), "UpdatedAt": z.string().optional(), "changed": z.array(z.string()).optional(), "has_workflow": z.boolean().optional(), "icon_color": z.string().optional(), "icon_name": z.string().optional(), "name": z.string().optional(), "tenant_id": z.number().optional(), "use_home_dashboard": z.boolean().optional() }).strict())
|
|
50
64
|
|
|
65
|
+
export const WidgetsListViewOptionsSchema: z.ZodTypeAny = z.lazy(() => z.object({ "cond_follow": z.boolean().optional(), "cond_follow_cols": z.array(z.string()).optional(), "cond_follow_tab": z.boolean().optional(), "link": z.record(z.string(), z.any()).optional() }).strict())
|
|
66
|
+
|
|
67
|
+
export const AppUpdateWidgetsIngressOptionsReqVOSchema: z.ZodTypeAny = z.lazy(() => z.object({ "options": WidgetsListViewOptionsSchema.optional() }).strict())
|
|
68
|
+
|
|
69
|
+
export const ChartOptionSchema: z.ZodTypeAny = z.lazy(() => z.object({ "conditions": PermitConditionSetSchema.optional(), "dimensions": z.array(CubeDimensionSchema).optional(), "entity_id": z.number().optional(), "measures": z.array(CubeMeasureSchema).optional(), "options": z.record(z.string(), z.any()).optional(), "type": z.string().optional() }).strict())
|
|
70
|
+
|
|
71
|
+
export const WidgetsOptionSchema: z.ZodTypeAny = z.lazy(() => z.object({ "charts": ChartOptionSchema.optional(), "ingress": WidgetsListViewOptionsSchema.optional(), "render": z.record(z.string(), z.any()).optional() }).strict())
|
|
72
|
+
|
|
73
|
+
export const AppUpdateWidgetsReqVOSchema: z.ZodTypeAny = z.lazy(() => z.object({ "entity_id": z.number().optional(), "options": WidgetsOptionSchema.optional() }).strict())
|
|
74
|
+
|
|
75
|
+
export const DashboardCreateReqVOSchema: z.ZodTypeAny = z.lazy(() => z.object({ "name": z.string().optional() }).strict())
|
|
76
|
+
|
|
77
|
+
export const LayoutPositionSchema: z.ZodTypeAny = z.lazy(() => z.object({ "h": z.number().optional(), "id": z.number().optional(), "w": z.number().optional(), "x": z.number().optional(), "y": z.number().optional() }).strict())
|
|
78
|
+
|
|
79
|
+
export const DashboardUpdateLayoutReqVOSchema: z.ZodTypeAny = z.lazy(() => z.array(LayoutPositionSchema))
|
|
80
|
+
|
|
81
|
+
export const DashboardUpdateOptionsReqVOSchema: z.ZodTypeAny = z.lazy(() => z.object({ "changed": z.array(z.string()).optional(), "i18n_name": z.record(z.string(), z.string()).optional(), "theme": z.object({ "gutter": z.number().optional(), "has_shadow": z.boolean().optional(), "round_corners": z.number().optional() }).strict().optional(), "users": z.array(PermitUserScopeSchema).optional() }).strict())
|
|
82
|
+
|
|
83
|
+
export const DashboardUpdateTitleReqVOSchema: z.ZodTypeAny = z.lazy(() => z.object({ "name": z.string().optional() }).strict())
|
|
84
|
+
|
|
51
85
|
export const EntityBulkUpdateActionVOSchema: z.ZodTypeAny = z.lazy(() => z.object({ "number_add": z.number().optional(), "number_mul": z.number().optional(), "number_sub": z.number().optional(), "text_append": z.string().optional(), "text_prepend": z.string().optional(), "text_replace": z.string().optional(), "text_search": z.string().optional(), "type": z.string().optional(), "value": z.any().optional() }).strict())
|
|
52
86
|
|
|
53
87
|
export const EntityBulkUpdateFieldVOSchema: z.ZodTypeAny = z.lazy(() => z.object({ "action": EntityBulkUpdateActionVOSchema.optional(), "id": z.number().optional() }).strict())
|
|
@@ -144,6 +178,10 @@ export const EntityWorkflowDeployReqVOSchema: z.ZodTypeAny = z.lazy(() => z.obje
|
|
|
144
178
|
|
|
145
179
|
export const EntityWorkflowUpdateReqVOSchema: z.ZodTypeAny = z.lazy(() => z.object({ "diagram": WorkflowConfigDiagramSchema.optional(), "id": z.number().optional(), "options": WorkflowOptionsSchema.optional() }).strict())
|
|
146
180
|
|
|
181
|
+
export const EntityUtilsQueryOrderSchema: z.ZodTypeAny = z.lazy(() => z.object({ "column": z.string().optional(), "order": z.string().optional() }).strict())
|
|
182
|
+
|
|
183
|
+
export const FetchWidgetsDataReqVOSchema: z.ZodTypeAny = z.lazy(() => z.object({ "dashboard_id": z.number().optional(), "ingress_hash_id": z.string().optional(), "ingress_id": z.number().optional(), "querys": z.object({ "search": z.record(z.string(), z.any()).optional(), "sorts": z.array(EntityUtilsQueryOrderSchema).optional() }).strict().optional(), "type": WidgetsTypeCodeSchema.optional() }).strict())
|
|
184
|
+
|
|
147
185
|
export const GetUploadTokenReqVOSchema: z.ZodTypeAny = z.lazy(() => z.object({ "global": z.boolean().optional() }).strict())
|
|
148
186
|
|
|
149
187
|
export const InsertReqVOSchema: z.ZodTypeAny = z.lazy(() => z.object({ "fields": z.record(z.string(), z.any()).optional(), "policy_id": z.string().optional() }).strict())
|
|
@@ -172,7 +210,15 @@ export const generatedRequestTypes = [
|
|
|
172
210
|
"AppIngressBulkApplyReqVO",
|
|
173
211
|
"AppIngressCreateReqVO",
|
|
174
212
|
"AppIngressUpdateReqVO",
|
|
213
|
+
"AppNewWidgetsReqVO",
|
|
214
|
+
"AppPreviewWidgetsDataReqVO",
|
|
175
215
|
"AppUpdateAppReqVO",
|
|
216
|
+
"AppUpdateWidgetsIngressOptionsReqVO",
|
|
217
|
+
"AppUpdateWidgetsReqVO",
|
|
218
|
+
"DashboardCreateReqVO",
|
|
219
|
+
"DashboardUpdateLayoutReqVO",
|
|
220
|
+
"DashboardUpdateOptionsReqVO",
|
|
221
|
+
"DashboardUpdateTitleReqVO",
|
|
176
222
|
"EntityBulkUpdateReqVO",
|
|
177
223
|
"EntityDoActionReqVO",
|
|
178
224
|
"EntityQueryRelationReqVO",
|
|
@@ -185,6 +231,7 @@ export const generatedRequestTypes = [
|
|
|
185
231
|
"EntityWorkflowApproveUsersReqVO",
|
|
186
232
|
"EntityWorkflowDeployReqVO",
|
|
187
233
|
"EntityWorkflowUpdateReqVO",
|
|
234
|
+
"FetchWidgetsDataReqVO",
|
|
188
235
|
"GetUploadTokenReqVO",
|
|
189
236
|
"InsertReqVO",
|
|
190
237
|
"MobileUploadCheckReqVO",
|
|
@@ -199,7 +246,15 @@ export const bodySchemas = {
|
|
|
199
246
|
"AppIngressBulkApplyReqVO": AppIngressBulkApplyReqVOSchema,
|
|
200
247
|
"AppIngressCreateReqVO": AppIngressCreateReqVOSchema,
|
|
201
248
|
"AppIngressUpdateReqVO": AppIngressUpdateReqVOSchema,
|
|
249
|
+
"AppNewWidgetsReqVO": AppNewWidgetsReqVOSchema,
|
|
250
|
+
"AppPreviewWidgetsDataReqVO": AppPreviewWidgetsDataReqVOSchema,
|
|
202
251
|
"AppUpdateAppReqVO": AppUpdateAppReqVOSchema,
|
|
252
|
+
"AppUpdateWidgetsIngressOptionsReqVO": AppUpdateWidgetsIngressOptionsReqVOSchema,
|
|
253
|
+
"AppUpdateWidgetsReqVO": AppUpdateWidgetsReqVOSchema,
|
|
254
|
+
"DashboardCreateReqVO": DashboardCreateReqVOSchema,
|
|
255
|
+
"DashboardUpdateLayoutReqVO": DashboardUpdateLayoutReqVOSchema,
|
|
256
|
+
"DashboardUpdateOptionsReqVO": DashboardUpdateOptionsReqVOSchema,
|
|
257
|
+
"DashboardUpdateTitleReqVO": DashboardUpdateTitleReqVOSchema,
|
|
203
258
|
"EntityBulkUpdateReqVO": EntityBulkUpdateReqVOSchema,
|
|
204
259
|
"EntityDoActionReqVO": EntityDoActionReqVOSchema,
|
|
205
260
|
"EntityQueryRelationReqVO": EntityQueryRelationReqVOSchema,
|
|
@@ -212,6 +267,7 @@ export const bodySchemas = {
|
|
|
212
267
|
"EntityWorkflowApproveUsersReqVO": EntityWorkflowApproveUsersReqVOSchema,
|
|
213
268
|
"EntityWorkflowDeployReqVO": EntityWorkflowDeployReqVOSchema,
|
|
214
269
|
"EntityWorkflowUpdateReqVO": EntityWorkflowUpdateReqVOSchema,
|
|
270
|
+
"FetchWidgetsDataReqVO": FetchWidgetsDataReqVOSchema,
|
|
215
271
|
"GetUploadTokenReqVO": GetUploadTokenReqVOSchema,
|
|
216
272
|
"InsertReqVO": InsertReqVOSchema,
|
|
217
273
|
"MobileUploadCheckReqVO": MobileUploadCheckReqVOSchema,
|
|
@@ -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
|
+
}
|