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/README.md +506 -124
- package/dist/agent.d.ts +73 -7
- package/dist/agent.js +334 -10
- package/dist/engine.d.ts +33 -6
- package/dist/engine.js +363 -43
- package/dist/index.d.ts +4 -3
- package/dist/index.js +7 -1
- package/dist/introspect.d.ts +42 -0
- package/dist/introspect.js +270 -0
- package/dist/schema.d.ts +26 -36
- package/dist/schema.js +93 -10
- package/package.json +40 -35
- package/prisma_safe_ai_query_readme (1).md +0 -124
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 ||
|
|
13
|
+
return (normalizedValue === normalizedRule ||
|
|
14
|
+
normalizedValue.startsWith(`${normalizedRule}.`));
|
|
13
15
|
}
|
|
14
|
-
static
|
|
15
|
-
const
|
|
16
|
-
if (
|
|
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
|
-
|
|
142
|
+
rawList = value;
|
|
19
143
|
}
|
|
20
|
-
if (typeof value ===
|
|
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(
|
|
149
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
26
150
|
try {
|
|
27
151
|
const parsed = JSON.parse(trimmed);
|
|
28
|
-
|
|
152
|
+
rawList = Array.isArray(parsed) ? parsed : [parsed];
|
|
29
153
|
}
|
|
30
154
|
catch {
|
|
31
|
-
|
|
155
|
+
rawList = trimmed.includes(",")
|
|
156
|
+
? trimmed
|
|
157
|
+
.split(",")
|
|
158
|
+
.map((part) => part.trim())
|
|
159
|
+
.filter(Boolean)
|
|
160
|
+
: [trimmed];
|
|
32
161
|
}
|
|
33
162
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
163
|
+
else {
|
|
164
|
+
rawList = trimmed.includes(",")
|
|
165
|
+
? trimmed
|
|
166
|
+
.split(",")
|
|
167
|
+
.map((part) => part.trim())
|
|
168
|
+
.filter(Boolean)
|
|
169
|
+
: [trimmed];
|
|
170
|
+
}
|
|
37
171
|
}
|
|
38
|
-
|
|
172
|
+
else {
|
|
173
|
+
rawList = [value];
|
|
174
|
+
}
|
|
175
|
+
return rawList.map((item) => this.coerceScalarValue(item, expectedType, schema, field));
|
|
39
176
|
}
|
|
40
|
-
if (typeof value ===
|
|
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(
|
|
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
|
|
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
|
|
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 ===
|
|
358
|
+
if (typeof rawAiOutput === "string") {
|
|
60
359
|
try {
|
|
61
360
|
parsedInput = JSON.parse(rawAiOutput);
|
|
62
361
|
}
|
|
63
|
-
catch
|
|
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 ===
|
|
367
|
+
const targetTable = typeof validatedData.targetTable === "string"
|
|
368
|
+
? validatedData.targetTable.trim()
|
|
369
|
+
: "";
|
|
69
370
|
if (!targetTable) {
|
|
70
|
-
throw new Error(
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
94
|
-
|
|
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;
|