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.
@@ -0,0 +1,270 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.findPrismaSchemaPath = findPrismaSchemaPath;
37
+ exports.parsePrismaSchemaText = parsePrismaSchemaText;
38
+ exports.extractPrismaSchema = extractPrismaSchema;
39
+ exports.formatSchemaForAIContext = formatSchemaForAIContext;
40
+ const fs = __importStar(require("fs"));
41
+ const path = __importStar(require("path"));
42
+ /**
43
+ * Searches default standard locations for schema.prisma if no explicit path is given.
44
+ */
45
+ function findPrismaSchemaPath(customPath) {
46
+ if (customPath) {
47
+ const resolved = path.resolve(customPath);
48
+ if (fs.existsSync(resolved))
49
+ return resolved;
50
+ return null;
51
+ }
52
+ const defaultLocations = [
53
+ path.resolve(process.cwd(), "prisma", "schema.prisma"),
54
+ path.resolve(process.cwd(), "schema.prisma"),
55
+ ];
56
+ for (const loc of defaultLocations) {
57
+ if (fs.existsSync(loc))
58
+ return loc;
59
+ }
60
+ return null;
61
+ }
62
+ const SCALAR_TYPES = new Set([
63
+ "String",
64
+ "Int",
65
+ "Float",
66
+ "Boolean",
67
+ "DateTime",
68
+ "Json",
69
+ "Bytes",
70
+ "Decimal",
71
+ "BigInt",
72
+ ]);
73
+ /**
74
+ * Parses raw schema.prisma contents into structured models, relations, foreign keys, and enums,
75
+ * filtering out datasource URLs, generators, comments, and private credentials.
76
+ */
77
+ function parsePrismaSchemaText(schemaContent) {
78
+ const lines = schemaContent.split(/\r?\n/);
79
+ const models = [];
80
+ const enums = [];
81
+ let currentModel = null;
82
+ let currentEnum = null;
83
+ for (const rawLine of lines) {
84
+ const line = rawLine.replace(/\/\/.*$/, "").trim();
85
+ if (!line)
86
+ continue;
87
+ const modelStartMatch = line.match(/^model\s+([A-Za-z0-9_]+)\s*\{/);
88
+ if (modelStartMatch) {
89
+ currentModel = { name: modelStartMatch[1], fields: [] };
90
+ continue;
91
+ }
92
+ const enumStartMatch = line.match(/^enum\s+([A-Za-z0-9_]+)\s*\{/);
93
+ if (enumStartMatch) {
94
+ currentEnum = { name: enumStartMatch[1], values: [] };
95
+ continue;
96
+ }
97
+ if (line === "}" || line.startsWith("}")) {
98
+ if (currentModel) {
99
+ models.push(currentModel);
100
+ currentModel = null;
101
+ }
102
+ if (currentEnum) {
103
+ enums.push(currentEnum);
104
+ currentEnum = null;
105
+ }
106
+ continue;
107
+ }
108
+ if (currentEnum) {
109
+ const enumVal = line.trim();
110
+ if (enumVal && /^[A-Za-z0-9_]+$/.test(enumVal)) {
111
+ currentEnum.values.push(enumVal);
112
+ }
113
+ continue;
114
+ }
115
+ if (currentModel) {
116
+ if (line.startsWith("@@"))
117
+ continue;
118
+ const tokens = line.split(/\s+/).filter(Boolean);
119
+ if (tokens.length >= 2) {
120
+ const fieldName = tokens[0];
121
+ const fieldType = tokens[1];
122
+ if (fieldName.startsWith("@"))
123
+ continue;
124
+ const isList = fieldType.endsWith("[]");
125
+ const isRequired = !fieldType.endsWith("?");
126
+ const cleanType = fieldType.replace(/[?\[\]]/g, "");
127
+ // Extract @relation(fields: [userId], references: [id]) if present
128
+ let relationFromFields;
129
+ let relationToFields;
130
+ const fieldsMatch = line.match(/fields:\s*\[([^\]]+)\]/);
131
+ if (fieldsMatch) {
132
+ relationFromFields = fieldsMatch[1]
133
+ .split(",")
134
+ .map((s) => s.trim())
135
+ .filter(Boolean);
136
+ }
137
+ const refsMatch = line.match(/references:\s*\[([^\]]+)\]/);
138
+ if (refsMatch) {
139
+ relationToFields = refsMatch[1]
140
+ .split(",")
141
+ .map((s) => s.trim())
142
+ .filter(Boolean);
143
+ }
144
+ const isExplicitRelation = line.includes("@relation") || !SCALAR_TYPES.has(cleanType);
145
+ currentModel.fields.push({
146
+ name: fieldName,
147
+ type: cleanType,
148
+ isList,
149
+ isRequired,
150
+ relation: isExplicitRelation,
151
+ ...(relationFromFields ? { relationFromFields } : {}),
152
+ ...(relationToFields ? { relationToFields } : {}),
153
+ });
154
+ }
155
+ }
156
+ }
157
+ // Post-process: Ensure fields whose type is an Enum are marked as non-relations
158
+ const enumNames = new Set(enums.map((e) => e.name));
159
+ for (const model of models) {
160
+ for (const field of model.fields) {
161
+ if (enumNames.has(field.type)) {
162
+ field.relation = false;
163
+ }
164
+ }
165
+ }
166
+ return { models, enums };
167
+ }
168
+ /**
169
+ * Extracts schema information from a Prisma client instance, DMMF, _runtimeDataModel, or schema file.
170
+ */
171
+ function extractPrismaSchema(input) {
172
+ if (input && typeof input === "object") {
173
+ // 1. Standard DMMF format (Array of models)
174
+ const dmmf = input.dmmf || input._dmmf;
175
+ if (dmmf?.datamodel?.models && Array.isArray(dmmf.datamodel.models)) {
176
+ return {
177
+ models: dmmf.datamodel.models.map((m) => ({
178
+ name: m.name,
179
+ fields: (m.fields || []).map((f) => ({
180
+ name: f.name,
181
+ type: f.type,
182
+ isList: Boolean(f.isList),
183
+ isRequired: Boolean(f.isRequired),
184
+ relation: f.kind === "object",
185
+ ...(f.relationFromFields?.length
186
+ ? { relationFromFields: f.relationFromFields }
187
+ : {}),
188
+ ...(f.relationToFields?.length
189
+ ? { relationToFields: f.relationToFields }
190
+ : {}),
191
+ })),
192
+ })),
193
+ enums: (dmmf.datamodel.enums || []).map((e) => ({
194
+ name: e.name,
195
+ values: (e.values || []).map((v) => typeof v === "string" ? v : v.name),
196
+ })),
197
+ };
198
+ }
199
+ // 2. Prisma v5/v6/v7 _runtimeDataModel format (Record map of models)
200
+ const runtimeModel = input._runtimeDataModel ||
201
+ (input.models && !Array.isArray(input.models) ? input : null);
202
+ if (runtimeModel?.models && typeof runtimeModel.models === "object") {
203
+ const models = Object.entries(runtimeModel.models).map(([modelName, m]) => ({
204
+ name: modelName,
205
+ fields: (m.fields || []).map((f) => ({
206
+ name: f.name,
207
+ type: f.type,
208
+ isList: Boolean(f.isList),
209
+ isRequired: Boolean(f.isRequired),
210
+ relation: f.kind === "object",
211
+ ...(f.relationFromFields?.length
212
+ ? { relationFromFields: f.relationFromFields }
213
+ : {}),
214
+ ...(f.relationToFields?.length
215
+ ? { relationToFields: f.relationToFields }
216
+ : {}),
217
+ })),
218
+ }));
219
+ const enums = runtimeModel.enums
220
+ ? Object.entries(runtimeModel.enums).map(([enumName, e]) => ({
221
+ name: enumName,
222
+ values: (e.values || []).map((v) => typeof v === "string" ? v : v.name),
223
+ }))
224
+ : [];
225
+ return { models, enums };
226
+ }
227
+ }
228
+ // 3. File path resolution
229
+ const filePath = findPrismaSchemaPath(typeof input === "string" ? input : undefined);
230
+ if (!filePath) {
231
+ return { models: [], enums: [] };
232
+ }
233
+ const content = fs.readFileSync(filePath, "utf-8");
234
+ return parsePrismaSchemaText(content);
235
+ }
236
+ /**
237
+ * Formats extracted schema models, foreign-key relationships, and enums into an AI-digestible context string.
238
+ */
239
+ function formatSchemaForAIContext(schema) {
240
+ if (!schema.models.length && !schema.enums.length) {
241
+ return "";
242
+ }
243
+ const parts = [];
244
+ if (schema.models.length > 0) {
245
+ parts.push("Database Models and Relations:");
246
+ for (const m of schema.models) {
247
+ const camelModelName = m.name.charAt(0).toLowerCase() + m.name.slice(1);
248
+ const fieldStrs = m.fields.map((f) => {
249
+ let descriptor = `${f.name}: ${f.type}${f.isList ? "[]" : ""}${!f.isRequired ? "?" : ""}`;
250
+ if (f.relation) {
251
+ if (f.relationFromFields?.length && f.relationToFields?.length) {
252
+ descriptor += ` (relation to ${f.type} via ${f.relationFromFields.join(",")} -> ${f.relationToFields.join(",")})`;
253
+ }
254
+ else {
255
+ descriptor += ` (relation to ${f.type})`;
256
+ }
257
+ }
258
+ return descriptor;
259
+ });
260
+ parts.push(`- Model ${m.name} (targetTable: "${camelModelName}") { ${fieldStrs.join(", ")} }`);
261
+ }
262
+ }
263
+ if (schema.enums.length > 0) {
264
+ parts.push("Database Enums:");
265
+ for (const e of schema.enums) {
266
+ parts.push(`- Enum ${e.name} [${e.values.join(", ")}]`);
267
+ }
268
+ }
269
+ return parts.join("\n");
270
+ }
package/dist/schema.d.ts CHANGED
@@ -1,49 +1,39 @@
1
- import { z } from 'zod';
2
- export declare const PrismaOperatorSchema: z.ZodEnum<["equals", "not", "in", "notIn", "lt", "lte", "gt", "gte", "contains", "startsWith", "endsWith"]>;
3
- export declare const QueryConditionSchema: z.ZodObject<{
4
- field: z.ZodString;
5
- operator: z.ZodEnum<["equals", "not", "in", "notIn", "lt", "lte", "gt", "gte", "contains", "startsWith", "endsWith"]>;
6
- value: z.ZodAny;
7
- }, "strip", z.ZodTypeAny, {
1
+ import { z } from "zod";
2
+ export declare const PrismaOperatorSchema: z.ZodEnum<["equals", "not", "in", "notIn", "lt", "lte", "gt", "gte", "contains", "startsWith", "endsWith", "some", "every", "none", "is", "isNot"]>;
3
+ export interface QueryCondition {
8
4
  field: string;
9
- operator: "equals" | "not" | "in" | "notIn" | "lt" | "lte" | "gt" | "gte" | "contains" | "startsWith" | "endsWith";
5
+ operator: z.infer<typeof PrismaOperatorSchema>;
10
6
  value?: any;
11
- }, {
12
- field: string;
13
- operator: "equals" | "not" | "in" | "notIn" | "lt" | "lte" | "gt" | "gte" | "contains" | "startsWith" | "endsWith";
14
- value?: any;
15
- }>;
7
+ }
8
+ export declare const QueryConditionSchema: z.ZodType<QueryCondition>;
9
+ export declare const OrderByDirectionSchema: z.ZodEnum<["asc", "desc"]>;
10
+ export declare const OrderByItemSchema: z.ZodRecord<z.ZodString, z.ZodEnum<["asc", "desc"]>>;
16
11
  export declare const AIQueryResponseSchema: z.ZodObject<{
17
12
  targetTable: z.ZodOptional<z.ZodString>;
18
13
  logicalOperator: z.ZodDefault<z.ZodEnum<["AND", "OR"]>>;
19
- filters: z.ZodArray<z.ZodObject<{
20
- field: z.ZodString;
21
- operator: z.ZodEnum<["equals", "not", "in", "notIn", "lt", "lte", "gt", "gte", "contains", "startsWith", "endsWith"]>;
22
- value: z.ZodAny;
23
- }, "strip", z.ZodTypeAny, {
24
- field: string;
25
- operator: "equals" | "not" | "in" | "notIn" | "lt" | "lte" | "gt" | "gte" | "contains" | "startsWith" | "endsWith";
26
- value?: any;
27
- }, {
28
- field: string;
29
- operator: "equals" | "not" | "in" | "notIn" | "lt" | "lte" | "gt" | "gte" | "contains" | "startsWith" | "endsWith";
30
- value?: any;
31
- }>, "many">;
14
+ filters: z.ZodArray<z.ZodType<QueryCondition, z.ZodTypeDef, QueryCondition>, "many">;
15
+ include: z.ZodEffects<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>, Record<string, any> | undefined, unknown>;
16
+ select: z.ZodEffects<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>, Record<string, any> | undefined, unknown>;
17
+ orderBy: z.ZodEffects<z.ZodOptional<z.ZodUnion<[z.ZodRecord<z.ZodString, z.ZodEnum<["asc", "desc"]>>, z.ZodArray<z.ZodRecord<z.ZodString, z.ZodEnum<["asc", "desc"]>>, "many">, z.ZodRecord<z.ZodString, z.ZodAny>]>>, Record<string, any> | Record<string, "asc" | "desc"> | Record<string, "asc" | "desc">[] | undefined, unknown>;
18
+ take: z.ZodEffects<z.ZodOptional<z.ZodUnion<[z.ZodNumber, z.ZodString, z.ZodNull]>>, number | undefined, string | number | null | undefined>;
19
+ skip: z.ZodEffects<z.ZodOptional<z.ZodUnion<[z.ZodNumber, z.ZodString, z.ZodNull]>>, number | undefined, string | number | null | undefined>;
32
20
  }, "strip", z.ZodTypeAny, {
33
21
  logicalOperator: "AND" | "OR";
34
- filters: {
35
- field: string;
36
- operator: "equals" | "not" | "in" | "notIn" | "lt" | "lte" | "gt" | "gte" | "contains" | "startsWith" | "endsWith";
37
- value?: any;
38
- }[];
22
+ filters: QueryCondition[];
39
23
  targetTable?: string | undefined;
24
+ include?: Record<string, any> | undefined;
25
+ select?: Record<string, any> | undefined;
26
+ orderBy?: Record<string, any> | Record<string, "asc" | "desc"> | Record<string, "asc" | "desc">[] | undefined;
27
+ take?: number | undefined;
28
+ skip?: number | undefined;
40
29
  }, {
41
- filters: {
42
- field: string;
43
- operator: "equals" | "not" | "in" | "notIn" | "lt" | "lte" | "gt" | "gte" | "contains" | "startsWith" | "endsWith";
44
- value?: any;
45
- }[];
30
+ filters: QueryCondition[];
46
31
  targetTable?: string | undefined;
47
32
  logicalOperator?: "AND" | "OR" | undefined;
33
+ include?: unknown;
34
+ select?: unknown;
35
+ orderBy?: unknown;
36
+ take?: string | number | null | undefined;
37
+ skip?: string | number | null | undefined;
48
38
  }>;
