@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.
- package/bundle/arcubase-admin.mjs +527 -183
- package/bundle/arcubase.mjs +527 -183
- package/dist/generated/zod_registry.generated.d.ts.map +1 -1
- package/dist/generated/zod_registry.generated.js +3 -3
- package/dist/runtime/dev_sdk_gen.d.ts.map +1 -1
- package/dist/runtime/dev_sdk_gen.js +423 -144
- package/dist/runtime/execute.d.ts.map +1 -1
- package/dist/runtime/execute.js +122 -48
- package/package.json +1 -1
- package/sdk-dist/docs/runtime-reference/dev-sdk-gen.md +27 -12
- package/sdk-dist/generated/zod_registry.generated.ts +3 -3
- package/sdk-dist/types/common.ts +1 -0
- package/sdk-dist/types/ingress.ts +15 -0
- package/sdk-dist/types/user-action.ts +1 -0
- package/src/generated/zod_registry.generated.ts +3 -3
- package/src/runtime/dev_sdk_gen.ts +453 -147
- package/src/runtime/execute.ts +128 -51
- package/src/tests/dev_sdk_gen.test.ts +134 -61
- package/src/tests/help.test.ts +2 -1
- package/src/tests/zod_registry.test.ts +1 -0
|
@@ -16,16 +16,29 @@ type ArcubaseField = {
|
|
|
16
16
|
type: string
|
|
17
17
|
required: boolean
|
|
18
18
|
options?: Record<string, unknown>
|
|
19
|
+
children?: ArcubaseField[]
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
type ArcubaseEntity = {
|
|
22
23
|
appId: number
|
|
23
24
|
id: number
|
|
24
25
|
name: string
|
|
25
|
-
sdkName: string
|
|
26
26
|
fields: ArcubaseField[]
|
|
27
27
|
}
|
|
28
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
|
+
|
|
29
42
|
function isRecord(value: unknown): value is Record<string, any> {
|
|
30
43
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
31
44
|
}
|
|
@@ -39,20 +52,10 @@ function toPascalCase(value: string): string {
|
|
|
39
52
|
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
40
53
|
.split(/[^A-Za-z0-9]+/)
|
|
41
54
|
.filter(Boolean)
|
|
42
|
-
if (words.length === 0) return '
|
|
55
|
+
if (words.length === 0) return 'Ingress'
|
|
43
56
|
return words.map((word) => `${word[0].toUpperCase()}${word.slice(1)}`).join('')
|
|
44
57
|
}
|
|
45
58
|
|
|
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
59
|
function normalizeFieldType(type: string): string {
|
|
57
60
|
switch (type) {
|
|
58
61
|
case 'number':
|
|
@@ -80,82 +83,151 @@ function normalizeFieldType(type: string): string {
|
|
|
80
83
|
}
|
|
81
84
|
}
|
|
82
85
|
|
|
83
|
-
function
|
|
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[] {
|
|
84
136
|
const root = unwrapData(payload)
|
|
85
137
|
if (!isRecord(root)) {
|
|
86
|
-
throw new CLIError('SDK_GEN_INVALID_SCHEMA', 'sdk-gen expected an app
|
|
138
|
+
throw new CLIError('SDK_GEN_INVALID_SCHEMA', 'sdk-gen expected an app ingress schema object', 2)
|
|
87
139
|
}
|
|
88
140
|
const appId = typeof root.id === 'number' ? root.id : typeof root.app_id === 'number' ? root.app_id : undefined
|
|
89
141
|
if (!appId) {
|
|
90
|
-
throw new CLIError('SDK_GEN_APP_ID_REQUIRED', 'sdk-gen expected app id in
|
|
142
|
+
throw new CLIError('SDK_GEN_APP_ID_REQUIRED', 'sdk-gen expected app id in schema payload', 2)
|
|
91
143
|
}
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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')
|
|
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))
|
|
102
150
|
.map((item) => {
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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 = {
|
|
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 {
|
|
117
159
|
appId,
|
|
118
|
-
id: item.id
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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,
|
|
122
167
|
}
|
|
123
|
-
entity.sdkName = sanitizeEntityKey(entity)
|
|
124
|
-
return entity
|
|
125
168
|
})
|
|
126
|
-
|
|
127
|
-
|
|
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
|
+
}
|
|
128
179
|
}
|
|
129
|
-
validateEntities(entities)
|
|
130
|
-
return entities
|
|
131
180
|
}
|
|
132
181
|
|
|
133
|
-
function
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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)
|
|
138
203
|
}
|
|
139
|
-
entityKeys.add(entity.sdkName)
|
|
140
204
|
|
|
141
205
|
const fieldKeys = new Set<string>()
|
|
142
|
-
for (const field of entity
|
|
206
|
+
for (const field of allFields(ingress.entity)) {
|
|
143
207
|
if (!field.key) {
|
|
144
|
-
throw new CLIError('SDK_GEN_FIELD_KEY_REQUIRED', `field.key is required for ${entity.name}.${field.label} (${field.id})`, 2)
|
|
208
|
+
throw new CLIError('SDK_GEN_FIELD_KEY_REQUIRED', `field.key is required for ${ingress.entity.name}.${field.label} (${field.id})`, 2)
|
|
145
209
|
}
|
|
146
|
-
if (
|
|
147
|
-
throw new CLIError('SDK_GEN_INVALID_FIELD_KEY', `field.key must be a TypeScript identifier: ${entity.name}.${field.key}`, 2)
|
|
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)
|
|
148
212
|
}
|
|
149
213
|
if (fieldKeys.has(field.key)) {
|
|
150
|
-
throw new CLIError('SDK_GEN_DUPLICATE_FIELD_KEY', `duplicate field.key in ${entity.name}: ${field.key}`, 2)
|
|
214
|
+
throw new CLIError('SDK_GEN_DUPLICATE_FIELD_KEY', `duplicate field.key in ${ingress.entity.name}: ${field.key}`, 2)
|
|
151
215
|
}
|
|
152
216
|
fieldKeys.add(field.key)
|
|
153
217
|
}
|
|
154
218
|
}
|
|
155
219
|
}
|
|
156
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
|
+
|
|
157
229
|
function emitRuntime(): string {
|
|
158
|
-
return `export type
|
|
230
|
+
return `export type ArcubaseClientConfig = {
|
|
159
231
|
baseURL: string
|
|
160
232
|
accessToken?: string
|
|
161
233
|
refreshToken?: string
|
|
@@ -169,7 +241,6 @@ export type ArcubaseRow<TFields> = {
|
|
|
169
241
|
}
|
|
170
242
|
|
|
171
243
|
export type ArcubaseRuntime = {
|
|
172
|
-
config: ArcubaseSdkConfig
|
|
173
244
|
request<T>(method: string, endpoint: string, body?: unknown): Promise<T>
|
|
174
245
|
}
|
|
175
246
|
|
|
@@ -178,7 +249,7 @@ type ArcubaseEnvelope<T> = {
|
|
|
178
249
|
error?: { message?: string; key?: string; type?: string }
|
|
179
250
|
}
|
|
180
251
|
|
|
181
|
-
export function createRuntime(config:
|
|
252
|
+
export function createRuntime(config: ArcubaseClientConfig): ArcubaseRuntime {
|
|
182
253
|
let accessToken = config.accessToken ?? ''
|
|
183
254
|
let refreshToken = config.refreshToken ?? ''
|
|
184
255
|
|
|
@@ -189,6 +260,7 @@ export function createRuntime(config: ArcubaseSdkConfig): ArcubaseRuntime {
|
|
|
189
260
|
headers: {
|
|
190
261
|
'Content-Type': 'application/json',
|
|
191
262
|
...(accessToken ? { Authorization: \`Bearer \${accessToken}\` } : {}),
|
|
263
|
+
...(refreshToken ? { 'X-Arcubase-Refresh-Token': refreshToken } : {}),
|
|
192
264
|
},
|
|
193
265
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
194
266
|
})
|
|
@@ -199,22 +271,73 @@ export function createRuntime(config: ArcubaseSdkConfig): ArcubaseRuntime {
|
|
|
199
271
|
return payload?.data as T
|
|
200
272
|
}
|
|
201
273
|
|
|
202
|
-
return {
|
|
203
|
-
|
|
204
|
-
|
|
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
|
+
}
|
|
205
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)
|
|
319
|
+
}
|
|
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) }))
|
|
206
326
|
}
|
|
207
327
|
`
|
|
208
328
|
}
|
|
209
329
|
|
|
210
|
-
function emitSchema(
|
|
330
|
+
function emitSchema(ingresses: ArcubaseIngress[]): string {
|
|
211
331
|
const schema = {
|
|
212
|
-
|
|
213
|
-
|
|
332
|
+
ingresses: Object.fromEntries(ingresses.map((ingress) => [
|
|
333
|
+
ingress.key,
|
|
214
334
|
{
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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) => [
|
|
218
341
|
field.key,
|
|
219
342
|
{
|
|
220
343
|
fieldId: field.id,
|
|
@@ -228,19 +351,189 @@ function emitSchema(entities: ArcubaseEntity[]): string {
|
|
|
228
351
|
}
|
|
229
352
|
return `export const arcubaseSchema = ${JSON.stringify(schema, null, 2)} as const
|
|
230
353
|
|
|
231
|
-
export type
|
|
354
|
+
export type ArcubaseIngressKey = keyof typeof arcubaseSchema.ingresses
|
|
232
355
|
`
|
|
233
356
|
}
|
|
234
357
|
|
|
235
|
-
function
|
|
236
|
-
const typeName = toPascalCase(
|
|
237
|
-
const
|
|
358
|
+
function emitIngress(ingress: ArcubaseIngress): string {
|
|
359
|
+
const typeName = toPascalCase(ingress.key)
|
|
360
|
+
const fields = allFields(ingress.entity)
|
|
361
|
+
const fieldLines = fields.map((field) => {
|
|
238
362
|
const optional = field.required ? '' : '?'
|
|
239
363
|
return ` ${field.key}${optional}: ${normalizeFieldType(field.type)}`
|
|
240
364
|
})
|
|
241
|
-
const fieldMapLines =
|
|
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
|
+
},
|
|
242
383
|
|
|
243
|
-
|
|
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(' | ')}
|
|
244
537
|
|
|
245
538
|
export type ${typeName}Fields = {
|
|
246
539
|
${fieldLines.join('\n')}
|
|
@@ -248,113 +541,126 @@ ${fieldLines.join('\n')}
|
|
|
248
541
|
|
|
249
542
|
export type ${typeName}CreateInput = ${typeName}Fields
|
|
250
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'}
|
|
251
546
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
}
|
|
547
|
+
export type ${typeName}Sort = {
|
|
548
|
+
column: ${typeName}FieldKey | 'id' | 'created_at' | 'updated_at' | 'creator' | string
|
|
549
|
+
order: 'asc' | 'desc'
|
|
550
|
+
}
|
|
256
551
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
265
|
-
out[String(fieldId)] = value
|
|
266
|
-
}
|
|
267
|
-
return out
|
|
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
|
|
268
559
|
}
|
|
269
560
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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>
|
|
279
572
|
}
|
|
280
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
|
+
|
|
281
587
|
function mapRow(raw: Record<string, any>): ArcubaseRow<${typeName}Fields> {
|
|
282
588
|
return {
|
|
283
589
|
id: Number(raw.id),
|
|
284
590
|
title: typeof raw.title === 'string' ? raw.title : undefined,
|
|
285
|
-
fields: fromArcubaseFields(raw.fields),
|
|
591
|
+
fields: fromArcubaseFields<${typeName}Fields>(raw.fields, fieldIds),
|
|
286
592
|
raw,
|
|
287
593
|
}
|
|
288
594
|
}
|
|
289
595
|
|
|
290
|
-
|
|
596
|
+
function toArcubaseSelection(selection: ${typeName}Selection): Record<string, any> {
|
|
291
597
|
return {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
},
|
|
598
|
+
...selection,
|
|
599
|
+
condition: mapArcubaseCondition(selection.condition ?? {}, fieldIds),
|
|
600
|
+
sorts: mapArcubaseSorts(selection.sorts, fieldIds),
|
|
318
601
|
}
|
|
319
602
|
}
|
|
603
|
+
|
|
604
|
+
export function create${typeName}Ingress(runtime: ArcubaseRuntime) {
|
|
605
|
+
return {
|
|
606
|
+
${methodLines(methodChunks)} }
|
|
607
|
+
}
|
|
320
608
|
`
|
|
321
609
|
}
|
|
322
610
|
|
|
323
|
-
function emitClient(
|
|
324
|
-
const imports =
|
|
325
|
-
const typeName = toPascalCase(
|
|
326
|
-
return `import { create${typeName}
|
|
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'`
|
|
327
615
|
})
|
|
328
|
-
const
|
|
329
|
-
|
|
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'
|
|
330
620
|
${imports.join('\n')}
|
|
331
621
|
|
|
332
|
-
export
|
|
622
|
+
export type ArcubaseIngressMap = {
|
|
623
|
+
${mapLines.join('\n')}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
export type ArcubaseIngressKey = keyof ArcubaseIngressMap
|
|
627
|
+
|
|
628
|
+
export function createArcubaseClient(config: ArcubaseClientConfig) {
|
|
333
629
|
const runtime = createRuntime(config)
|
|
334
630
|
return {
|
|
335
|
-
|
|
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
|
+
},
|
|
336
638
|
}
|
|
337
639
|
}
|
|
338
640
|
`
|
|
339
641
|
}
|
|
340
642
|
|
|
341
|
-
function emitIndex(
|
|
342
|
-
return `export {
|
|
343
|
-
export type {
|
|
643
|
+
function emitIndex(ingresses: ArcubaseIngress[]): string {
|
|
644
|
+
return `export { createArcubaseClient } from './client.js'
|
|
645
|
+
export type { ArcubaseClientConfig, ArcubaseRow } from './runtime.js'
|
|
344
646
|
export { arcubaseSchema } from './schema.js'
|
|
345
|
-
|
|
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')}
|
|
346
652
|
`
|
|
347
653
|
}
|
|
348
654
|
|
|
349
655
|
export function generateArcubaseProjectSDK(payload: unknown): GeneratedSDK {
|
|
350
|
-
const
|
|
656
|
+
const ingresses = normalizeIngresses(payload)
|
|
351
657
|
return {
|
|
352
658
|
files: [
|
|
353
659
|
{ path: 'runtime.ts', contents: emitRuntime() },
|
|
354
|
-
{ path: 'schema.ts', contents: emitSchema(
|
|
355
|
-
...
|
|
356
|
-
{ path: 'client.ts', contents: emitClient(
|
|
357
|
-
{ path: 'index.ts', contents: emitIndex(
|
|
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) },
|
|
358
664
|
],
|
|
359
665
|
}
|
|
360
666
|
}
|