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/README.md
CHANGED
|
@@ -1,124 +1,506 @@
|
|
|
1
|
-
# 🛡️ Prisma Safe AI Query
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
1
|
+
# 🛡️ Prisma Safe AI Query
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/prisma-safe-ai-query)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://github.com/gbengataiwo)
|
|
6
|
+
|
|
7
|
+
> **Translate Natural Language into Safe, Typed, Multi-Level Prisma Queries with Google Gemini.**
|
|
8
|
+
|
|
9
|
+
Eliminate backend AI prompt engineering, parsing logic, and SQL injection risks. `prisma-safe-ai-query` takes user prompts (e.g., *"Who are the inactive subscribers and what were their last login attempts?"*), auto-inspects your Prisma schema, and generates fully structured, nested Prisma queries with dynamic table targeting, recursive field security, and type enforcement.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 📑 Table of Contents
|
|
14
|
+
|
|
15
|
+
- [🛡️ Prisma Safe AI Query](#️-prisma-safe-ai-query)
|
|
16
|
+
- [📑 Table of Contents](#-table-of-contents)
|
|
17
|
+
- [⚡ Why Prisma Safe AI Query?](#-why-prisma-safe-ai-query)
|
|
18
|
+
- [🏗️ Architecture Overview](#️-architecture-overview)
|
|
19
|
+
- [📦 Installation](#-installation)
|
|
20
|
+
- [⚙️ Environment Setup](#️-environment-setup)
|
|
21
|
+
- [💬 Natural Language vs. Raw Structured Response](#-natural-language-vs-raw-structured-response)
|
|
22
|
+
- [🖥️ Backend Setup (Next.js, Express, Fastify)](#️-backend-setup-nextjs-express-fastify)
|
|
23
|
+
- [Next.js App Router (`app/api/chat-query/route.ts`)](#nextjs-app-router-appapichat-queryroutets)
|
|
24
|
+
- [Express.js Example (`server.ts` or `server.js`)](#expressjs-example-serverts-or-serverjs)
|
|
25
|
+
- [🎨 Frontend Integration (React, Vue.js)](#-frontend-integration-react-vuejs)
|
|
26
|
+
- [1. React / Next.js Client Component](#1-react--nextjs-client-component)
|
|
27
|
+
- [🔗 Multi-Level Relational Queries](#-multi-level-relational-queries)
|
|
28
|
+
- [🧬 Zero-Config Schema Introspection](#-zero-config-schema-introspection)
|
|
29
|
+
- [Privacy \& Security Guarantee](#privacy--security-guarantee)
|
|
30
|
+
- [🔒 Security \& Access Control](#-security--access-control)
|
|
31
|
+
- [Fail-Closed Behavior](#fail-closed-behavior)
|
|
32
|
+
- [🤖 Dynamic Gemini Model Selection](#-dynamic-gemini-model-selection)
|
|
33
|
+
- [🛠️ Standalone Parser for Custom LLMs](#️-standalone-parser-for-custom-llms)
|
|
34
|
+
- [📖 API Reference](#-api-reference)
|
|
35
|
+
- [`new PrismaGeminiAgent(apiKey?: string, model?: string)`](#new-prismageminiagentapikey-string-model-string)
|
|
36
|
+
- [`agent.generateSafeQuery(prompt: string, config?: AgentConfig)`](#agentgeneratesafequeryprompt-string-config-agentconfig)
|
|
37
|
+
- [`agent.execute(prompt: string, prismaClient: PrismaClient, config?: AgentConfig)`](#agentexecute-prompt-string-prismaclient-prismaclient-config-agentconfig)
|
|
38
|
+
- [Standalone schema utilities](#standalone-schema-utilities)
|
|
39
|
+
- [Returns `ParsedDynamicQuery`:](#returns-parseddynamicquery)
|
|
40
|
+
- [📄 License](#-license)
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## ⚡ Why Prisma Safe AI Query?
|
|
45
|
+
|
|
46
|
+
- **Zero SQL / Raw Injection Risk**: Never executes unvalidated strings or concatenated queries. Outputs strictly validated Prisma query AST objects (`where`, `include`, `select`, `orderBy`, `take`).
|
|
47
|
+
- **Multi-Level Relational Queries**: Connects multiple models automatically (e.g., `Subscription` -> `User` -> `LoginAttempts`).
|
|
48
|
+
- **Zero-Config Schema Introspection**: Reads your `schema.prisma` automatically without sending sensitive credentials or database URLs to the AI.
|
|
49
|
+
- **Deep Recursive Security**: Blocks unauthorized columns and relational paths (like `passwordHash` or `user.privateMeta`) across root and nested levels.
|
|
50
|
+
- **Universal Compatibility**: Works out of the box with `@prisma/client` (`v5.x`, `v6.x`, `v7.x+`) and Zod (`v3.x` & `v4.x`).
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 🏗️ Architecture Overview
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
User (Browser Chat UI)
|
|
58
|
+
│
|
|
59
|
+
▼ POST /api/ai-query { prompt: "Show inactive subscribers and their last login" }
|
|
60
|
+
Backend API Route
|
|
61
|
+
│
|
|
62
|
+
├─► PrismaGeminiAgent.execute(prompt, prisma)
|
|
63
|
+
│ ├── 1. Auto-introspects schema.prisma (models, fields, relations)
|
|
64
|
+
│ ├── 2. Calls Google Gemini (structured JSON mode)
|
|
65
|
+
│ └── 3. Validates with Zod & enforces blockList/allowList recursively
|
|
66
|
+
│
|
|
67
|
+
├─► Prisma Client findMany execution
|
|
68
|
+
└─► Optional natural-language summary
|
|
69
|
+
│
|
|
70
|
+
▼ Returns: { data, query, naturalLanguage? }
|
|
71
|
+
Frontend Render
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 📦 Installation
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
npm install prisma-safe-ai-query
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
> **Requirements:** Ensure your project has `@prisma/client` (`>=5.0.0`) and `zod` installed.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## ⚙️ Environment Setup
|
|
87
|
+
|
|
88
|
+
Add your Gemini credentials and optional model selection to your `.env` file:
|
|
89
|
+
|
|
90
|
+
```env
|
|
91
|
+
# Required: Get a free API key at https://aistudio.google.com/
|
|
92
|
+
GEMINI_API_KEY=your_gemini_api_key_here
|
|
93
|
+
|
|
94
|
+
# Optional: Choose a Gemini model (defaults to gemini-3.1-flash-lite-preview)
|
|
95
|
+
GEMINI_MODEL=gemini-2.5-flash
|
|
96
|
+
|
|
97
|
+
# Optional: Return a human-like conversational answer alongside structured data (defaults to true)
|
|
98
|
+
# Set to false to return ONLY raw database records
|
|
99
|
+
NATURAL_LANGUAGE_RESPONSE=true
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## 💬 Natural Language vs. Raw Structured Response
|
|
105
|
+
|
|
106
|
+
`prisma-safe-ai-query` gives you the best of both worlds:
|
|
107
|
+
|
|
108
|
+
- **When `NATURAL_LANGUAGE_RESPONSE=true` (Default)**:
|
|
109
|
+
Returns conversational human text answering the prompt **plus** the raw database records:
|
|
110
|
+
> *"There is only one inactive subscriber and the full name is Kate Michael and his last logged In date is 28th April 2026"*
|
|
111
|
+
- **When `NATURAL_LANGUAGE_RESPONSE=false`**:
|
|
112
|
+
Bypasses the second AI pass and returns **only** the exact structured JSON records from Prisma.
|
|
113
|
+
|
|
114
|
+
You can also toggle this dynamically per request in code:
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
// Explicit override in code:
|
|
118
|
+
const result = await aiAgent.execute(userPrompt, prisma, {
|
|
119
|
+
naturalLanguageResponse: false // forces raw database results only
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The `execute()` result contains the structured `data` and sanitized `query`. When natural language output is enabled, it also contains `naturalLanguage`. This method executes a Prisma `findMany` call, so the supplied Prisma client must expose a delegate for the selected model.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## 🖥️ Backend Setup (Next.js, Express, Fastify)
|
|
128
|
+
|
|
129
|
+
Here is a full backend API route demonstrating how to receive a prompt, translate it, and execute it across dynamic tables.
|
|
130
|
+
|
|
131
|
+
### Next.js App Router (`app/api/chat-query/route.ts`)
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
import { NextResponse } from 'next/server';
|
|
135
|
+
import { PrismaGeminiAgent } from 'prisma-safe-ai-query';
|
|
136
|
+
import { PrismaClient } from '@prisma/client';
|
|
137
|
+
|
|
138
|
+
const prisma = new PrismaClient();
|
|
139
|
+
const aiAgent = new PrismaGeminiAgent();
|
|
140
|
+
|
|
141
|
+
export async function POST(req: Request) {
|
|
142
|
+
try {
|
|
143
|
+
const { prompt } = await req.json();
|
|
144
|
+
|
|
145
|
+
if (!prompt) {
|
|
146
|
+
return NextResponse.json({ error: "Prompt is required" }, { status: 400 });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Single one-liner: generates safe query, executes on Prisma, and formats response
|
|
150
|
+
const result = await aiAgent.execute(prompt, prisma, {
|
|
151
|
+
blockList: ['password', 'passwordHash', 'creditCard', 'refreshToken']
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
return NextResponse.json({
|
|
155
|
+
targetTable: result.query.targetTable,
|
|
156
|
+
data: result.data,
|
|
157
|
+
naturalLanguage: result.naturalLanguage // e.g. "There is only one inactive subscriber..."
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
} catch (error: any) {
|
|
161
|
+
return NextResponse.json(
|
|
162
|
+
{ error: error.message || "Query generation failed" },
|
|
163
|
+
{ status: 400 }
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Express.js Example (`server.ts` or `server.js`)
|
|
170
|
+
|
|
171
|
+
**Using TypeScript or JavaScript (ES Modules):**
|
|
172
|
+
|
|
173
|
+
```javascript
|
|
174
|
+
import express from 'express';
|
|
175
|
+
import { PrismaGeminiAgent } from 'prisma-safe-ai-query';
|
|
176
|
+
import { PrismaClient } from '@prisma/client';
|
|
177
|
+
|
|
178
|
+
const app = express();
|
|
179
|
+
app.use(express.json());
|
|
180
|
+
|
|
181
|
+
const prisma = new PrismaClient();
|
|
182
|
+
const aiAgent = new PrismaGeminiAgent();
|
|
183
|
+
|
|
184
|
+
app.post('/api/chat-query', async (req, res) => {
|
|
185
|
+
try {
|
|
186
|
+
const { prompt } = req.body;
|
|
187
|
+
|
|
188
|
+
// Single call: generates query, executes on Prisma, and returns data + naturalLanguage
|
|
189
|
+
const result = await aiAgent.execute(prompt, prisma, {
|
|
190
|
+
blockList: ['password', 'secretToken', 'refreshToken']
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
res.json({
|
|
194
|
+
targetTable: result.query.targetTable,
|
|
195
|
+
data: result.data,
|
|
196
|
+
naturalLanguage: result.naturalLanguage // e.g. "There is only one inactive subscriber..."
|
|
197
|
+
});
|
|
198
|
+
} catch (err) {
|
|
199
|
+
res.status(400).json({ error: err.message });
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
app.listen(3000, () => console.log('Server running on port 3000'));
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**Using Node.js CommonJS (`require`):**
|
|
207
|
+
|
|
208
|
+
```javascript
|
|
209
|
+
const express = require('express');
|
|
210
|
+
const { PrismaGeminiAgent } = require('prisma-safe-ai-query');
|
|
211
|
+
const { PrismaClient } = require('@prisma/client');
|
|
212
|
+
|
|
213
|
+
const app = express();
|
|
214
|
+
app.use(express.json());
|
|
215
|
+
|
|
216
|
+
const prisma = new PrismaClient();
|
|
217
|
+
const aiAgent = new PrismaGeminiAgent();
|
|
218
|
+
|
|
219
|
+
app.post('/api/chat-query', async (req, res) => {
|
|
220
|
+
try {
|
|
221
|
+
const { prompt } = req.body;
|
|
222
|
+
|
|
223
|
+
const result = await aiAgent.execute(prompt, prisma, {
|
|
224
|
+
blockList: ['password', 'creditCard']
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
res.json({
|
|
228
|
+
targetTable: result.query.targetTable,
|
|
229
|
+
data: result.data,
|
|
230
|
+
naturalLanguage: result.naturalLanguage
|
|
231
|
+
});
|
|
232
|
+
} catch (err) {
|
|
233
|
+
res.status(400).json({ error: err.message });
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
app.listen(3000, () => console.log('Server running on port 3000'));
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## 🎨 Frontend Integration (React, Vue.js)
|
|
243
|
+
|
|
244
|
+
Because database operations must run securely on your server, your frontend connects to your backend API endpoint.
|
|
245
|
+
|
|
246
|
+
### 1. React / Next.js Client Component
|
|
247
|
+
|
|
248
|
+
```tsx
|
|
249
|
+
import React, { useState } from 'react';
|
|
250
|
+
|
|
251
|
+
export default function DatabaseChat() {
|
|
252
|
+
const [prompt, setPrompt] = useState('');
|
|
253
|
+
const [loading, setLoading] = useState(false);
|
|
254
|
+
const [response, setResponse] = useState<any>(null);
|
|
255
|
+
const [error, setError] = useState<string | null>(null);
|
|
256
|
+
|
|
257
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
258
|
+
e.preventDefault();
|
|
259
|
+
setLoading(true);
|
|
260
|
+
setError(null);
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
const res = await fetch('/api/chat-query', {
|
|
264
|
+
method: 'POST',
|
|
265
|
+
headers: { 'Content-Type': 'application/json' },
|
|
266
|
+
body: JSON.stringify({ prompt }),
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const json = await res.json();
|
|
270
|
+
if (!res.ok) throw new Error(json.error || 'Request failed');
|
|
271
|
+
|
|
272
|
+
setResponse(json);
|
|
273
|
+
} catch (err: any) {
|
|
274
|
+
setError(err.message);
|
|
275
|
+
} finally {
|
|
276
|
+
setLoading(false);
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
return (
|
|
281
|
+
<div style={{ maxWidth: 800, margin: '2rem auto', fontFamily: 'sans-serif' }}>
|
|
282
|
+
<h2>💬 Natural Language Database Query</h2>
|
|
283
|
+
|
|
284
|
+
<form onSubmit={handleSubmit} style={{ display: 'flex', gap: '0.5rem' }}>
|
|
285
|
+
<input
|
|
286
|
+
type="text"
|
|
287
|
+
value={prompt}
|
|
288
|
+
onChange={(e) => setPrompt(e.target.value)}
|
|
289
|
+
placeholder="e.g. Find inactive subscribers and show their full name and last login"
|
|
290
|
+
style={{ flex: 1, padding: '0.75rem', borderRadius: 6, border: '1px solid #ccc' }}
|
|
291
|
+
/>
|
|
292
|
+
<button type="submit" disabled={loading} style={{ padding: '0.75rem 1.5rem', cursor: 'pointer' }}>
|
|
293
|
+
{loading ? 'Querying...' : 'Ask DB'}
|
|
294
|
+
</button>
|
|
295
|
+
</form>
|
|
296
|
+
|
|
297
|
+
{error && <p style={{ color: 'red', marginTop: '1rem' }}>❌ {error}</p>}
|
|
298
|
+
|
|
299
|
+
{response && (
|
|
300
|
+
<div style={{ marginTop: '1.5rem', background: '#f5f5f5', padding: '1.25rem', borderRadius: 8 }}>
|
|
301
|
+
{/* Natural Language Response */}
|
|
302
|
+
{response.naturalLanguage && (
|
|
303
|
+
<div style={{ background: '#e8f4fd', borderLeft: '4px solid #0288d1', padding: '0.75rem 1rem', marginBottom: '1rem', borderRadius: 4 }}>
|
|
304
|
+
<strong>🤖 AI Answer:</strong>
|
|
305
|
+
<p style={{ margin: '0.25rem 0 0' }}>{response.naturalLanguage}</p>
|
|
306
|
+
</div>
|
|
307
|
+
)}
|
|
308
|
+
|
|
309
|
+
<h4>Target Model: <code>{response.targetTable}</code> ({response.data.length} records)</h4>
|
|
310
|
+
<pre style={{ maxHeight: 350, overflow: 'auto', background: '#fff', padding: '1rem', borderRadius: 4 }}>
|
|
311
|
+
{JSON.stringify(response.data, null, 2)}
|
|
312
|
+
</pre>
|
|
313
|
+
</div>
|
|
314
|
+
)}
|
|
315
|
+
</div>
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
## 🔗 Multi-Level Relational Queries
|
|
323
|
+
|
|
324
|
+
When users ask relational questions across connected tables, `prisma-safe-ai-query` constructs nested Prisma relations:
|
|
325
|
+
|
|
326
|
+
**Prompt:**
|
|
327
|
+
> *"Who are the inactive subscribers on the database and tell me their full names and their last login attempt?"*
|
|
328
|
+
|
|
329
|
+
**Generated Query Object:**
|
|
330
|
+
```javascript
|
|
331
|
+
{
|
|
332
|
+
targetTable: 'subscription',
|
|
333
|
+
where: {
|
|
334
|
+
AND: [
|
|
335
|
+
{ status: { equals: 'inactive' } }
|
|
336
|
+
]
|
|
337
|
+
},
|
|
338
|
+
include: {
|
|
339
|
+
user: {
|
|
340
|
+
include: {
|
|
341
|
+
loginAttempts: {
|
|
342
|
+
take: 1,
|
|
343
|
+
orderBy: { createdAt: 'desc' }
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
This output matches standard Prisma syntax directly:
|
|
352
|
+
```typescript
|
|
353
|
+
const result = await prisma[query.targetTable].findMany({
|
|
354
|
+
where: query.where,
|
|
355
|
+
include: query.include
|
|
356
|
+
});
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## 🧬 Zero-Config Schema Introspection
|
|
362
|
+
|
|
363
|
+
Schema introspection is enabled by default. The agent looks for `./prisma/schema.prisma` and then `./schema.prisma`; you can provide a custom path or an initialized Prisma client:
|
|
364
|
+
|
|
365
|
+
```typescript
|
|
366
|
+
// 1. Automatic: Finds ./prisma/schema.prisma or ./schema.prisma
|
|
367
|
+
const query = await aiAgent.generateSafeQuery(prompt);
|
|
368
|
+
|
|
369
|
+
// 2. Custom schema location
|
|
370
|
+
const query = await aiAgent.generateSafeQuery(prompt, {
|
|
371
|
+
schemaPath: './db/prisma/schema.prisma'
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// 3. Or pass your initialized PrismaClient directly
|
|
375
|
+
const query = await aiAgent.generateSafeQuery(prompt, {
|
|
376
|
+
prismaClient: prisma
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// 4. Disable automatic schema discovery and provide your own context
|
|
380
|
+
const query = await aiAgent.generateSafeQuery(prompt, {
|
|
381
|
+
autoIntrospect: false,
|
|
382
|
+
dbContext: 'Model User { id: Int, email: String }'
|
|
383
|
+
});
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### Privacy & Security Guarantee
|
|
387
|
+
The introspection utilities extract only model and field metadata for:
|
|
388
|
+
- Models & relation fields
|
|
389
|
+
- Scalar columns & types
|
|
390
|
+
- Enums & enum values
|
|
391
|
+
|
|
392
|
+
Datasource URLs and generator configuration are not included in the formatted context sent to Gemini. `parsePrismaSchemaText`, `extractPrismaSchema`, `formatSchemaForAIContext`, and `findPrismaSchemaPath` are also exported for applications that need to inspect or format schema metadata directly.
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
## 🔒 Security & Access Control
|
|
397
|
+
|
|
398
|
+
Control what tables, columns, and relations the AI is permitted to touch using recursive validation rules:
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
const query = await aiAgent.generateSafeQuery(prompt, {
|
|
402
|
+
// Option A: Block specific sensitive fields anywhere in the query
|
|
403
|
+
blockList: [
|
|
404
|
+
'passwordHash', // Blocks any column named passwordHash
|
|
405
|
+
'creditCardNumber', // Blocks creditCardNumber
|
|
406
|
+
'auditLogs', // Blocks the auditLogs table
|
|
407
|
+
'user.privateNotes' // Blocks relational dot-paths
|
|
408
|
+
],
|
|
409
|
+
|
|
410
|
+
// Option B: Allow only these target models and fields / relation paths
|
|
411
|
+
// A model entry permits the target model; it does not by itself permit every field.
|
|
412
|
+
// allowList: ['user', 'email', 'fullName']
|
|
413
|
+
});
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
Rules support exact names and dot-path descendants. For example, `user.email` permits that relation path, and `user` permits a target table named `user`. Add explicit field names when filtering by scalar columns. `blockList` takes precedence over `allowList`.
|
|
417
|
+
|
|
418
|
+
### Fail-Closed Behavior
|
|
419
|
+
If an AI attempt targets a blocked column (e.g. `include: { user: { select: { passwordHash: true } } }`), the engine throws a `Security Exception` immediately before execution.
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
## 🤖 Dynamic Gemini Model Selection
|
|
424
|
+
|
|
425
|
+
Switch Gemini models dynamically via environment variables or per agent instance:
|
|
426
|
+
|
|
427
|
+
```env
|
|
428
|
+
# In your .env file
|
|
429
|
+
GEMINI_MODEL=gemini-2.5-flash
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
Or pass it directly in code:
|
|
433
|
+
|
|
434
|
+
```typescript
|
|
435
|
+
// 1. Explicit model override in constructor
|
|
436
|
+
const agent = new PrismaGeminiAgent('api-key-here', 'gemini-1.5-pro');
|
|
437
|
+
|
|
438
|
+
// 2. Default fallback
|
|
439
|
+
const agent = new PrismaGeminiAgent(); // Uses GEMINI_MODEL env or gemini-3.1-flash-lite-preview
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
---
|
|
443
|
+
|
|
444
|
+
## 🛠️ Standalone Parser for Custom LLMs
|
|
445
|
+
|
|
446
|
+
If you use OpenAI, Anthropic Claude, or local Ollama models, you can still use the core sanitization engine independently:
|
|
447
|
+
|
|
448
|
+
```typescript
|
|
449
|
+
import { PrismaAIEngine } from 'prisma-safe-ai-query';
|
|
450
|
+
|
|
451
|
+
// Accepts raw LLM JSON string or parsed object
|
|
452
|
+
const query = PrismaAIEngine.parseDynamicQuery(rawAiResponse, {
|
|
453
|
+
blockList: ['password', 'secretKey']
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
console.log(query.targetTable); // 'user'
|
|
457
|
+
console.log(query.where); // { AND: [...] }
|
|
458
|
+
console.log(query.include); // Nested relations
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
---
|
|
462
|
+
|
|
463
|
+
## 📖 API Reference
|
|
464
|
+
|
|
465
|
+
### `new PrismaGeminiAgent(apiKey?: string, model?: string)`
|
|
466
|
+
Creates a new AI agent instance.
|
|
467
|
+
- `apiKey`: Optional. Defaults to `process.env.GEMINI_API_KEY`.
|
|
468
|
+
- `model`: Optional. Defaults to `process.env.GEMINI_MODEL` or `"gemini-3.1-flash-lite-preview"`.
|
|
469
|
+
|
|
470
|
+
### `agent.generateSafeQuery(prompt: string, config?: AgentConfig)`
|
|
471
|
+
Generates and sanitizes a complete Prisma query.
|
|
472
|
+
- `config.blockList`: Array of string patterns to block.
|
|
473
|
+
- `config.allowList`: Array of allowed models and fields (strict mode).
|
|
474
|
+
- `config.schemaPath`: Path to `schema.prisma`.
|
|
475
|
+
- `config.prismaClient`: PrismaClient instance for introspection.
|
|
476
|
+
- `config.autoIntrospect`: Boolean (default: `true`).
|
|
477
|
+
- `config.dbContext`: Custom text to supplement database context.
|
|
478
|
+
- `config.schemaContext`: Optional extracted schema metadata used to coerce filter values to Prisma field types.
|
|
479
|
+
- `config.naturalLanguageResponse`: Controls `execute()` summaries; defaults to `NATURAL_LANGUAGE_RESPONSE`, then `true`.
|
|
480
|
+
|
|
481
|
+
### `agent.execute(prompt: string, prismaClient: PrismaClient, config?: AgentConfig)`
|
|
482
|
+
Generates a safe query, executes it through the matching Prisma model's `findMany`, and returns `{ data, query, naturalLanguage? }`. It can use an explicit `select` or `include`, sorting, and pagination from the generated query. If neither `select` nor `include` is provided, it may build a field selection from introspected model metadata when access rules are configured. When the prompt requests related data but no relation was included, it can also attempt to fetch related records through detected foreign-key fields. Blocked or non-allowlisted data is filtered from the returned records.
|
|
483
|
+
|
|
484
|
+
When `naturalLanguageResponse` is disabled, `naturalLanguage` is omitted and no summary generation call is made.
|
|
485
|
+
|
|
486
|
+
### Standalone schema utilities
|
|
487
|
+
The package also exports `extractPrismaSchema`, `parsePrismaSchemaText`, `formatSchemaForAIContext`, `findPrismaSchemaPath`, and the `ExtractedSchema`, `ExtractedModel`, `ExtractedField`, and `ExtractedEnum` types. `PrismaAIEngine.filterBlockedData(data, config?)` is available to recursively filter records with the same security configuration.
|
|
488
|
+
|
|
489
|
+
### Returns `ParsedDynamicQuery`:
|
|
490
|
+
```typescript
|
|
491
|
+
interface ParsedDynamicQuery {
|
|
492
|
+
targetTable: string;
|
|
493
|
+
where: Record<string, any>;
|
|
494
|
+
include?: Record<string, any>;
|
|
495
|
+
select?: Record<string, any>;
|
|
496
|
+
orderBy?: any;
|
|
497
|
+
take?: number;
|
|
498
|
+
skip?: number;
|
|
499
|
+
}
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
---
|
|
503
|
+
|
|
504
|
+
## 📄 License
|
|
505
|
+
|
|
506
|
+
MIT © [Olugbenga Taiwo](https://github.com/gbengataiwo)
|