49
39
  export type AIQueryResponse = z.infer<typeof AIQueryResponseSchema>;
package/dist/schema.js CHANGED
@@ -1,18 +1,101 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AIQueryResponseSchema = exports.QueryConditionSchema = exports.PrismaOperatorSchema = void 0;
3
+ exports.AIQueryResponseSchema = exports.OrderByItemSchema = exports.OrderByDirectionSchema = exports.QueryConditionSchema = exports.PrismaOperatorSchema = void 0;
4
4
  const zod_1 = require("zod");
5
5
  exports.PrismaOperatorSchema = zod_1.z.enum([
6
- 'equals', 'not', 'in', 'notIn', 'lt', 'lte',
7
- 'gt', 'gte', 'contains', 'startsWith', 'endsWith'
6
+ "equals",
7
+ "not",
8
+ "in",
9
+ "notIn",
10
+ "lt",
11
+ "lte",
12
+ "gt",
13
+ "gte",
14
+ "contains",
15
+ "startsWith",
16
+ "endsWith",
17
+ // Relational list operators
18
+ "some",
19
+ "every",
20
+ "none",
21
+ // Nullability & existence operators
22
+ "is",
23
+ "isNot",
8
24
  ]);
9
- exports.QueryConditionSchema = zod_1.z.object({
10
- field: zod_1.z.string().describe("The database column name (can include dot notation for relations)"),
25
+ exports.QueryConditionSchema = zod_1.z.lazy(() => zod_1.z.object({
26
+ field: zod_1.z
27
+ .string()
28
+ .describe("The database column name or relation field (supports dot-notation like 'user.email')"),
11
29
  operator: exports.PrismaOperatorSchema,
12
- value: zod_1.z.any().describe("The value to filter by"),
13
- });
30
+ value: zod_1.z
31
+ .any()
32
+ .optional()
33
+ .describe("The filter value or nested sub-condition"),
34
+ }));
35
+ exports.OrderByDirectionSchema = zod_1.z.enum(["asc", "desc"]);
36
+ exports.OrderByItemSchema = zod_1.z.record(zod_1.z.string(), exports.OrderByDirectionSchema);
37
+ const parseJsonOrObject = (val) => {
38
+ if (val === null || val === undefined || val === "")
39
+ return undefined;
40
+ if (typeof val === "string") {
41
+ const trimmed = val.trim();
42
+ if (!trimmed)
43
+ return undefined;
44
+ try {
45
+ return JSON.parse(trimmed);
46
+ }
47
+ catch {
48
+ return undefined;
49
+ }
50
+ }
51
+ return val;
52
+ };
14
53
  exports.AIQueryResponseSchema = zod_1.z.object({
15
- targetTable: zod_1.z.string().optional().describe("The dynamic database table to query (e.g., 'user', 'product')"),
16
- logicalOperator: zod_1.z.enum(['AND', 'OR']).default('AND'),
17
- filters: zod_1.z.array(exports.QueryConditionSchema)
54
+ targetTable: zod_1.z
55
+ .string()
56
+ .optional()
57
+ .describe("The primary database table to query (e.g., 'subscription', 'user')"),
58
+ logicalOperator: zod_1.z.enum(["AND", "OR"]).default("AND"),
59
+ filters: zod_1.z
60
+ .array(exports.QueryConditionSchema)
61
+ .describe("Filter conditions for where clause"),
62
+ include: zod_1.z
63
+ .preprocess(parseJsonOrObject, zod_1.z.record(zod_1.z.string(), zod_1.z.any()).optional())
64
+ .describe("Relations to include in multi-level queries (nested object structure)"),
65
+ select: zod_1.z
66
+ .preprocess(parseJsonOrObject, zod_1.z.record(zod_1.z.string(), zod_1.z.any()).optional())
67
+ .describe("Specific fields to select"),
68
+ orderBy: zod_1.z
69
+ .preprocess(parseJsonOrObject, zod_1.z
70
+ .union([
71
+ exports.OrderByItemSchema,
72
+ zod_1.z.array(exports.OrderByItemSchema),
73
+ zod_1.z.record(zod_1.z.string(), zod_1.z.any()),
74
+ ])
75
+ .optional())
76
+ .describe("Ordering of the results"),
77
+ take: zod_1.z
78
+ .union([zod_1.z.number(), zod_1.z.string(), zod_1.z.null()])
79
+ .optional()
80
+ .transform((val) => {
81
+ if (val === null || val === undefined || val === "")
82
+ return undefined;
83
+ const parsed = typeof val === "string" ? parseInt(val, 10) : val;
84
+ if (isNaN(parsed) || parsed <= 0)
85
+ return undefined;
86
+ return parsed;
87
+ })
88
+ .describe("Maximum number of rows to return (positive integer)"),
89
+ skip: zod_1.z
90
+ .union([zod_1.z.number(), zod_1.z.string(), zod_1.z.null()])
91
+ .optional()
92
+ .transform((val) => {
93
+ if (val === null || val === undefined || val === "")
94
+ return undefined;
95
+ const parsed = typeof val === "string" ? parseInt(val, 10) : val;
96
+ if (isNaN(parsed) || parsed < 0)
97
+ return undefined;
98
+ return parsed;
99
+ })
100
+ .describe("Number of rows to skip (pagination)"),
18
101
  });
