@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
|
@@ -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 '
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
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 (
|
|
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
|
|
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:
|
|
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
|
-
|
|
169
|
-
|
|
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(
|
|
277
|
+
function emitSchema(ingresses) {
|
|
175
278
|
const schema = {
|
|
176
|
-
|
|
177
|
-
|
|
279
|
+
ingresses: Object.fromEntries(ingresses.map((ingress) => [
|
|
280
|
+
ingress.key,
|
|
178
281
|
{
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
|
301
|
+
export type ArcubaseIngressKey = keyof typeof arcubaseSchema.ingresses
|
|
196
302
|
`;
|
|
197
303
|
}
|
|
198
|
-
function
|
|
199
|
-
const typeName = toPascalCase(
|
|
200
|
-
const
|
|
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 =
|
|
205
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
482
|
+
export type ${typeName}Sort = {
|
|
483
|
+
column: ${typeName}FieldKey | 'id' | 'created_at' | 'updated_at' | 'creator' | string
|
|
484
|
+
order: 'asc' | 'desc'
|
|
485
|
+
}
|
|
218
486
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
531
|
+
function toArcubaseSelection(selection: ${typeName}Selection): Record<string, any> {
|
|
253
532
|
return {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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(
|
|
285
|
-
const imports =
|
|
286
|
-
const typeName = toPascalCase(
|
|
287
|
-
return `import { create${typeName}
|
|
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
|
|
290
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
302
|
-
return `export {
|
|
303
|
-
export type {
|
|
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
|
-
|
|
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
|
|
588
|
+
const ingresses = normalizeIngresses(payload);
|
|
310
589
|
return {
|
|
311
590
|
files: [
|
|
312
591
|
{ path: 'runtime.ts', contents: emitRuntime() },
|
|
313
|
-
{ path: 'schema.ts', contents: emitSchema(
|
|
314
|
-
...
|
|
315
|
-
{ path: 'client.ts', contents: emitClient(
|
|
316
|
-
{ path: 'index.ts', contents: emitIndex(
|
|
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
|
}
|