prisma-safe-ai-query 1.0.6 → 2.0.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/dist/engine.js CHANGED
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PrismaAIEngine = void 0;
4
4
  const schema_1 = require("./schema");
5
+ const NUMERIC_FIELD_HEURISTIC = /(?:^id$|Id$|^age$|^count$|^total$|^amount$|^price$|^quantity$|^score$|^level$|^page$|^limit$)/;
5
6
  class PrismaAIEngine {
6
7
  static matchesSecurityRule(value, rule) {
7
8
  const normalizedValue = value.trim();
@@ -9,92 +10,411 @@ class PrismaAIEngine {
9
10
  if (!normalizedValue || !normalizedRule) {
10
11
  return false;
11
12
  }
12
- return normalizedValue === normalizedRule || normalizedValue.startsWith(`${normalizedRule}.`);
13
+ return (normalizedValue === normalizedRule ||
14
+ normalizedValue.startsWith(`${normalizedRule}.`));
13
15
  }
14
- static normalizeFilterValue(filter) {
15
- const { operator, value } = filter;
16
- if (operator === 'in' || operator === 'notIn') {
16
+ static validatePath(pathStr, config, entityType = "field", rootTable) {
17
+ const normalizedPath = pathStr.trim();
18
+ if (!normalizedPath)
19
+ return;
20
+ const parts = normalizedPath.split(".");
21
+ const fieldLeaf = parts[parts.length - 1];
22
+ const tablePrefixedPath = rootTable &&
23
+ !normalizedPath.startsWith(`${rootTable}.`) &&
24
+ normalizedPath !== rootTable
25
+ ? `${rootTable}.${normalizedPath}`
26
+ : normalizedPath;
27
+ if (config?.blockList) {
28
+ const isBlocked = config.blockList.some((rule) => {
29
+ const trimmedRule = rule.trim();
30
+ return (this.matchesSecurityRule(normalizedPath, trimmedRule) ||
31
+ this.matchesSecurityRule(tablePrefixedPath, trimmedRule) ||
32
+ this.matchesSecurityRule(fieldLeaf, trimmedRule));
33
+ });
34
+ if (isBlocked) {
35
+ if (entityType === "table") {
36
+ throw new Error(`Security Exception: AI attempted to query a blocked table -> ${normalizedPath}`);
37
+ }
38
+ throw new Error(`Security Exception: AI attempted to query a blocked table or field -> ${normalizedPath}`);
39
+ }
40
+ }
41
+ if (config?.allowList) {
42
+ if (entityType === "table") {
43
+ const isTableAllowed = config.allowList.some((rule) => {
44
+ const trimmedRule = rule.trim();
45
+ return (this.matchesSecurityRule(normalizedPath, trimmedRule) ||
46
+ normalizedPath.toLowerCase() === trimmedRule.toLowerCase());
47
+ });
48
+ if (!isTableAllowed) {
49
+ throw new Error(`Security Exception: AI attempted to query a non-allowlisted table -> ${normalizedPath}`);
50
+ }
51
+ }
52
+ else {
53
+ // For fields, check if the specific field path, prefixed path, or leaf is allowlisted
54
+ const isFieldAllowed = config.allowList.some((rule) => {
55
+ const trimmedRule = rule.trim();
56
+ return (this.matchesSecurityRule(normalizedPath, trimmedRule) ||
57
+ this.matchesSecurityRule(fieldLeaf, trimmedRule));
58
+ });
59
+ if (!isFieldAllowed) {
60
+ throw new Error(`Security Exception: AI attempted to query a non-allowlisted field -> ${normalizedPath}`);
61
+ }
62
+ }
63
+ }
64
+ }
65
+ static coerceScalarValue(val, expectedType, schema, fieldName) {
66
+ if (val === null || val === undefined)
67
+ return val;
68
+ if (expectedType) {
69
+ const lowerType = expectedType.toLowerCase();
70
+ if (lowerType === "int" || lowerType === "bigint") {
71
+ if (typeof val === "string") {
72
+ const parsed = parseInt(val.trim(), 10);
73
+ if (!isNaN(parsed))
74
+ return parsed;
75
+ }
76
+ else if (typeof val === "number") {
77
+ return Math.round(val);
78
+ }
79
+ }
80
+ else if (lowerType === "float" || lowerType === "decimal") {
81
+ if (typeof val === "string") {
82
+ const parsed = parseFloat(val.trim());
83
+ if (!isNaN(parsed))
84
+ return parsed;
85
+ }
86
+ }
87
+ else if (lowerType === "boolean") {
88
+ if (typeof val === "string") {
89
+ const trimmed = val.trim().toLowerCase();
90
+ if (trimmed === "true")
91
+ return true;
92
+ if (trimmed === "false")
93
+ return false;
94
+ }
95
+ }
96
+ else if (lowerType === "datetime") {
97
+ if (typeof val === "string" && val.trim()) {
98
+ const d = new Date(val.trim());
99
+ if (!isNaN(d.getTime()))
100
+ return d;
101
+ }
102
+ }
103
+ else if (schema?.enums?.length && typeof val === "string") {
104
+ // Case-insensitive Enum matching (e.g. "inactive" -> "INACTIVE")
105
+ const enumDef = schema.enums.find((e) => e.name.toLowerCase() === lowerType);
106
+ if (enumDef) {
107
+ const trimmedVal = val.trim();
108
+ const matchedEnumVal = enumDef.values.find((v) => v.toLowerCase() === trimmedVal.toLowerCase());
109
+ if (matchedEnumVal) {
110
+ return matchedEnumVal;
111
+ }
112
+ }
113
+ }
114
+ }
115
+ else {
116
+ // Heuristic fallback when schema type is not provided:
117
+ // Only coerce numeric strings for likely numeric column names (id, userId, age, count, etc.)
118
+ if (typeof val === "string") {
119
+ const trimmed = val.trim();
120
+ const leafName = fieldName ? fieldName.split(".").pop() || "" : "";
121
+ if (/^-?\d+$/.test(trimmed) &&
122
+ (!leafName || NUMERIC_FIELD_HEURISTIC.test(leafName))) {
123
+ const parsed = parseInt(trimmed, 10);
124
+ if (Number.isSafeInteger(parsed))
125
+ return parsed;
126
+ }
127
+ else if (trimmed.toLowerCase() === "true") {
128
+ return true;
129
+ }
130
+ else if (trimmed.toLowerCase() === "false") {
131
+ return false;
132
+ }
133
+ }
134
+ }
135
+ return val;
136
+ }
137
+ static normalizeFilterValue(filter, expectedType, schema) {
138
+ const { field, operator, value } = filter;
139
+ if (operator === "in" || operator === "notIn") {
140
+ let rawList = [];
17
141
  if (Array.isArray(value)) {
18
- return value;
142
+ rawList = value;
19
143
  }
20
- if (typeof value === 'string') {
144
+ else if (typeof value === "string") {
21
145
  const trimmed = value.trim();
22
146
  if (!trimmed) {
23
147
  return [];
24
148
  }
25
- if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
149
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
26
150
  try {
27
151
  const parsed = JSON.parse(trimmed);
28
- return Array.isArray(parsed) ? parsed : [parsed];
152
+ rawList = Array.isArray(parsed) ? parsed : [parsed];
29
153
  }
30
154
  catch {
31
- // Fall back to comma-splitting below.
155
+ rawList = trimmed.includes(",")
156
+ ? trimmed
157
+ .split(",")
158
+ .map((part) => part.trim())
159
+ .filter(Boolean)
160
+ : [trimmed];
32
161
  }
33
162
  }
34
- return trimmed.includes(',')
35
- ? trimmed.split(',').map(part => part.trim()).filter(Boolean)
36
- : [trimmed];
163
+ else {
164
+ rawList = trimmed.includes(",")
165
+ ? trimmed
166
+ .split(",")
167
+ .map((part) => part.trim())
168
+ .filter(Boolean)
169
+ : [trimmed];
170
+ }
37
171
  }
38
- return [value];
172
+ else {
173
+ rawList = [value];
174
+ }
175
+ return rawList.map((item) => this.coerceScalarValue(item, expectedType, schema, field));
39
176
  }
40
- if (typeof value === 'string') {
177
+ if (typeof value === "string") {
41
178
  const trimmed = value.trim();
42
179
  if (!trimmed) {
43
180
  return value;
44
181
  }
45
182
  try {
46
- if ((trimmed.startsWith('[') && trimmed.endsWith(']')) || (trimmed.startsWith('{') && trimmed.endsWith('}'))) {
183
+ if ((trimmed.startsWith("[") && trimmed.endsWith("]")) ||
184
+ (trimmed.startsWith("{") && trimmed.endsWith("}"))) {
47
185
  const parsed = JSON.parse(trimmed);
48
186
  return parsed;
49
187
  }
50
188
  }
51
189
  catch {
52
- // Keep the original string when the value is not valid JSON.
190
+ // Keep original string if not valid JSON
191
+ }
192
+ }
193
+ return this.coerceScalarValue(value, expectedType, schema, field);
194
+ }
195
+ /**
196
+ * Recursively strips blocked fields from returned database records or objects.
197
+ */
198
+ static filterBlockedData(data, config, currentPath = "", rootTable) {
199
+ if (!config?.blockList?.length && !config?.allowList?.length) {
200
+ return data;
201
+ }
202
+ if (data === null || data === undefined) {
203
+ return data;
204
+ }
205
+ if (Array.isArray(data)) {
206
+ return data.map((item) => this.filterBlockedData(item, config, currentPath, rootTable));
207
+ }
208
+ if (typeof data === "object" && !(data instanceof Date)) {
209
+ const sanitized = {};
210
+ for (const [key, val] of Object.entries(data)) {
211
+ const subPath = currentPath ? `${currentPath}.${key}` : key;
212
+ const tablePrefixedPath = rootTable && !subPath.startsWith(`${rootTable}.`)
213
+ ? `${rootTable}.${subPath}`
214
+ : subPath;
215
+ const parts = subPath.split(".");
216
+ const fieldLeaf = parts[parts.length - 1];
217
+ // Check blockList
218
+ let isBlocked = false;
219
+ if (config.blockList?.length) {
220
+ isBlocked = config.blockList.some((rule) => {
221
+ const trimmedRule = rule.trim();
222
+ return (this.matchesSecurityRule(subPath, trimmedRule) ||
223
+ this.matchesSecurityRule(fieldLeaf, trimmedRule));
224
+ });
225
+ }
226
+ // Check allowList (if supplied)
227
+ let isAllowed = true;
228
+ if (config.allowList?.length) {
229
+ isAllowed = config.allowList.some((rule) => {
230
+ const trimmedRule = rule.trim();
231
+ return (this.matchesSecurityRule(subPath, trimmedRule) ||
232
+ this.matchesSecurityRule(tablePrefixedPath, trimmedRule) ||
233
+ this.matchesSecurityRule(fieldLeaf, trimmedRule));
234
+ });
235
+ }
236
+ if (isBlocked || !isAllowed) {
237
+ continue;
238
+ }
239
+ sanitized[key] = this.filterBlockedData(val, config, subPath, rootTable);
240
+ }
241
+ return sanitized;
242
+ }
243
+ return data;
244
+ }
245
+ static sanitizeTree(tree, currentPath, config, rootTable) {
246
+ const sanitized = {};
247
+ for (const [key, val] of Object.entries(tree)) {
248
+ const isPrismaKeyword = key === "include" ||
249
+ key === "select" ||
250
+ key === "where" ||
251
+ key === "orderBy" ||
252
+ key === "take" ||
253
+ key === "skip" ||
254
+ key === "some" ||
255
+ key === "every" ||
256
+ key === "none" ||
257
+ key === "is" ||
258
+ key === "isNot";
259
+ const subPath = isPrismaKeyword
260
+ ? currentPath
261
+ : currentPath
262
+ ? `${currentPath}.${key}`
263
+ : key;
264
+ if (!isPrismaKeyword) {
265
+ this.validatePath(subPath, config, "field", rootTable);
266
+ }
267
+ if (val === true || val === false) {
268
+ sanitized[key] = val;
53
269
  }
270
+ else if (val && typeof val === "object") {
271
+ if (Array.isArray(val)) {
272
+ sanitized[key] = val.map((item) => item && typeof item === "object"
273
+ ? this.sanitizeTree(item, subPath, config, rootTable)
274
+ : item);
275
+ }
276
+ else {
277
+ sanitized[key] = this.sanitizeTree(val, subPath, config, rootTable);
278
+ }
279
+ }
280
+ else {
281
+ sanitized[key] = val;
282
+ }
283
+ }
284
+ return sanitized;
285
+ }
286
+ static sanitizeOrderBy(orderBy, config, rootTable) {
287
+ if (!orderBy)
288
+ return undefined;
289
+ if (Array.isArray(orderBy)) {
290
+ return orderBy.map((item) => this.sanitizeOrderBy(item, config, rootTable));
291
+ }
292
+ if (typeof orderBy === "object") {
293
+ return this.sanitizeTree(orderBy, "", config, rootTable);
54
294
  }
55
- return value;
295
+ return orderBy;
296
+ }
297
+ static findModel(modelName, schema) {
298
+ if (!schema?.models?.length)
299
+ return undefined;
300
+ const lower = modelName.trim().toLowerCase();
301
+ return schema.models.find((m) => m.name.toLowerCase() === lower);
302
+ }
303
+ /**
304
+ * Expands dot-notation field paths (e.g. "user.profile.age" or "user.loginAttempts.success")
305
+ * into nested Prisma filter objects, automatically wrapping list relations with `some`.
306
+ */
307
+ static buildNestedCondition(fieldParts, operator, value, currentModelName, schema) {
308
+ if (fieldParts.length === 1) {
309
+ return {
310
+ [fieldParts[0]]: {
311
+ [operator]: value,
312
+ },
313
+ };
314
+ }
315
+ const [head, ...tail] = fieldParts;
316
+ const model = currentModelName
317
+ ? this.findModel(currentModelName, schema)
318
+ : undefined;
319
+ const fieldDef = model?.fields.find((f) => f.name.toLowerCase() === head.toLowerCase());
320
+ const nextModelName = fieldDef?.type;
321
+ const nestedInner = this.buildNestedCondition(tail, operator, value, nextModelName, schema);
322
+ // If the relation is a 1-to-many list (e.g. loginAttempts: LoginAttempt[]), Prisma requires `{ some: ... }`
323
+ if (fieldDef?.isList && fieldDef?.relation) {
324
+ return {
325
+ [head]: {
326
+ some: nestedInner,
327
+ },
328
+ };
329
+ }
330
+ return {
331
+ [head]: nestedInner,
332
+ };
333
+ }
334
+ /**
335
+ * Resolves a field's type across both direct columns and multi-level dot-notation paths.
336
+ */
337
+ static lookupFieldType(targetTable, fieldPath, schema) {
338
+ if (!schema?.models?.length)
339
+ return undefined;
340
+ const parts = fieldPath.trim().split(".");
341
+ let currentModel = this.findModel(targetTable, schema);
342
+ for (let i = 0; i < parts.length; i++) {
343
+ if (!currentModel)
344
+ return undefined;
345
+ const part = parts[i].toLowerCase();
346
+ const field = currentModel.fields.find((f) => f.name.toLowerCase() === part);
347
+ if (!field)
348
+ return undefined;
349
+ if (i === parts.length - 1) {
350
+ return field.type;
351
+ }
352
+ currentModel = this.findModel(field.type, schema);
353
+ }
354
+ return undefined;
56
355
  }
57
356
  static parseDynamicQuery(rawAiOutput, config) {
58
357
  let parsedInput = rawAiOutput;
59
- if (typeof rawAiOutput === 'string') {
358
+ if (typeof rawAiOutput === "string") {
60
359
  try {
61
360
  parsedInput = JSON.parse(rawAiOutput);
62
361
  }
63
- catch (e) {
362
+ catch {
64
363
  throw new Error("AI output was not valid JSON.");
65
364
  }
66
365
  }
67
366
  const validatedData = schema_1.AIQueryResponseSchema.parse(parsedInput);
68
- const targetTable = typeof validatedData.targetTable === 'string' ? validatedData.targetTable.trim() : '';
367
+ const targetTable = typeof validatedData.targetTable === "string"
368
+ ? validatedData.targetTable.trim()
369
+ : "";
69
370
  if (!targetTable) {
70
- throw new Error('Security Exception: AI output did not include a valid targetTable.');
71
- }
72
- if (config?.blockList && config.blockList.some(rule => this.matchesSecurityRule(targetTable, rule))) {
73
- throw new Error(`Security Exception: AI attempted to query a blocked table -> ${targetTable}`);
74
- }
75
- if (config?.allowList && !config.allowList.some(rule => this.matchesSecurityRule(targetTable, rule))) {
76
- throw new Error(`Security Exception: AI attempted to query a non-allowlisted table -> ${targetTable}`);
371
+ throw new Error("Security Exception: AI output did not include a valid targetTable.");
77
372
  }
78
- const conditions = validatedData.filters.map(filter => {
373
+ // Validate root table
374
+ this.validatePath(targetTable, config, "table");
375
+ // Build conditions (expanding dot-notation paths into nested Prisma relational filters)
376
+ const conditions = validatedData.filters.map((filter) => {
79
377
  const normalizedField = filter.field.trim();
80
- if (config?.blockList && config.blockList.some(rule => this.matchesSecurityRule(normalizedField, rule))) {
81
- throw new Error(`Security Exception: AI attempted to query a blocked table or field -> ${normalizedField}`);
82
- }
83
- if (config?.allowList && !config.allowList.some(rule => this.matchesSecurityRule(normalizedField, rule))) {
84
- throw new Error(`Security Exception: AI attempted to query a non-allowlisted field -> ${normalizedField}`);
85
- }
86
- const normalizedValue = this.normalizeFilterValue(filter);
87
- return {
88
- [normalizedField]: {
89
- [filter.operator]: normalizedValue
90
- }
91
- };
378
+ // Validate full field path
379
+ this.validatePath(normalizedField, config, "field", targetTable);
380
+ const expectedType = this.lookupFieldType(targetTable, normalizedField, config?.schemaContext);
381
+ const normalizedValue = this.normalizeFilterValue(filter, expectedType, config?.schemaContext);
382
+ const fieldParts = normalizedField
383
+ .split(".")
384
+ .map((p) => p.trim())
385
+ .filter(Boolean);
386
+ return this.buildNestedCondition(fieldParts, filter.operator, normalizedValue, targetTable, config?.schemaContext);
92
387
  });
93
- const whereClause = conditions.length === 0 ? {} : { [validatedData.logicalOperator]: conditions };
94
- return {
388
+ const whereClause = conditions.length === 0
389
+ ? {}
390
+ : { [validatedData.logicalOperator]: conditions };
391
+ const result = {
95
392
  targetTable,
96
- where: whereClause
393
+ where: whereClause,
97
394
  };
395
+ // Sanitize and attach multi-level include or select (mutually exclusive in Prisma at root)
396
+ if (validatedData.select &&
397
+ typeof validatedData.select === "object" &&
398
+ Object.keys(validatedData.select).length > 0) {
399
+ result.select = this.sanitizeTree(validatedData.select, "", config, targetTable);
400
+ }
401
+ else if (validatedData.include &&
402
+ typeof validatedData.include === "object" &&
403
+ Object.keys(validatedData.include).length > 0) {
404
+ result.include = this.sanitizeTree(validatedData.include, "", config, targetTable);
405
+ }
406
+ // Sanitize and attach ordering
407
+ if (validatedData.orderBy) {
408
+ result.orderBy = this.sanitizeOrderBy(validatedData.orderBy, config, targetTable);
409
+ }
410
+ // Attach pagination
411
+ if (typeof validatedData.take === "number") {
412
+ result.take = validatedData.take;
413
+ }
414
+ if (typeof validatedData.skip === "number") {
415
+ result.skip = validatedData.skip;
416
+ }
417
+ return result;
98
418
  }
99
419
  }
100
420
  exports.PrismaAIEngine = PrismaAIEngine;
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
- export { PrismaAIEngine, type SecurityConfig } from './engine';
2
- export { PrismaGeminiAgent, type AgentConfig } from './agent';
3
- export { AIQueryResponseSchema, PrismaOperatorSchema, type AIQueryResponse } from './schema';
1
+ export { PrismaAIEngine, type SecurityConfig, type ParsedDynamicQuery } from './engine';
2
+ export { PrismaGeminiAgent, type AgentConfig, type NaturalLanguageResult } from './agent';
3
+ export { AIQueryResponseSchema, PrismaOperatorSchema, QueryConditionSchema, type AIQueryResponse, type QueryCondition } from './schema';
4
+ export { extractPrismaSchema, parsePrismaSchemaText, formatSchemaForAIContext, findPrismaSchemaPath, type ExtractedSchema, type ExtractedModel, type ExtractedField, type ExtractedEnum } from './introspect';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PrismaOperatorSchema = exports.AIQueryResponseSchema = exports.PrismaGeminiAgent = exports.PrismaAIEngine = void 0;
3
+ exports.findPrismaSchemaPath = exports.formatSchemaForAIContext = exports.parsePrismaSchemaText = exports.extractPrismaSchema = exports.QueryConditionSchema = exports.PrismaOperatorSchema = exports.AIQueryResponseSchema = exports.PrismaGeminiAgent = exports.PrismaAIEngine = void 0;
4
4
  var engine_1 = require("./engine");
5
5
  Object.defineProperty(exports, "PrismaAIEngine", { enumerable: true, get: function () { return engine_1.PrismaAIEngine; } });
6
6
  var agent_1 = require("./agent");
@@ -8,3 +8,9 @@ Object.defineProperty(exports, "PrismaGeminiAgent", { enumerable: true, get: fun
8
8
  var schema_1 = require("./schema");
9
9
  Object.defineProperty(exports, "AIQueryResponseSchema", { enumerable: true, get: function () { return schema_1.AIQueryResponseSchema; } });
10
10
  Object.defineProperty(exports, "PrismaOperatorSchema", { enumerable: true, get: function () { return schema_1.PrismaOperatorSchema; } });
11
+ Object.defineProperty(exports, "QueryConditionSchema", { enumerable: true, get: function () { return schema_1.QueryConditionSchema; } });
12
+ var introspect_1 = require("./introspect");
13
+ Object.defineProperty(exports, "extractPrismaSchema", { enumerable: true, get: function () { return introspect_1.extractPrismaSchema; } });
14
+ Object.defineProperty(exports, "parsePrismaSchemaText", { enumerable: true, get: function () { return introspect_1.parsePrismaSchemaText; } });
15
+ Object.defineProperty(exports, "formatSchemaForAIContext", { enumerable: true, get: function () { return introspect_1.formatSchemaForAIContext; } });
16
+ Object.defineProperty(exports, "findPrismaSchemaPath", { enumerable: true, get: function () { return introspect_1.findPrismaSchemaPath; } });
@@ -0,0 +1,42 @@
1
+ export interface ExtractedField {
2
+ name: string;
3
+ type: string;
4
+ isList: boolean;
5
+ isRequired: boolean;
6
+ relation?: boolean;
7
+ relationFromFields?: string[];
8
+ relationToFields?: string[];
9
+ }
10
+ export interface ExtractedModel {
11
+ name: string;
12
+ fields: ExtractedField[];
13
+ }
14
+ export interface ExtractedEnum {
15
+ name: string;
16
+ values: string[];
17
+ }
18
+ export interface ExtractedSchema {
19
+ models: ExtractedModel[];
20
+ enums: ExtractedEnum[];
21
+ }
22
+ /**
23
+ * Searches default standard locations for schema.prisma if no explicit path is given.
24
+ */
25
+ export declare function findPrismaSchemaPath(customPath?: string): string | null;
26
+ /**
27
+ * Parses raw schema.prisma contents into structured models, relations, foreign keys, and enums,
28
+ * filtering out datasource URLs, generators, comments, and private credentials.
29
+ */
30
+ export declare function parsePrismaSchemaText(schemaContent: string): ExtractedSchema;
31
+ /**
32
+ * Extracts schema information from a Prisma client instance, DMMF, _runtimeDataModel, or schema file.
33
+ */
34
+ export declare function extractPrismaSchema(input?: string | {
35
+ _dmmf?: any;
36
+ _runtimeDataModel?: any;
37
+ dmmf?: any;
38
+ } | any): ExtractedSchema;
39
+ /**
40
+ * Formats extracted schema models, foreign-key relationships, and enums into an AI-digestible context string.
41
+ */
42
+ export declare function formatSchemaForAIContext(schema: ExtractedSchema): string;