package/package.json CHANGED
@@ -1,35 +1,40 @@
1
- {
2
- "name": "prisma-safe-ai-query",
3
- "version": "1.0.6",
4
- "description": "An end-to-end AI ORM: securely translates natural language into Prisma queries using Google Gemini and Zod.",
5
- "main": "dist/index.js",
6
- "types": "dist/index.d.ts",
7
- "scripts": {
8
- "build": "tsc",
9
- "prepublishOnly": "npm run build",
10
- "test": "npm run build && node --test tests/engine.test.js"
11
- },
12
- "keywords": [
13
- "prisma",
14
- "ai",
15
- "llm",
16
- "gemini",
17
- "zod",
18
- "security",
19
- "orm",
20
- "dynamic-routing"
21
- ],
22
- "author": "Olugbenga Taiwo",
23
- "license": "MIT",
24
- "dependencies": {
25
- "@google/genai": "^1.45.0"
26
- },
27
- "peerDependencies": {
28
- "@prisma/client": ">=5.0.0",
29
- "zod": "^3.22.0 || ^4.0.0"
30
- },
31
- "devDependencies": {
32
- "typescript": "^5.7.3",
33
- "@types/node": "^22.20.1"
34
- }
35
- }
1
+ {
2
+ "name": "prisma-safe-ai-query",
3
+ "version": "2.0.0",
4
+ "description": "An end-to-end AI ORM: securely translates natural language into Prisma queries using Google Gemini and Zod.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist",
9
+ "README.md",
10
+ "LICENSE"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "prepublishOnly": "npm run build",
15
+ "test": "npm run build && node --test tests/engine.test.js"
16
+ },
17
+ "keywords": [
18
+ "prisma",
19
+ "ai",
20
+ "llm",
21
+ "gemini",
22
+ "zod",
23
+ "security",
24
+ "orm",
25
+ "multi-level-query"
26
+ ],
27
+ "author": "Olugbenga Taiwo",
28
+ "license": "MIT",
29
+ "dependencies": {
30
+ "@google/genai": "^1.45.0"
31
+ },
32
+ "peerDependencies": {
33
+ "@prisma/client": ">=5.0.0",
34
+ "zod": "^3.22.0 || ^4.0.0"
35
+ },
36
+ "devDependencies": {
37
+ "typescript": "^5.7.3",
38
+ "@types/node": "^22.20.1"
39
+ }
40
+ }
@@ -1,124 +0,0 @@
1
- # 🛡️ Prisma Safe AI Query
2
-
3
- A lightweight, enterprise-grade utility that securely translates natural language into strongly typed, safe Prisma queries using Google Gemini. By bundling `@google/genai` and `zod` directly, this package eliminates all backend boilerplate. You pass in a user prompt, and the package handles the AI orchestration, schema enforcement, and SQL-injection prevention automatically.
4
-
5
- ## 📦 Installation
6
-
7
- ```bash
8
- npm install prisma-safe-ai-query
9
- ```
10
- *(Requires `@prisma/client` as a peer dependency)*
11
-
12
- ## 🚀 Quick Start (The PrismaGeminiAgent)
13
-
14
- Instead of manually configuring the Gemini SDK, our built-in `PrismaGeminiAgent` handles everything end-to-end.
15
-
16
- ### 1. Set your API Key
17
- Ensure you have your Gemini API key in your `.env` file:
18
- ```env
19
- GEMINI_API_KEY=your_copied_api_key_here
20
- GEMINI_MODEL=gemini-3.8-flash
21
- ```
22
- *(Need an API key? Get one for free at [aistudio.google.com](https://aistudio.google.com/))*
23
-
24
- ## 💬 The Chat Flow (Frontend to Backend)
25
-
26
- This package makes it incredibly easy to build a "Chat with your Database" UI. Your frontend remains a simple chat interface, while `PrismaGeminiAgent` handles the heavy lifting on your backend.
27
-
28
- ### Step 1: The Frontend (React/Vue/Vanilla)
29
- The user types a natural language query in your chat UI (e.g., *"Show me all active users in London"*). The frontend simply POSTs this message to your backend.
30
-
31
- ```javascript
32
- // Example React submit handler
33
- async function handleChatSubmit(userInput) {
34
- const response = await fetch('/api/chat', {
35
- method: 'POST',
36
- headers: { 'Content-Type': 'application/json' },
37
- body: JSON.stringify({ userPrompt: userInput })
38
- });
39
-
40
- const result = await response.json();
41
-
42
- if (result.error) return alert(result.error);
43
-
44
- console.log("Safe Database Results:", result.data);
45
- // Render results in your chat UI...
46
- }
47
- ```
48
-
49
- ### Step 2: The Backend API Route (Next.js / Express)
50
- Your backend receives the prompt, safely converts it to a Prisma query, executes it dynamically across ANY table, and returns the data.
51
-
52
- **Import your Prisma Client**
53
- ```typescript
54
- // Prisma v5 import
55
- import { PrismaClient } from '@prisma/client';
56
- const prisma = new PrismaClient();
57
-
58
- // Prisma v6+ import from helper file
59
- // const prisma = require("./helper/prisma");
60
- ```
61
- *(Check out my tutorial on [how to install Prisma v7.8.0 in your Express/PostgreSQL project](https://charisintelligence.com.ng/prisma-v7-express-postgresql/))*
62
-
63
- **Execute the AI Agent**
64
- ```typescript
65
- import { PrismaGeminiAgent } from 'prisma-safe-ai-query';
66
-
67
- // Automatically picks up GEMINI_API_KEY from process.env
68
- const aiAgent = new PrismaGeminiAgent();
69
-
70
- export async function POST(req: Request) {
71
- const { userPrompt } = await req.json(); // e.g. "Show me all active users in London"
72
-
73
- try {
74
- // 1. One line handles Gemini generation, dynamic routing, and Zod security!
75
- const { targetTable, where } = await aiAgent.generateSafeQuery(userPrompt, {
76
- dbContext: "Available tables are user, product, and order.", // Helps Gemini route correctly
77
- blockList: ['password', 'credit_card'] // Automatically blocks these columns
78
- });
79
-
80
- // 2. DYNAMIC ROUTING: Validate the table and execute
81
- if (!targetTable || !(targetTable in prisma)) {
82
- return new Response(JSON.stringify({ error: "Invalid table requested." }), { status: 400 });
83
- }
84
-
85
- // Executes equivalent of prisma.user.findMany(...)
86
- const dbResults = await (prisma as any)[targetTable].findMany({
87
- where: where,
88
- take: 10
89
- });
90
-
91
- // 3. Return the data to the frontend chat UI
92
- return new Response(JSON.stringify({ data: dbResults }), { status: 200 });
93
-
94
- } catch (error) {
95
- return new Response(JSON.stringify({ error: error.message }), { status: 400 });
96
- }
97
- }
98
- ```
99
-
100
- ## 🔒 Security Configuration
101
-
102
- You can strictly control what the AI can and cannot access by passing a configuration object as the second parameter to the `aiAgent.generateSafeQuery()` method.
103
-
104
- **Where to configure this:** You should update these settings directly inside your backend controller or API route file (e.g., `app/api/chat/route.ts` in Next.js, or your Express endpoint function) exactly where you process the incoming chat request.
105
-
106
- ```typescript
107
- const { targetTable, where } = await aiAgent.generateSafeQuery(userPrompt, {
108
-
109
- // OPTION 1: Describe your DB to Gemini so it doesn't hallucinate tables.
110
- // A fast hack is to use an AI (like ChatGPT) to generate your dbContext by giving it your prisma schema.
111
- dbContext: "We have tables: user, product. User has fields: email, role, status.",
112
-
113
- // OPTION 2: Block specific sensitive fields across all tables
114
- blockList: ['passwordHash', 'billingDetails'],
115
-
116
- // OPTION 3: Strict Mode - allow ONLY these fields
117
- // allowList: ['status', 'role', 'city']
118
- });
119
- ```
120
-
121
- ## 👨‍💻 Author
122
- Created by **Olugbenga Taiwo**
123
- - [LinkedIn](https://www.linkedin.com/in/taiwo-olugbenga)
124
- - [GitHub Profile](https://github.com/oluibk2020/)