@svadmin/core 0.42.1 → 0.45.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/core",
3
- "version": "0.42.1",
3
+ "version": "0.45.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.2",
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.4",
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.2",
19
+ "happy-dom": "^20.11.12",
20
20
  "jsdom": "^30.0.1",
21
- "vitest": "^4.1.10"
21
+ "vitest": "^4.1.11"
22
22
  },
23
23
  "peerDependencies": {
24
- "svelte": "^5.56.8",
25
- "@tanstack/svelte-query": "^6.1.38"
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: 'create' | 'update' | 'delete' | 'login' | 'logout';
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;
@@ -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/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 { inferFieldType, inferResource } from './inferencer';
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 '@svadmin/core';
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: { name: resourceName, label, fields: [], primaryKey },
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';