@svadmin/core 0.42.1 → 0.46.0
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/package.json +19 -7
- package/src/audit.ts +42 -1
- package/src/chatProvider.svelte.ts +30 -0
- package/src/helpers-pure.ts +194 -0
- package/src/helpers.ts +3 -0
- package/src/i18n.svelte.ts +22 -0
- package/src/index.ts +20 -4
- package/src/inferencer-graphql.ts +495 -0
- package/src/inferencer.ts +225 -4
- package/src/types.ts +9 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.46.0",
|
|
4
4
|
"author": "zuohuadong",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -9,20 +9,20 @@
|
|
|
9
9
|
},
|
|
10
10
|
"main": "src/index.ts",
|
|
11
11
|
"devDependencies": {
|
|
12
|
-
"@happy-dom/global-registrator": "^20.11.
|
|
12
|
+
"@happy-dom/global-registrator": "^20.11.12",
|
|
13
13
|
"@sveltejs/vite-plugin-svelte": "^7.3.0",
|
|
14
14
|
"@testing-library/dom": "^10.4.1",
|
|
15
15
|
"@testing-library/svelte": "^5.4.2",
|
|
16
|
-
"@testing-library/user-event": "^14.6.
|
|
16
|
+
"@testing-library/user-event": "^14.6.6",
|
|
17
17
|
"@types/bun": "^1.4.0",
|
|
18
18
|
"@types/jsdom": "^30.0.0",
|
|
19
|
-
"happy-dom": "^20.11.
|
|
19
|
+
"happy-dom": "^20.11.12",
|
|
20
20
|
"jsdom": "^30.0.1",
|
|
21
|
-
"vitest": "^4.1.
|
|
21
|
+
"vitest": "^4.1.11"
|
|
22
22
|
},
|
|
23
23
|
"peerDependencies": {
|
|
24
|
-
"svelte": "^5.56.
|
|
25
|
-
"@tanstack/svelte-query": "^6.1.
|
|
24
|
+
"svelte": "^5.56.10",
|
|
25
|
+
"@tanstack/svelte-query": "^6.1.48"
|
|
26
26
|
},
|
|
27
27
|
"exports": {
|
|
28
28
|
".": {
|
|
@@ -56,6 +56,18 @@
|
|
|
56
56
|
"./theme": {
|
|
57
57
|
"types": "./src/theme.svelte.ts",
|
|
58
58
|
"default": "./src/theme.svelte.ts"
|
|
59
|
+
},
|
|
60
|
+
"./inferencer": {
|
|
61
|
+
"types": "./src/inferencer.ts",
|
|
62
|
+
"default": "./src/inferencer.ts"
|
|
63
|
+
},
|
|
64
|
+
"./inferencer-openapi": {
|
|
65
|
+
"types": "./src/inferencer-openapi.ts",
|
|
66
|
+
"default": "./src/inferencer-openapi.ts"
|
|
67
|
+
},
|
|
68
|
+
"./inferencer-graphql": {
|
|
69
|
+
"types": "./src/inferencer-graphql.ts",
|
|
70
|
+
"default": "./src/inferencer-graphql.ts"
|
|
59
71
|
}
|
|
60
72
|
},
|
|
61
73
|
"bugs": {
|
package/src/audit.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
// Audit logging — record admin operations
|
|
2
2
|
// Full AuditLogProvider interface
|
|
3
3
|
|
|
4
|
+
export type AuditAction = 'create' | 'update' | 'delete' | 'login' | 'logout' | 'rollback' | 'undo' | (string & {});
|
|
5
|
+
|
|
4
6
|
export interface AuditEntry {
|
|
5
7
|
id?: string | number;
|
|
6
8
|
timestamp: string;
|
|
7
|
-
action:
|
|
9
|
+
action: AuditAction;
|
|
10
|
+
mutationId?: string;
|
|
8
11
|
resource?: string;
|
|
9
12
|
recordId?: string | number;
|
|
10
13
|
userId?: string;
|
|
@@ -136,3 +139,41 @@ export async function writeAuditEntry(
|
|
|
136
139
|
},
|
|
137
140
|
});
|
|
138
141
|
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 记录乐观变更回滚或撤回事件至审计流。
|
|
145
|
+
* 将前序状态快照与回滚原因作为不可变事实写入审计。
|
|
146
|
+
*/
|
|
147
|
+
export async function recordMutationRollback(
|
|
148
|
+
params: {
|
|
149
|
+
resource: string;
|
|
150
|
+
recordId?: string | number;
|
|
151
|
+
mutationId?: string;
|
|
152
|
+
previousData?: Record<string, unknown>;
|
|
153
|
+
currentData?: Record<string, unknown>;
|
|
154
|
+
reason?: string;
|
|
155
|
+
userId?: string;
|
|
156
|
+
tenantId?: string | number;
|
|
157
|
+
requestId?: string;
|
|
158
|
+
traceId?: string;
|
|
159
|
+
},
|
|
160
|
+
provider: AuditLogProvider | null | undefined = auditLogProvider,
|
|
161
|
+
): Promise<AuditEntry> {
|
|
162
|
+
return writeAuditEntry({
|
|
163
|
+
action: 'rollback',
|
|
164
|
+
resource: params.resource,
|
|
165
|
+
recordId: params.recordId,
|
|
166
|
+
userId: params.userId,
|
|
167
|
+
tenantId: params.tenantId,
|
|
168
|
+
requestId: params.requestId,
|
|
169
|
+
traceId: params.traceId,
|
|
170
|
+
outcome: 'success',
|
|
171
|
+
previousData: params.previousData,
|
|
172
|
+
data: params.currentData,
|
|
173
|
+
meta: {
|
|
174
|
+
actionType: 'mutation_rollback',
|
|
175
|
+
mutationId: params.mutationId,
|
|
176
|
+
reason: params.reason,
|
|
177
|
+
},
|
|
178
|
+
}, provider);
|
|
179
|
+
}
|
|
@@ -92,6 +92,12 @@ export interface AdminTool {
|
|
|
92
92
|
* The agent emits an `approval_request` event and waits for `approveToolCall()`.
|
|
93
93
|
*/
|
|
94
94
|
needsApproval?: boolean;
|
|
95
|
+
/** 显式声明是否为只读操作(只读操作可并发执行,无副作用) */
|
|
96
|
+
readOnly?: boolean;
|
|
97
|
+
/** 显式声明是否为破坏性/高风险操作(默认触发 Approval Gate) */
|
|
98
|
+
destructive?: boolean;
|
|
99
|
+
/** 显式声明是否支持并发调用 */
|
|
100
|
+
concurrent?: boolean;
|
|
95
101
|
/** Execute the tool with the given arguments. */
|
|
96
102
|
execute(args: Record<string, unknown>): Promise<ToolResult>;
|
|
97
103
|
}
|
|
@@ -224,6 +230,30 @@ export function getChatContext(): ChatContext {
|
|
|
224
230
|
return chatContext;
|
|
225
231
|
}
|
|
226
232
|
|
|
233
|
+
/**
|
|
234
|
+
* 将 AdminTool 投影为发给 LLM / MCP 客户端的白名单 Schema。
|
|
235
|
+
* 剥离底层执行逻辑(execute)与宿主私有状态,保留 name、description、parameters 及并发/安全声明。
|
|
236
|
+
*/
|
|
237
|
+
export function projectAdminToolSchema(tool: AdminTool): {
|
|
238
|
+
name: string;
|
|
239
|
+
description: string;
|
|
240
|
+
parameters: Record<string, AdminToolParameter>;
|
|
241
|
+
readOnly?: boolean;
|
|
242
|
+
destructive?: boolean;
|
|
243
|
+
concurrent?: boolean;
|
|
244
|
+
needsApproval?: boolean;
|
|
245
|
+
} {
|
|
246
|
+
return {
|
|
247
|
+
name: tool.name,
|
|
248
|
+
description: tool.description,
|
|
249
|
+
parameters: { ...tool.parameters },
|
|
250
|
+
...(tool.readOnly !== undefined ? { readOnly: tool.readOnly } : {}),
|
|
251
|
+
...(tool.destructive !== undefined ? { destructive: tool.destructive } : {}),
|
|
252
|
+
...(tool.concurrent !== undefined ? { concurrent: tool.concurrent } : {}),
|
|
253
|
+
...(tool.needsApproval !== undefined ? { needsApproval: tool.needsApproval } : {}),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
227
257
|
export function resetChatProvider(): void {
|
|
228
258
|
chatProvider = null;
|
|
229
259
|
agentProvider = null;
|
package/src/helpers-pure.ts
CHANGED
|
@@ -164,3 +164,197 @@ export function parseCSV(text: string): string[][] {
|
|
|
164
164
|
}
|
|
165
165
|
return rows;
|
|
166
166
|
}
|
|
167
|
+
|
|
168
|
+
// ─── Universal Schema Validator Helper ──────────────────────────
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Universal schema interface compatible with TypeBox, Standard Schema (v1), Valibot, Zod,
|
|
172
|
+
* Yup, Joi, etc.
|
|
173
|
+
*/
|
|
174
|
+
export interface SchemaValidatorLike {
|
|
175
|
+
Check?: (value: unknown) => boolean;
|
|
176
|
+
Errors?: (value: unknown) => Iterable<{ path?: string; message: string; schema?: unknown }>;
|
|
177
|
+
safeParse?: (data: unknown) => {
|
|
178
|
+
success: boolean;
|
|
179
|
+
data?: unknown;
|
|
180
|
+
error?: {
|
|
181
|
+
issues?: Array<{ path?: Array<string | number>; message: string }>;
|
|
182
|
+
errors?: Array<{ path?: Array<string | number>; message: string }>;
|
|
183
|
+
};
|
|
184
|
+
};
|
|
185
|
+
parse?: (data: unknown) => unknown;
|
|
186
|
+
validate?: (data: unknown) => {
|
|
187
|
+
error?: {
|
|
188
|
+
details?: Array<{ path?: Array<string | number>; message: string }>;
|
|
189
|
+
message?: string;
|
|
190
|
+
};
|
|
191
|
+
};
|
|
192
|
+
'~standard'?: {
|
|
193
|
+
validate: (data: unknown) => unknown | Promise<unknown>;
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* TypeBox validator interface matching TypeCompiler.Compile or Value.Errors
|
|
199
|
+
*/
|
|
200
|
+
export type TypeBoxValidatorLike = {
|
|
201
|
+
Check?: (value: unknown) => boolean;
|
|
202
|
+
Errors: (value: unknown) => Iterable<{ path?: string; message: string }>;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Creates a synchronous form validator function from any TypeBox, Standard Schema v1,
|
|
207
|
+
* Valibot, Zod, Yup, or Joi object.
|
|
208
|
+
*
|
|
209
|
+
* @example
|
|
210
|
+
* ```ts
|
|
211
|
+
* import { Type } from '@sinclair/typebox';
|
|
212
|
+
* import { TypeCompiler } from '@sinclair/typebox/compiler';
|
|
213
|
+
* import { createSchemaValidator, useForm } from '@svadmin/core';
|
|
214
|
+
*
|
|
215
|
+
* const userSchema = Type.Object({
|
|
216
|
+
* name: Type.String({ minLength: 2 }),
|
|
217
|
+
* email: Type.String({ format: 'email' }),
|
|
218
|
+
* });
|
|
219
|
+
*
|
|
220
|
+
* const form = useForm({
|
|
221
|
+
* resource: 'users',
|
|
222
|
+
* validate: createSchemaValidator(TypeCompiler.Compile(userSchema)),
|
|
223
|
+
* });
|
|
224
|
+
* ```
|
|
225
|
+
*/
|
|
226
|
+
export function createSchemaValidator(
|
|
227
|
+
schema: SchemaValidatorLike | unknown
|
|
228
|
+
): (values: Record<string, unknown>) => Record<string, string> | null {
|
|
229
|
+
if (!schema || typeof schema !== 'object') {
|
|
230
|
+
return () => null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const s = schema as SchemaValidatorLike;
|
|
234
|
+
|
|
235
|
+
return (values: Record<string, unknown>) => {
|
|
236
|
+
const errors: Record<string, string> = {};
|
|
237
|
+
|
|
238
|
+
// 0. TypeBox TypeCompiler compiled validator (.Check & .Errors)
|
|
239
|
+
if (typeof s.Check === 'function' && typeof s.Errors === 'function') {
|
|
240
|
+
if (s.Check(values)) return null;
|
|
241
|
+
for (const err of s.Errors(values)) {
|
|
242
|
+
const rawPath = err.path || '';
|
|
243
|
+
const cleanPath = rawPath.replace(/^\//, '').replace(/\//g, '.');
|
|
244
|
+
const key = cleanPath || '_root';
|
|
245
|
+
if (!errors[key]) errors[key] = err.message;
|
|
246
|
+
}
|
|
247
|
+
return Object.keys(errors).length > 0 ? errors : null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 0b. TypeBox Value.Errors iterator provider
|
|
251
|
+
if (typeof s.Errors === 'function') {
|
|
252
|
+
for (const err of s.Errors(values)) {
|
|
253
|
+
const rawPath = err.path || '';
|
|
254
|
+
const cleanPath = rawPath.replace(/^\//, '').replace(/\//g, '.');
|
|
255
|
+
const key = cleanPath || '_root';
|
|
256
|
+
if (!errors[key]) errors[key] = err.message;
|
|
257
|
+
}
|
|
258
|
+
return Object.keys(errors).length > 0 ? errors : null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// 1. Standard Schema v1 (~standard)
|
|
262
|
+
if (s['~standard'] && typeof s['~standard'].validate === 'function') {
|
|
263
|
+
const res = s['~standard'].validate(values);
|
|
264
|
+
if (res && typeof res === 'object' && 'issues' in res && Array.isArray((res as { issues?: Array<{ path?: Array<unknown>; message: string }> }).issues)) {
|
|
265
|
+
for (const issue of (res as { issues: Array<{ path?: Array<unknown>; message: string }> }).issues) {
|
|
266
|
+
const rawPath = issue.path?.[0];
|
|
267
|
+
const key = typeof rawPath === 'object' && rawPath !== null && 'key' in rawPath
|
|
268
|
+
? String((rawPath as { key: unknown }).key)
|
|
269
|
+
: rawPath != null
|
|
270
|
+
? String(rawPath)
|
|
271
|
+
: '_root';
|
|
272
|
+
if (!errors[key]) errors[key] = issue.message;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return Object.keys(errors).length > 0 ? errors : null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// 2. Zod / Valibot safeParse
|
|
279
|
+
if (typeof s.safeParse === 'function') {
|
|
280
|
+
const res = s.safeParse(values);
|
|
281
|
+
if (!res.success && res.error) {
|
|
282
|
+
const issues = res.error.issues ?? res.error.errors ?? [];
|
|
283
|
+
for (const issue of issues) {
|
|
284
|
+
const key = issue.path?.[0] != null ? String(issue.path[0]) : '_root';
|
|
285
|
+
if (!errors[key]) errors[key] = issue.message;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return Object.keys(errors).length > 0 ? errors : null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// 3. Joi / generic .validate()
|
|
292
|
+
if (typeof s.validate === 'function') {
|
|
293
|
+
const res = s.validate(values);
|
|
294
|
+
if (res && typeof res === 'object' && res.error) {
|
|
295
|
+
const details = res.error.details;
|
|
296
|
+
if (Array.isArray(details) && details.length > 0) {
|
|
297
|
+
for (const d of details) {
|
|
298
|
+
const key = d.path?.[0] != null ? String(d.path[0]) : '_root';
|
|
299
|
+
if (!errors[key]) errors[key] = d.message;
|
|
300
|
+
}
|
|
301
|
+
} else if (res.error.message) {
|
|
302
|
+
errors._root = res.error.message;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return Object.keys(errors).length > 0 ? errors : null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// 4. Synchronous .parse() that throws (e.g. standard Zod parse or Yup validateSync)
|
|
309
|
+
if (typeof s.parse === 'function') {
|
|
310
|
+
try {
|
|
311
|
+
s.parse(values);
|
|
312
|
+
} catch (err: unknown) {
|
|
313
|
+
if (err && typeof err === 'object') {
|
|
314
|
+
const issues = (err as { issues?: Array<{ path?: Array<string | number>; message: string }>; errors?: Array<{ path?: Array<string | number>; message: string }> }).issues ??
|
|
315
|
+
(err as { errors?: Array<{ path?: Array<string | number>; message: string }> }).errors;
|
|
316
|
+
if (Array.isArray(issues)) {
|
|
317
|
+
for (const issue of issues) {
|
|
318
|
+
const key = issue.path?.[0] != null ? String(issue.path[0]) : '_root';
|
|
319
|
+
if (!errors[key]) errors[key] = issue.message;
|
|
320
|
+
}
|
|
321
|
+
} else if ('message' in err && typeof (err as { message: unknown }).message === 'string') {
|
|
322
|
+
errors._root = (err as { message: string }).message;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return Object.keys(errors).length > 0 ? errors : null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return null;
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Creates a synchronous form validator function specifically optimized for TypeBox
|
|
335
|
+
* compiled validators (from TypeCompiler.Compile) or TypeBox value checks.
|
|
336
|
+
*
|
|
337
|
+
* @example
|
|
338
|
+
* ```ts
|
|
339
|
+
* import { Type } from '@sinclair/typebox';
|
|
340
|
+
* import { TypeCompiler } from '@sinclair/typebox/compiler';
|
|
341
|
+
* import { createTypeBoxValidator, useForm } from '@svadmin/core';
|
|
342
|
+
*
|
|
343
|
+
* const userSchema = Type.Object({
|
|
344
|
+
* name: Type.String({ minLength: 2 }),
|
|
345
|
+
* email: Type.String({ format: 'email' }),
|
|
346
|
+
* });
|
|
347
|
+
*
|
|
348
|
+
* const compiledUser = TypeCompiler.Compile(userSchema);
|
|
349
|
+
*
|
|
350
|
+
* const form = useForm({
|
|
351
|
+
* resource: 'users',
|
|
352
|
+
* validate: createTypeBoxValidator(compiledUser),
|
|
353
|
+
* });
|
|
354
|
+
* ```
|
|
355
|
+
*/
|
|
356
|
+
export function createTypeBoxValidator(
|
|
357
|
+
compiledOrSchema: TypeBoxValidatorLike | SchemaValidatorLike | unknown
|
|
358
|
+
): (values: Record<string, unknown>) => Record<string, string> | null {
|
|
359
|
+
return createSchemaValidator(compiledOrSchema);
|
|
360
|
+
}
|
package/src/helpers.ts
CHANGED
|
@@ -8,7 +8,10 @@ export {
|
|
|
8
8
|
getDefaultFilter, getDefaultSortOrder,
|
|
9
9
|
unionFilters, unionSorters,
|
|
10
10
|
file2Base64, parseCSV,
|
|
11
|
+
createSchemaValidator,
|
|
12
|
+
createTypeBoxValidator,
|
|
11
13
|
} from './helpers-pure';
|
|
14
|
+
export type { SchemaValidatorLike, TypeBoxValidatorLike } from './helpers-pure';
|
|
12
15
|
|
|
13
16
|
|
|
14
17
|
// ─── Document Title Helper ────────────────────────────────────
|
package/src/i18n.svelte.ts
CHANGED
|
@@ -14,6 +14,17 @@ const baseLocales: Record<string, Record<string, string>> = {
|
|
|
14
14
|
'common.upload': '上传',
|
|
15
15
|
'common.returnHome': '返回首页',
|
|
16
16
|
// Common
|
|
17
|
+
'common.add': '添加',
|
|
18
|
+
'common.moveUp': '上移',
|
|
19
|
+
'common.moveDown': '下移',
|
|
20
|
+
'common.treeSelect': '树形选择',
|
|
21
|
+
'common.cascader': '级联选择',
|
|
22
|
+
'common.transfer': '穿梭框',
|
|
23
|
+
'common.filterBuilder': '高级筛选',
|
|
24
|
+
'common.addRule': '添加筛选条件',
|
|
25
|
+
'common.sourceList': '源列表',
|
|
26
|
+
'common.targetList': '目标列表',
|
|
27
|
+
'common.selectLeafOnly': '只允许选择叶子节点',
|
|
17
28
|
'common.save': '保存',
|
|
18
29
|
'common.cancel': '取消',
|
|
19
30
|
'common.create': '新建',
|
|
@@ -674,6 +685,17 @@ const baseLocales: Record<string, Record<string, string>> = {
|
|
|
674
685
|
'common.upload': 'Upload',
|
|
675
686
|
'common.returnHome': 'Return to Home',
|
|
676
687
|
// Common
|
|
688
|
+
'common.add': 'Add',
|
|
689
|
+
'common.moveUp': 'Move Up',
|
|
690
|
+
'common.moveDown': 'Move Down',
|
|
691
|
+
'common.treeSelect': 'Tree Select',
|
|
692
|
+
'common.cascader': 'Cascader',
|
|
693
|
+
'common.transfer': 'Transfer',
|
|
694
|
+
'common.filterBuilder': 'Filter Builder',
|
|
695
|
+
'common.addRule': 'Add Filter Rule',
|
|
696
|
+
'common.sourceList': 'Source',
|
|
697
|
+
'common.targetList': 'Target',
|
|
698
|
+
'common.selectLeafOnly': 'Select leaf only',
|
|
677
699
|
'common.save': 'Save',
|
|
678
700
|
'common.cancel': 'Cancel',
|
|
679
701
|
'common.create': 'Create',
|
package/src/index.ts
CHANGED
|
@@ -90,7 +90,7 @@ export {
|
|
|
90
90
|
export type { NotificationParams } from './notification.svelte';
|
|
91
91
|
export { t, setLocale, getLocale, getAvailableLocales, addTranslations, useTranslation, setI18nProvider, getI18nProvider, createI18nScope, provideI18nScope, getI18nScope, resetI18n } from './i18n.svelte';
|
|
92
92
|
export type { I18nProvider, I18nScope, I18nScopeOptions } from './i18n.svelte';
|
|
93
|
-
export { audit, auditWithProvider, writeAuditEntry, setAuditHandler, setAuditLogProvider, getAuditLogProvider } from './audit';
|
|
93
|
+
export { audit, auditWithProvider, writeAuditEntry, recordMutationRollback, setAuditHandler, setAuditLogProvider, getAuditLogProvider } from './audit';
|
|
94
94
|
export type { AuditLogProvider } from './audit';
|
|
95
95
|
export type {
|
|
96
96
|
ApiCredentialSummary,
|
|
@@ -116,7 +116,7 @@ export type {
|
|
|
116
116
|
WebhookSummary,
|
|
117
117
|
} from './enterprise';
|
|
118
118
|
export { assertEnterpriseRequestContext, createEnterpriseRequestContext } from './enterprise';
|
|
119
|
-
export { setChatProvider, getChatProvider, setChatContext, getChatContext, setAgentProvider, getAgentProvider, registerApproval, resolveApproval, hasPendingApprovals, resetChatProvider } from './chatProvider.svelte';
|
|
119
|
+
export { setChatProvider, getChatProvider, setChatContext, getChatContext, setAgentProvider, getAgentProvider, registerApproval, resolveApproval, hasPendingApprovals, resetChatProvider, projectAdminToolSchema } from './chatProvider.svelte';
|
|
120
120
|
export type { ChatProvider, ChatMessage, ChatContext, ChatAction, AgentProvider, AgentEvent, AgentOptions, AdminTool, AdminToolParameter, ToolResult } from './chatProvider.svelte';
|
|
121
121
|
export { getTheme, setTheme, toggleTheme, getResolvedTheme, getColorTheme, setColorTheme, getColorThemes, configureTheme, getThemeConfig, clearCssOverrides, builtinPresets, registerColorPreset, getColorPresets, registerThemeOwner, updateThemeOwner, unregisterThemeOwner, resetTheme } from './theme.svelte';
|
|
122
122
|
export type { ThemeMode, ColorTheme, ThemeStrategy, ThemeConfig, ColorPreset, ThemeOwnerOptions, ThemeOwnerToken } from './theme.svelte';
|
|
@@ -150,7 +150,7 @@ export type {
|
|
|
150
150
|
export type { InvalidateScope } from './options.svelte';
|
|
151
151
|
export type { LiveProvider, LiveEvent, LiveMode } from './live.svelte';
|
|
152
152
|
export type { Action, CanParams, CanResult, AccessControlProvider, FeatureGateConfig, FeatureGateUser } from './permissions.svelte';
|
|
153
|
-
export type { AuditEntry, AuditHandler } from './audit';
|
|
153
|
+
export type { AuditEntry, AuditHandler, AuditAction } from './audit';
|
|
154
154
|
export { useCan } from './useCan';
|
|
155
155
|
export type { UseCanOptions, UseCanResult } from './useCan';
|
|
156
156
|
export { createCaslAccessControl } from './adapters/casl';
|
|
@@ -177,7 +177,18 @@ export { useParsed, resetGlobalPath, syncGlobalPath } from './useParsed.svelte';
|
|
|
177
177
|
export * from './useStepsForm.svelte';
|
|
178
178
|
export { createHashRouterProvider, createHistoryRouterProvider } from './router-provider';
|
|
179
179
|
export type { RouterNavigationResult, RouterProvider } from './router-provider';
|
|
180
|
-
export {
|
|
180
|
+
export {
|
|
181
|
+
inferFieldType,
|
|
182
|
+
inferResource,
|
|
183
|
+
generateTypeBoxSchemaCode,
|
|
184
|
+
generateListPageCode,
|
|
185
|
+
generateCreatePageCode,
|
|
186
|
+
generateEditPageCode,
|
|
187
|
+
generateShowPageCode,
|
|
188
|
+
generateResourceCode,
|
|
189
|
+
generateComponentCode,
|
|
190
|
+
generateResourceBundle,
|
|
191
|
+
} from './inferencer';
|
|
181
192
|
export type { InferResult } from './inferencer';
|
|
182
193
|
export { createWebSocketLiveProvider } from './live-websocket';
|
|
183
194
|
export type { WebSocketLiveProviderOptions } from './live-websocket';
|
|
@@ -185,12 +196,17 @@ export { createSSELiveProvider } from './live-sse';
|
|
|
185
196
|
export type { SSELiveProviderOptions } from './live-sse';
|
|
186
197
|
export { inferFromOpenAPI } from './inferencer-openapi';
|
|
187
198
|
export type { InferFromOpenAPIOptions } from './inferencer-openapi';
|
|
199
|
+
export { inferFromGraphQL, GRAPHQL_INTROSPECTION_QUERY } from './inferencer-graphql';
|
|
200
|
+
export type { InferFromGraphQLOptions, GraphQLIntrospectionSchema, GraphQLTypeDescriptor } from './inferencer-graphql';
|
|
188
201
|
export {
|
|
189
202
|
getDefaultFilter, getDefaultSortOrder,
|
|
190
203
|
unionFilters, unionSorters,
|
|
191
204
|
file2Base64, generateDefaultDocumentTitle,
|
|
192
205
|
deriveValidator,
|
|
206
|
+
createSchemaValidator,
|
|
207
|
+
createTypeBoxValidator,
|
|
193
208
|
} from './helpers';
|
|
209
|
+
export type { SchemaValidatorLike, TypeBoxValidatorLike } from './helpers';
|
|
194
210
|
export { TableState } from './table-state.svelte';
|
|
195
211
|
export type { TableStateOptions } from './table-state.svelte';
|
|
196
212
|
export type { UseInfiniteListOptions } from './hooks.svelte';
|
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphQL Schema / Introspection → ResourceDefinition converter
|
|
3
|
+
*
|
|
4
|
+
* Parses a GraphQL introspection JSON result or schema definition and generates
|
|
5
|
+
* ResourceDefinition[] by analyzing entity types, fields, enums, queries, and mutations.
|
|
6
|
+
*/
|
|
7
|
+
import type { FieldDefinition, ResourceDefinition } from './types';
|
|
8
|
+
|
|
9
|
+
// ─── GraphQL Introspection Types ─────────────────────────────
|
|
10
|
+
|
|
11
|
+
export interface GraphQLIntrospectionSchema {
|
|
12
|
+
__schema?: GraphQLSchemaBody;
|
|
13
|
+
data?: {
|
|
14
|
+
__schema?: GraphQLSchemaBody;
|
|
15
|
+
};
|
|
16
|
+
types?: GraphQLTypeDescriptor[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface GraphQLSchemaBody {
|
|
20
|
+
queryType?: { name?: string | null } | null;
|
|
21
|
+
mutationType?: { name?: string | null } | null;
|
|
22
|
+
subscriptionType?: { name?: string | null } | null;
|
|
23
|
+
types: GraphQLTypeDescriptor[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface GraphQLTypeDescriptor {
|
|
27
|
+
kind: 'SCALAR' | 'OBJECT' | 'INTERFACE' | 'UNION' | 'ENUM' | 'INPUT_OBJECT' | 'LIST' | 'NON_NULL';
|
|
28
|
+
name: string | null;
|
|
29
|
+
description?: string | null;
|
|
30
|
+
fields?: GraphQLFieldDescriptor[] | null;
|
|
31
|
+
inputFields?: GraphQLInputValueDescriptor[] | null;
|
|
32
|
+
interfaces?: GraphQLTypeRef[] | null;
|
|
33
|
+
enumValues?: Array<{ name: string; description?: string | null }> | null;
|
|
34
|
+
possibleTypes?: GraphQLTypeRef[] | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface GraphQLFieldDescriptor {
|
|
38
|
+
name: string;
|
|
39
|
+
description?: string | null;
|
|
40
|
+
args?: GraphQLInputValueDescriptor[];
|
|
41
|
+
type: GraphQLTypeRef;
|
|
42
|
+
isDeprecated?: boolean;
|
|
43
|
+
deprecationReason?: string | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface GraphQLInputValueDescriptor {
|
|
47
|
+
name: string;
|
|
48
|
+
description?: string | null;
|
|
49
|
+
type: GraphQLTypeRef;
|
|
50
|
+
defaultValue?: string | null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface GraphQLTypeRef {
|
|
54
|
+
kind: 'SCALAR' | 'OBJECT' | 'INTERFACE' | 'UNION' | 'ENUM' | 'INPUT_OBJECT' | 'LIST' | 'NON_NULL';
|
|
55
|
+
name?: string | null;
|
|
56
|
+
ofType?: GraphQLTypeRef | null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Standard GraphQL introspection query string to fetch the schema from any GraphQL endpoint.
|
|
61
|
+
*/
|
|
62
|
+
export const GRAPHQL_INTROSPECTION_QUERY = `query IntrospectionQuery {
|
|
63
|
+
__schema {
|
|
64
|
+
queryType { name }
|
|
65
|
+
mutationType { name }
|
|
66
|
+
subscriptionType { name }
|
|
67
|
+
types {
|
|
68
|
+
kind
|
|
69
|
+
name
|
|
70
|
+
description
|
|
71
|
+
fields(includeDeprecated: true) {
|
|
72
|
+
name
|
|
73
|
+
description
|
|
74
|
+
type {
|
|
75
|
+
kind
|
|
76
|
+
name
|
|
77
|
+
ofType {
|
|
78
|
+
kind
|
|
79
|
+
name
|
|
80
|
+
ofType {
|
|
81
|
+
kind
|
|
82
|
+
name
|
|
83
|
+
ofType {
|
|
84
|
+
kind
|
|
85
|
+
name
|
|
86
|
+
ofType {
|
|
87
|
+
kind
|
|
88
|
+
name
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
enumValues(includeDeprecated: true) {
|
|
96
|
+
name
|
|
97
|
+
description
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}`;
|
|
102
|
+
|
|
103
|
+
// ─── Core Type Resolution ────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
interface ResolvedTypeInfo {
|
|
106
|
+
baseName: string;
|
|
107
|
+
kind: string;
|
|
108
|
+
isList: boolean;
|
|
109
|
+
isRequired: boolean;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function unwrapType(typeRef: GraphQLTypeRef): ResolvedTypeInfo {
|
|
113
|
+
let current: GraphQLTypeRef = typeRef;
|
|
114
|
+
let isRequired = false;
|
|
115
|
+
let isList = false;
|
|
116
|
+
|
|
117
|
+
if (current.kind === 'NON_NULL') {
|
|
118
|
+
isRequired = true;
|
|
119
|
+
if (current.ofType) current = current.ofType;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (current.kind === 'LIST') {
|
|
123
|
+
isList = true;
|
|
124
|
+
if (current.ofType) {
|
|
125
|
+
current = current.ofType;
|
|
126
|
+
if (current.kind === 'NON_NULL') {
|
|
127
|
+
if (current.ofType) current = current.ofType;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
baseName: current.name ?? 'String',
|
|
134
|
+
kind: current.kind,
|
|
135
|
+
isList,
|
|
136
|
+
isRequired,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function mapGraphQLTypeToFieldType(
|
|
141
|
+
key: string,
|
|
142
|
+
typeInfo: ResolvedTypeInfo,
|
|
143
|
+
enumMap: Map<string, string[]>
|
|
144
|
+
): FieldDefinition['type'] {
|
|
145
|
+
const { baseName, isList } = typeInfo;
|
|
146
|
+
const lk = key.toLowerCase();
|
|
147
|
+
|
|
148
|
+
// Enums
|
|
149
|
+
if (enumMap.has(baseName)) return 'select';
|
|
150
|
+
|
|
151
|
+
// Arrays / Lists
|
|
152
|
+
if (isList) {
|
|
153
|
+
if (lk.includes('image') || lk.includes('photo') || lk.includes('avatar') || lk.includes('picture')) {
|
|
154
|
+
return 'images';
|
|
155
|
+
}
|
|
156
|
+
return 'tags';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Built-in Scalars & Custom Scalars
|
|
160
|
+
switch (baseName) {
|
|
161
|
+
case 'Int':
|
|
162
|
+
case 'Float':
|
|
163
|
+
case 'Decimal':
|
|
164
|
+
case 'BigInt':
|
|
165
|
+
return 'number';
|
|
166
|
+
case 'Boolean':
|
|
167
|
+
return 'boolean';
|
|
168
|
+
case 'Date':
|
|
169
|
+
case 'DateTime':
|
|
170
|
+
case 'Time':
|
|
171
|
+
case 'Timestamp':
|
|
172
|
+
return 'date';
|
|
173
|
+
case 'JSON':
|
|
174
|
+
case 'JsonObject':
|
|
175
|
+
case 'JSONObject':
|
|
176
|
+
case 'Json':
|
|
177
|
+
return 'json';
|
|
178
|
+
case 'ID':
|
|
179
|
+
return 'text';
|
|
180
|
+
case 'String':
|
|
181
|
+
default:
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// String field heuristics based on key name
|
|
186
|
+
if (lk.includes('email')) return 'email';
|
|
187
|
+
if (lk.includes('phone') || lk.includes('tel') || lk.includes('mobile')) return 'phone';
|
|
188
|
+
if (lk.includes('url') || lk.includes('link') || lk.includes('website')) return 'url';
|
|
189
|
+
if (lk.includes('avatar') || lk.includes('image') || lk.includes('photo') || lk.includes('thumbnail') || lk.includes('logo')) return 'image';
|
|
190
|
+
if (lk.includes('color') || lk.includes('colour')) return 'color';
|
|
191
|
+
if (lk.includes('description') || lk.includes('content') || lk.includes('body') || lk.includes('bio') || lk.includes('summary')) return 'textarea';
|
|
192
|
+
if (lk === 'created_at' || lk === 'updated_at' || lk.endsWith('_at') || lk.endsWith('_date') || /\bdate\b/.test(lk)) return 'date';
|
|
193
|
+
|
|
194
|
+
return 'text';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ─── GraphQL SDL Parser (Fallback for raw .graphql schemas) ──
|
|
198
|
+
|
|
199
|
+
function parseGraphQLSDL(sdl: string): GraphQLSchemaBody {
|
|
200
|
+
const types: GraphQLTypeDescriptor[] = [];
|
|
201
|
+
const queryType: { name: string } = { name: 'Query' };
|
|
202
|
+
const mutationType: { name: string } = { name: 'Mutation' };
|
|
203
|
+
|
|
204
|
+
// Remove comments
|
|
205
|
+
const cleanSDL = sdl.replace(/#[^\n\r]*/g, '');
|
|
206
|
+
|
|
207
|
+
// Extract enums: enum Status { DRAFT PUBLISHED ARCHIVED }
|
|
208
|
+
const enumRegex = /enum\s+([A-Za-z0-9_]+)\s*\{([^}]+)\}/g;
|
|
209
|
+
let match: RegExpExecArray | null;
|
|
210
|
+
while ((match = enumRegex.exec(cleanSDL)) !== null) {
|
|
211
|
+
const enumName = match[1];
|
|
212
|
+
const rawValues = match[2].split(/\s+/).filter(Boolean);
|
|
213
|
+
types.push({
|
|
214
|
+
kind: 'ENUM',
|
|
215
|
+
name: enumName,
|
|
216
|
+
enumValues: rawValues.map(v => ({ name: v })),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Extract types: type Post { id: ID! title: String ... }
|
|
221
|
+
const typeRegex = /type\s+([A-Za-z0-9_]+)\s*(?:implements\s+[A-Za-z0-9_&,\s]+)?\s*\{([^}]+)\}/g;
|
|
222
|
+
while ((match = typeRegex.exec(cleanSDL)) !== null) {
|
|
223
|
+
const typeName = match[1];
|
|
224
|
+
const fieldsBody = match[2];
|
|
225
|
+
const fieldLines = fieldsBody.split('\n').map(l => l.trim()).filter(Boolean);
|
|
226
|
+
const fields: GraphQLFieldDescriptor[] = [];
|
|
227
|
+
|
|
228
|
+
for (const line of fieldLines) {
|
|
229
|
+
// fieldName(args): TypeName!
|
|
230
|
+
const fieldMatch = /^([A-Za-z0-9_]+)(?:\([^)]*\))?\s*:\s*([[\]A-Za-z0-9_!]+)/.exec(line);
|
|
231
|
+
if (!fieldMatch) continue;
|
|
232
|
+
|
|
233
|
+
const fName = fieldMatch[1];
|
|
234
|
+
const rawType = fieldMatch[2];
|
|
235
|
+
|
|
236
|
+
const isNonNullable = rawType.endsWith('!');
|
|
237
|
+
const cleanType = isNonNullable ? rawType.slice(0, -1) : rawType;
|
|
238
|
+
const isList = cleanType.startsWith('[') && cleanType.endsWith(']');
|
|
239
|
+
const innerType = isList ? cleanType.slice(1, -1).replace(/!$/, '') : cleanType;
|
|
240
|
+
|
|
241
|
+
let typeRef: GraphQLTypeRef = {
|
|
242
|
+
kind: 'SCALAR',
|
|
243
|
+
name: innerType,
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
if (isList) {
|
|
247
|
+
typeRef = {
|
|
248
|
+
kind: 'LIST',
|
|
249
|
+
ofType: typeRef,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (isNonNullable) {
|
|
254
|
+
typeRef = {
|
|
255
|
+
kind: 'NON_NULL',
|
|
256
|
+
ofType: typeRef,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
fields.push({
|
|
261
|
+
name: fName,
|
|
262
|
+
type: typeRef,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
types.push({
|
|
267
|
+
kind: 'OBJECT',
|
|
268
|
+
name: typeName,
|
|
269
|
+
fields,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
queryType,
|
|
275
|
+
mutationType,
|
|
276
|
+
types,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ─── Core: inferFromGraphQL ──────────────────────────────────
|
|
281
|
+
|
|
282
|
+
export interface InferFromGraphQLOptions {
|
|
283
|
+
/** Include only specific type names */
|
|
284
|
+
include?: string[];
|
|
285
|
+
/** Exclude specific type names */
|
|
286
|
+
exclude?: string[];
|
|
287
|
+
/** Custom primary key (default: 'id') */
|
|
288
|
+
primaryKey?: string;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Parses a GraphQL schema (introspection object or SDL string) and returns ResourceDefinition[].
|
|
293
|
+
*/
|
|
294
|
+
export function inferFromGraphQL(
|
|
295
|
+
schemaOrIntrospection: GraphQLIntrospectionSchema | GraphQLSchemaBody | string | unknown,
|
|
296
|
+
options: InferFromGraphQLOptions = {}
|
|
297
|
+
): ResourceDefinition[] {
|
|
298
|
+
const { primaryKey = 'id', include, exclude } = options;
|
|
299
|
+
|
|
300
|
+
let schemaBody: GraphQLSchemaBody;
|
|
301
|
+
|
|
302
|
+
if (typeof schemaOrIntrospection === 'string') {
|
|
303
|
+
schemaBody = parseGraphQLSDL(schemaOrIntrospection);
|
|
304
|
+
} else if (
|
|
305
|
+
schemaOrIntrospection &&
|
|
306
|
+
typeof schemaOrIntrospection === 'object' &&
|
|
307
|
+
'data' in schemaOrIntrospection &&
|
|
308
|
+
(schemaOrIntrospection as { data?: { __schema?: GraphQLSchemaBody } }).data?.__schema
|
|
309
|
+
) {
|
|
310
|
+
schemaBody = (schemaOrIntrospection as { data: { __schema: GraphQLSchemaBody } }).data.__schema;
|
|
311
|
+
} else if (
|
|
312
|
+
schemaOrIntrospection &&
|
|
313
|
+
typeof schemaOrIntrospection === 'object' &&
|
|
314
|
+
'__schema' in schemaOrIntrospection &&
|
|
315
|
+
(schemaOrIntrospection as { __schema?: GraphQLSchemaBody }).__schema
|
|
316
|
+
) {
|
|
317
|
+
schemaBody = (schemaOrIntrospection as { __schema: GraphQLSchemaBody }).__schema;
|
|
318
|
+
} else if (
|
|
319
|
+
schemaOrIntrospection &&
|
|
320
|
+
typeof schemaOrIntrospection === 'object' &&
|
|
321
|
+
'types' in schemaOrIntrospection &&
|
|
322
|
+
Array.isArray((schemaOrIntrospection as GraphQLSchemaBody).types)
|
|
323
|
+
) {
|
|
324
|
+
schemaBody = schemaOrIntrospection as GraphQLSchemaBody;
|
|
325
|
+
} else {
|
|
326
|
+
return [];
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const types = schemaBody.types ?? [];
|
|
330
|
+
const queryTypeName = schemaBody.queryType?.name ?? 'Query';
|
|
331
|
+
const mutationTypeName = schemaBody.mutationType?.name ?? 'Mutation';
|
|
332
|
+
const subscriptionTypeName = schemaBody.subscriptionType?.name ?? 'Subscription';
|
|
333
|
+
|
|
334
|
+
// Build enum map
|
|
335
|
+
const enumMap = new Map<string, string[]>();
|
|
336
|
+
for (const t of types) {
|
|
337
|
+
if (t.kind === 'ENUM' && t.name) {
|
|
338
|
+
const values = (t.enumValues ?? []).map(ev => ev.name);
|
|
339
|
+
enumMap.set(t.name, values);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Build object types map
|
|
344
|
+
const objectTypeMap = new Map<string, GraphQLTypeDescriptor>();
|
|
345
|
+
for (const t of types) {
|
|
346
|
+
if (t.kind === 'OBJECT' && t.name) {
|
|
347
|
+
objectTypeMap.set(t.name, t);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Find root query & mutation fields to detect capabilities
|
|
352
|
+
const queryFields = new Set<string>();
|
|
353
|
+
const queryType = objectTypeMap.get(queryTypeName);
|
|
354
|
+
if (queryType?.fields) {
|
|
355
|
+
for (const f of queryType.fields) queryFields.add(f.name.toLowerCase());
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const mutationFields = new Set<string>();
|
|
359
|
+
const mutationType = objectTypeMap.get(mutationTypeName);
|
|
360
|
+
if (mutationType?.fields) {
|
|
361
|
+
for (const f of mutationType.fields) mutationFields.add(f.name.toLowerCase());
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const resources: ResourceDefinition[] = [];
|
|
365
|
+
|
|
366
|
+
for (const typeObj of types) {
|
|
367
|
+
const typeName = typeObj.name;
|
|
368
|
+
if (!typeName) continue;
|
|
369
|
+
|
|
370
|
+
// Filter out root types, internal types (__*), and input objects
|
|
371
|
+
if (
|
|
372
|
+
typeName.startsWith('__') ||
|
|
373
|
+
typeName === queryTypeName ||
|
|
374
|
+
typeName === mutationTypeName ||
|
|
375
|
+
typeName === subscriptionTypeName ||
|
|
376
|
+
typeObj.kind !== 'OBJECT' ||
|
|
377
|
+
!typeObj.fields ||
|
|
378
|
+
typeObj.fields.length === 0
|
|
379
|
+
) {
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Apply include/exclude filters
|
|
384
|
+
if (include && !include.includes(typeName)) continue;
|
|
385
|
+
if (exclude && exclude.includes(typeName)) continue;
|
|
386
|
+
|
|
387
|
+
const baseName = typeName.toLowerCase();
|
|
388
|
+
const resourceName = baseName.endsWith('s') ? baseName : baseName + 's';
|
|
389
|
+
|
|
390
|
+
const fields: FieldDefinition[] = [];
|
|
391
|
+
|
|
392
|
+
for (const fieldDesc of typeObj.fields) {
|
|
393
|
+
const fieldName = fieldDesc.name;
|
|
394
|
+
const typeInfo = unwrapType(fieldDesc.type);
|
|
395
|
+
|
|
396
|
+
let fieldType = mapGraphQLTypeToFieldType(fieldName, typeInfo, enumMap);
|
|
397
|
+
let relatedResource: string | null = null;
|
|
398
|
+
|
|
399
|
+
// Check if type references another Object entity in the schema
|
|
400
|
+
if (objectTypeMap.has(typeInfo.baseName) && typeInfo.baseName !== typeName) {
|
|
401
|
+
const target = typeInfo.baseName.toLowerCase();
|
|
402
|
+
relatedResource = target.endsWith('s') ? target : target + 's';
|
|
403
|
+
fieldType = 'relation';
|
|
404
|
+
} else if (fieldName.endsWith('_id') || fieldName.endsWith('Id')) {
|
|
405
|
+
const base = fieldName.replace(/_id$|Id$/, '');
|
|
406
|
+
relatedResource = base.endsWith('s') ? base : base + 's';
|
|
407
|
+
fieldType = 'relation';
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const field: FieldDefinition = {
|
|
411
|
+
key: fieldName,
|
|
412
|
+
label: humanize(fieldName),
|
|
413
|
+
type: relatedResource ? 'relation' : fieldType,
|
|
414
|
+
required: typeInfo.isRequired,
|
|
415
|
+
sortable: ['text', 'number', 'date'].includes(fieldType),
|
|
416
|
+
searchable: ['text', 'email'].includes(fieldType),
|
|
417
|
+
showInList: fieldName !== primaryKey && fieldType !== 'textarea' && fieldType !== 'json',
|
|
418
|
+
showInForm: fieldName !== primaryKey,
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
if (relatedResource) {
|
|
422
|
+
field.resource = relatedResource;
|
|
423
|
+
field.optionLabel = 'name';
|
|
424
|
+
field.optionValue = 'id';
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (enumMap.has(typeInfo.baseName)) {
|
|
428
|
+
const enumValues = enumMap.get(typeInfo.baseName) ?? [];
|
|
429
|
+
field.options = enumValues.map(val => ({
|
|
430
|
+
label: humanize(val),
|
|
431
|
+
value: val,
|
|
432
|
+
}));
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
fields.push(field);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Sort fields: primaryKey first, then alphabetically
|
|
439
|
+
fields.sort((a, b) => {
|
|
440
|
+
if (a.key === primaryKey) return -1;
|
|
441
|
+
if (b.key === primaryKey) return 1;
|
|
442
|
+
return a.key.localeCompare(b.key);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// Detect CRUD capabilities
|
|
446
|
+
const canShow = queryFields.size === 0 ||
|
|
447
|
+
queryFields.has(baseName) ||
|
|
448
|
+
queryFields.has(resourceName) ||
|
|
449
|
+
queryFields.has(`get${baseName}`) ||
|
|
450
|
+
queryFields.has(`${baseName}byid`) ||
|
|
451
|
+
queryFields.has(`find${baseName}`);
|
|
452
|
+
|
|
453
|
+
const canCreate = mutationFields.size === 0 ||
|
|
454
|
+
mutationFields.has(`create${baseName}`) ||
|
|
455
|
+
mutationFields.has(`insert${baseName}`) ||
|
|
456
|
+
mutationFields.has(`add${baseName}`) ||
|
|
457
|
+
mutationFields.has(`insert_${resourceName}`) ||
|
|
458
|
+
mutationFields.has(`create_${baseName}`);
|
|
459
|
+
|
|
460
|
+
const canEdit = mutationFields.size === 0 ||
|
|
461
|
+
mutationFields.has(`update${baseName}`) ||
|
|
462
|
+
mutationFields.has(`edit${baseName}`) ||
|
|
463
|
+
mutationFields.has(`update_${resourceName}`) ||
|
|
464
|
+
mutationFields.has(`update_${baseName}`) ||
|
|
465
|
+
mutationFields.has(`save${baseName}`);
|
|
466
|
+
|
|
467
|
+
const canDelete = mutationFields.size === 0 ||
|
|
468
|
+
mutationFields.has(`delete${baseName}`) ||
|
|
469
|
+
mutationFields.has(`remove${baseName}`) ||
|
|
470
|
+
mutationFields.has(`delete_${resourceName}`) ||
|
|
471
|
+
mutationFields.has(`delete_${baseName}`);
|
|
472
|
+
|
|
473
|
+
resources.push({
|
|
474
|
+
name: resourceName,
|
|
475
|
+
label: typeName,
|
|
476
|
+
primaryKey,
|
|
477
|
+
fields,
|
|
478
|
+
canCreate,
|
|
479
|
+
canEdit,
|
|
480
|
+
canDelete,
|
|
481
|
+
canShow,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return resources;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// ─── Utilities ───────────────────────────────────────────────
|
|
489
|
+
|
|
490
|
+
function humanize(key: string): string {
|
|
491
|
+
return key
|
|
492
|
+
.replace(/_/g, ' ')
|
|
493
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
494
|
+
.replace(/\b\w/g, c => c.toUpperCase());
|
|
495
|
+
}
|
package/src/inferencer.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Analyzes API response data and infers ResourceDefinition + FieldDefinition[].
|
|
5
5
|
* Can also generate copy-paste-ready Svelte component code.
|
|
6
6
|
*/
|
|
7
|
-
import type { FieldDefinition, ResourceDefinition } from '
|
|
7
|
+
import type { FieldDefinition, ResourceDefinition } from './types';
|
|
8
8
|
|
|
9
9
|
// ─── Field Type Inference ────────────────────────────────────
|
|
10
10
|
|
|
@@ -83,6 +83,13 @@ export interface InferResult {
|
|
|
83
83
|
fields: FieldDefinition[];
|
|
84
84
|
resource: ResourceDefinition;
|
|
85
85
|
code: string;
|
|
86
|
+
typeboxCode: string;
|
|
87
|
+
componentCode: {
|
|
88
|
+
list: string;
|
|
89
|
+
create: string;
|
|
90
|
+
edit: string;
|
|
91
|
+
show: string;
|
|
92
|
+
};
|
|
86
93
|
}
|
|
87
94
|
|
|
88
95
|
/**
|
|
@@ -103,10 +110,18 @@ export function inferResource(
|
|
|
103
110
|
const label = options.label ?? capitalize(resourceName);
|
|
104
111
|
|
|
105
112
|
if (!sampleData.length) {
|
|
113
|
+
const emptyResource: ResourceDefinition = { name: resourceName, label, fields: [], primaryKey };
|
|
106
114
|
return {
|
|
107
115
|
fields: [],
|
|
108
|
-
resource:
|
|
116
|
+
resource: emptyResource,
|
|
109
117
|
code: `// No data available to infer fields for "${resourceName}".`,
|
|
118
|
+
typeboxCode: generateTypeBoxSchemaCode(emptyResource),
|
|
119
|
+
componentCode: {
|
|
120
|
+
list: generateListPageCode(emptyResource),
|
|
121
|
+
create: generateCreatePageCode(emptyResource),
|
|
122
|
+
edit: generateEditPageCode(emptyResource),
|
|
123
|
+
show: generateShowPageCode(emptyResource),
|
|
124
|
+
},
|
|
110
125
|
};
|
|
111
126
|
}
|
|
112
127
|
|
|
@@ -190,15 +205,22 @@ export function inferResource(
|
|
|
190
205
|
};
|
|
191
206
|
|
|
192
207
|
const code = generateCode(resource);
|
|
208
|
+
const typeboxCode = generateTypeBoxSchemaCode(resource);
|
|
209
|
+
const componentCode = {
|
|
210
|
+
list: generateListPageCode(resource),
|
|
211
|
+
create: generateCreatePageCode(resource),
|
|
212
|
+
edit: generateEditPageCode(resource),
|
|
213
|
+
show: generateShowPageCode(resource),
|
|
214
|
+
};
|
|
193
215
|
|
|
194
|
-
return { fields, resource, code };
|
|
216
|
+
return { fields, resource, code, typeboxCode, componentCode };
|
|
195
217
|
}
|
|
196
218
|
|
|
197
219
|
// ─── Code Generation ─────────────────────────────────────────
|
|
198
220
|
|
|
199
221
|
function generateCode(resource: ResourceDefinition): string {
|
|
200
222
|
const esc = (s: string) => s.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
201
|
-
const fieldsCode = resource.fields.map(f => {
|
|
223
|
+
const fieldsCode = resource.fields.map((f: FieldDefinition) => {
|
|
202
224
|
const lines = [
|
|
203
225
|
` { key: '${esc(f.key)}', label: '${esc(f.label)}', type: '${f.type}'`,
|
|
204
226
|
];
|
|
@@ -235,6 +257,180 @@ ${fieldsCode}
|
|
|
235
257
|
`;
|
|
236
258
|
}
|
|
237
259
|
|
|
260
|
+
/**
|
|
261
|
+
* Generate Svelte 5 ListPage component code for a resource.
|
|
262
|
+
*/
|
|
263
|
+
export function generateListPageCode(resource: ResourceDefinition): string {
|
|
264
|
+
return `<script lang="ts">
|
|
265
|
+
import { ListPage, AutoTable } from '@svadmin/ui';
|
|
266
|
+
</script>
|
|
267
|
+
|
|
268
|
+
<ListPage resourceName="${resource.name}">
|
|
269
|
+
<AutoTable resourceName="${resource.name}" />
|
|
270
|
+
</ListPage>
|
|
271
|
+
`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Generate Svelte 5 CreatePage component code for a resource.
|
|
276
|
+
*/
|
|
277
|
+
export function generateCreatePageCode(resource: ResourceDefinition): string {
|
|
278
|
+
return `<script lang="ts">
|
|
279
|
+
import { CreatePage, AutoForm } from '@svadmin/ui';
|
|
280
|
+
</script>
|
|
281
|
+
|
|
282
|
+
<CreatePage resourceName="${resource.name}">
|
|
283
|
+
<AutoForm resourceName="${resource.name}" mode="create" />
|
|
284
|
+
</CreatePage>
|
|
285
|
+
`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Generate Svelte 5 EditPage component code for a resource.
|
|
290
|
+
*/
|
|
291
|
+
export function generateEditPageCode(resource: ResourceDefinition): string {
|
|
292
|
+
return `<script lang="ts">
|
|
293
|
+
import { EditPage, AutoForm } from '@svadmin/ui';
|
|
294
|
+
|
|
295
|
+
interface Props {
|
|
296
|
+
id?: string | number;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
let { id }: Props = $props();
|
|
300
|
+
</script>
|
|
301
|
+
|
|
302
|
+
<EditPage resourceName="${resource.name}" {id}>
|
|
303
|
+
<AutoForm resourceName="${resource.name}" {id} mode="edit" />
|
|
304
|
+
</EditPage>
|
|
305
|
+
`;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Generate Svelte 5 ShowPage component code for a resource.
|
|
310
|
+
*/
|
|
311
|
+
export function generateShowPageCode(resource: ResourceDefinition): string {
|
|
312
|
+
return `<script lang="ts">
|
|
313
|
+
import { ShowPage, AutoForm } from '@svadmin/ui';
|
|
314
|
+
|
|
315
|
+
interface Props {
|
|
316
|
+
id?: string | number;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
let { id }: Props = $props();
|
|
320
|
+
</script>
|
|
321
|
+
|
|
322
|
+
<ShowPage resourceName="${resource.name}" {id}>
|
|
323
|
+
<AutoForm resourceName="${resource.name}" {id} mode="show" />
|
|
324
|
+
</ShowPage>
|
|
325
|
+
`;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Generate TypeScript code for ResourceDefinition.
|
|
330
|
+
*/
|
|
331
|
+
export function generateResourceCode(resource: ResourceDefinition): string {
|
|
332
|
+
return generateCode(resource);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Generate TypeScript code for TypeBox schema definition.
|
|
337
|
+
*/
|
|
338
|
+
export function generateTypeBoxSchemaCode(resource: ResourceDefinition): string {
|
|
339
|
+
const esc = (s: string) => s.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
340
|
+
const schemaProps = resource.fields.map((f: FieldDefinition) => {
|
|
341
|
+
let typeDef: string;
|
|
342
|
+
const isRequired = f.required === true;
|
|
343
|
+
|
|
344
|
+
switch (f.type) {
|
|
345
|
+
case 'number':
|
|
346
|
+
typeDef = 'Type.Number()';
|
|
347
|
+
break;
|
|
348
|
+
case 'boolean':
|
|
349
|
+
typeDef = 'Type.Boolean()';
|
|
350
|
+
break;
|
|
351
|
+
case 'date':
|
|
352
|
+
typeDef = "Type.String({ format: 'date-time' })";
|
|
353
|
+
break;
|
|
354
|
+
case 'email':
|
|
355
|
+
typeDef = "Type.String({ format: 'email' })";
|
|
356
|
+
break;
|
|
357
|
+
case 'url':
|
|
358
|
+
typeDef = "Type.String({ format: 'uri' })";
|
|
359
|
+
break;
|
|
360
|
+
case 'phone':
|
|
361
|
+
typeDef = "Type.String({ pattern: '^\\\\+?[\\\\d\\\\s\\\\-()]{7,}$' })";
|
|
362
|
+
break;
|
|
363
|
+
case 'tags':
|
|
364
|
+
typeDef = 'Type.Array(Type.String())';
|
|
365
|
+
break;
|
|
366
|
+
case 'images':
|
|
367
|
+
typeDef = "Type.Array(Type.String({ format: 'uri' }))";
|
|
368
|
+
break;
|
|
369
|
+
case 'json':
|
|
370
|
+
typeDef = 'Type.Record(Type.String(), Type.Unknown())';
|
|
371
|
+
break;
|
|
372
|
+
case 'select':
|
|
373
|
+
if (f.options && f.options.length > 0) {
|
|
374
|
+
const literals = f.options.map(o => `Type.Literal('${esc(String(o.value))}')`).join(', ');
|
|
375
|
+
typeDef = `Type.Union([${literals}])`;
|
|
376
|
+
} else {
|
|
377
|
+
typeDef = 'Type.String()';
|
|
378
|
+
}
|
|
379
|
+
break;
|
|
380
|
+
case 'relation':
|
|
381
|
+
typeDef = 'Type.Union([Type.String(), Type.Number()])';
|
|
382
|
+
break;
|
|
383
|
+
case 'textarea':
|
|
384
|
+
case 'richtext':
|
|
385
|
+
case 'text':
|
|
386
|
+
default:
|
|
387
|
+
typeDef = 'Type.String()';
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (!isRequired && f.key !== (resource.primaryKey ?? 'id')) {
|
|
392
|
+
typeDef = `Type.Optional(${typeDef})`;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return ` ${f.key}: ${typeDef},`;
|
|
396
|
+
}).join('\n');
|
|
397
|
+
|
|
398
|
+
const baseName = resource.name.endsWith('s') ? resource.name.slice(0, -1) : resource.name;
|
|
399
|
+
const typeName = capitalize(baseName);
|
|
400
|
+
|
|
401
|
+
return `import { Type, type Static } from '@sinclair/typebox';
|
|
402
|
+
|
|
403
|
+
export const ${typeName}Schema = Type.Object({
|
|
404
|
+
${schemaProps}
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
export type ${typeName} = Static<typeof ${typeName}Schema>;
|
|
408
|
+
`;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Universal code generator for resource definition or Svelte page components.
|
|
413
|
+
*/
|
|
414
|
+
export function generateComponentCode(
|
|
415
|
+
resource: ResourceDefinition,
|
|
416
|
+
kind: 'resource' | 'typebox' | 'list' | 'create' | 'edit' | 'show'
|
|
417
|
+
): string {
|
|
418
|
+
switch (kind) {
|
|
419
|
+
case 'resource':
|
|
420
|
+
return generateResourceCode(resource);
|
|
421
|
+
case 'typebox':
|
|
422
|
+
return generateTypeBoxSchemaCode(resource);
|
|
423
|
+
case 'list':
|
|
424
|
+
return generateListPageCode(resource);
|
|
425
|
+
case 'create':
|
|
426
|
+
return generateCreatePageCode(resource);
|
|
427
|
+
case 'edit':
|
|
428
|
+
return generateEditPageCode(resource);
|
|
429
|
+
case 'show':
|
|
430
|
+
return generateShowPageCode(resource);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
238
434
|
// ─── Utilities ───────────────────────────────────────────────
|
|
239
435
|
|
|
240
436
|
function capitalize(s: string): string {
|
|
@@ -247,3 +443,28 @@ function humanize(key: string): string {
|
|
|
247
443
|
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
248
444
|
.replace(/\b\w/g, c => c.toUpperCase());
|
|
249
445
|
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Generate a complete InferResult bundle from an existing ResourceDefinition.
|
|
449
|
+
*/
|
|
450
|
+
export function generateResourceBundle(resource: ResourceDefinition): InferResult {
|
|
451
|
+
return {
|
|
452
|
+
fields: resource.fields,
|
|
453
|
+
resource,
|
|
454
|
+
code: generateResourceCode(resource),
|
|
455
|
+
typeboxCode: generateTypeBoxSchemaCode(resource),
|
|
456
|
+
componentCode: {
|
|
457
|
+
list: generateListPageCode(resource),
|
|
458
|
+
create: generateCreatePageCode(resource),
|
|
459
|
+
edit: generateEditPageCode(resource),
|
|
460
|
+
show: generateShowPageCode(resource),
|
|
461
|
+
},
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export { inferFromOpenAPI } from './inferencer-openapi';
|
|
466
|
+
export type { InferFromOpenAPIOptions } from './inferencer-openapi';
|
|
467
|
+
export { inferFromGraphQL, GRAPHQL_INTROSPECTION_QUERY } from './inferencer-graphql';
|
|
468
|
+
export type { InferFromGraphQLOptions, GraphQLIntrospectionSchema, GraphQLTypeDescriptor } from './inferencer-graphql';
|
|
469
|
+
|
|
470
|
+
export type { FieldDefinition, ResourceDefinition } from './types';
|
package/src/types.ts
CHANGED
|
@@ -477,7 +477,8 @@ export interface FieldDefinition {
|
|
|
477
477
|
label: string;
|
|
478
478
|
type: 'text' | 'number' | 'boolean' | 'date' | 'select' | 'multiselect' | 'tags'
|
|
479
479
|
| 'textarea' | 'richtext' | 'image' | 'images' | 'json' | 'relation' | 'color'
|
|
480
|
-
| 'url' | 'email' | 'phone' | 'currency' | 'file' | 'markdown' | 'password' | 'array'
|
|
480
|
+
| 'url' | 'email' | 'phone' | 'currency' | 'file' | 'markdown' | 'password' | 'array'
|
|
481
|
+
| 'tree-select' | 'treeselect' | 'cascader' | 'transfer' | 'rate' | 'rating' | 'avatar' | 'copy' | 'code';
|
|
481
482
|
required?: boolean;
|
|
482
483
|
searchable?: boolean;
|
|
483
484
|
sortable?: boolean;
|
|
@@ -488,7 +489,13 @@ export interface FieldDefinition {
|
|
|
488
489
|
showInCreate?: boolean;
|
|
489
490
|
showInEdit?: boolean;
|
|
490
491
|
showInShow?: boolean;
|
|
491
|
-
options?: { label: string; value: string | number }[];
|
|
492
|
+
options?: { label: string; value: string | number; children?: unknown[]; disabled?: boolean }[];
|
|
493
|
+
treeOptions?: { label: string; value: string | number; children?: unknown[]; disabled?: boolean }[];
|
|
494
|
+
cascaderOptions?: { label: string; value: string | number; children?: unknown[]; disabled?: boolean }[];
|
|
495
|
+
transferData?: { key: string | number; title: string; description?: string; disabled?: boolean }[];
|
|
496
|
+
multiple?: boolean;
|
|
497
|
+
changeOnSelect?: boolean;
|
|
498
|
+
separator?: string;
|
|
492
499
|
defaultValue?: unknown;
|
|
493
500
|
// Relation support
|
|
494
501
|
resource?: string; // related resource name
|