agent-sql 0.3.3 → 0.3.5

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 CHANGED
@@ -1,4 +1,7 @@
1
- # agent-sql
1
+ <p align="center">
2
+ <img src="https://cdn.jsdelivr.net/gh/carderne/agent-sql@main/docs/agent-sql.png" width="40" />
3
+ </p>
4
+ <h1 align="center">agent-sql</h1>
2
5
 
3
6
  Sanitise agent-written SQL for multi-tenant DBs.
4
7
 
@@ -19,6 +22,8 @@ agent-sql works by fully parsing the supplied SQL query into an AST and transfor
19
22
  - **`JOIN`s added:** if needed to reach the guard tenant tables (save on tokens).
20
23
  - **No sneaky joins:** no `join secrets on true`. We have your back.
21
24
 
25
+ I plan to support inserts, updates, CTEs, subqueries once I can convincingly make them safe.
26
+
22
27
  ## Quickstart
23
28
 
24
29
  ```bash
@@ -28,7 +33,7 @@ npm install agent-sql
28
33
  ```ts
29
34
  import { agentSql } from "agent-sql";
30
35
 
31
- const sql = agentSql(`SELECT * FROM msg`, "msg.tenant_id", 123);
36
+ const sql = agentSql("SELECT * FROM msg", "msg.tenant_id", 123);
32
37
 
33
38
  console.log(sql);
34
39
  // SELECT *
@@ -39,30 +44,81 @@ console.log(sql);
39
44
 
40
45
  ## Usage
41
46
 
42
- ### Define once, use many times
47
+ ### Define a schema
43
48
 
44
- The simple approach above is enough to get started.
45
- But since no schema is provided, `JOIN`s will be blocked.
46
- A schema can be passed to `agentSql`, but typically you'll want to set it up once and re-use.
49
+ In the simple example above, all `JOIN`s will be blocked.
50
+ For agent-sql to know what joins and tables permit, you need to define a schema.
51
+ Heads up: if you use Drizzle, you can just [use your Drizzle schema](#integration-with-ai-sdk-and-drizzle).
47
52
 
48
53
  ```ts
49
54
  import { createAgentSql, defineSchema } from "agent-sql";
50
- import { tool } from "ai";
51
- import { sql } from "drizzle-orm";
52
- import { db } from "@/db";
53
55
 
54
56
  // Define your schema.
55
57
  // Only the tables listed will be permitted
56
58
  // Joins can only use the FKs defined here
57
59
  const schema = defineSchema({
58
- user: { id: null },
59
- msg: { userId: { ft: "user", fc: "id" } },
60
+ tenant: { id: null },
61
+ msg: { tenant_id: { ft: "tenant", fc: "id" } },
60
62
  });
61
63
 
62
- function makeSqlTool(userId: string) {
64
+ // Use your schema from above
65
+ // Specify 1+ column->value pairs that will be enforced
66
+ const agentSql = createAgentSql(schema, { "tenant.id": 123 });
67
+
68
+ // Now use it
69
+ const sql = agentSql("SELECT * FROM msg");
70
+ ```
71
+
72
+ Outputs:
73
+
74
+ ```sql
75
+ SELECT
76
+ msg.* -- qualify the *
77
+ FROM msg
78
+ INNER JOIN tenant -- add the needed join for the guard
79
+ ON tenant.id = msg.tenant_id -- use the schema to join correctly
80
+ WHERE tenant.id = 123 -- apply the guard
81
+ LIMIT 10000 -- limit the rows
82
+ ```
83
+
84
+ ### Bad stuff is blocked
85
+
86
+ The following query will be blocked (many times over).
87
+
88
+ ```sql
89
+ SELECT
90
+ sneaky_func('./bad_file') -- won't pass whitelist
91
+ FROM secret
92
+ JOIN random -- not an approved table
93
+ ON random.id = secret.id -- not an approved FK pair
94
+ JOIN danger -- disconnected from join graph
95
+ ON true -- not allowed
96
+ WHERE true -- won't trick anyone
97
+ ```
98
+
99
+ ### Integration with AI SDK and Drizzle
100
+
101
+ If you're using Drizzle, you can skip the schema step and use the one you already have!
102
+
103
+ Just pass it through, and `agentSql` will respect your schema.
104
+
105
+ ```ts
106
+ import { tool } from "ai";
107
+ import { sql } from "drizzle-orm";
108
+
109
+ import { createAgentSql } from "agent-sql";
110
+ import { defineSchemaFromDrizzle } from "agent-sql/drizzle";
111
+
112
+ import { db } from "@/db";
113
+ import * as drizzleSchema from "@/db/schema";
114
+
115
+ // No need to re-enter your schema, we'll pull it in from Drizzle
116
+ const schema = defineSchemaFromDrizzle(drizzleSchema);
117
+
118
+ function makeSqlTool(tenantId: string) {
63
119
  // Create a sanitiser function for this tenant
64
120
  // Specify one or more column->value pairs that will be enforced
65
- const agentSql = createAgentSql(schema, { "user.id": userId });
121
+ const agentSql = createAgentSql(schema, { "tenant.id": tenantId });
66
122
 
67
123
  return tool({
68
124
  description: "Run raw SQL against the DB",
@@ -70,34 +126,22 @@ function makeSqlTool(userId: string) {
70
126
  execute: async ({ query }) => {
71
127
  // The LLM can pass any query it likes, we'll sanitise it if possible
72
128
  // and return helpful error messages if not
73
- const sanitised = agentSql(query);
129
+ const sql = agentSql(query);
74
130
  // Now we can throw that straight at the db and be confident it'll only
75
131
  // return data from the specified tenant
76
- return db.execute(sql.raw(sanitised));
132
+ return db.execute(sql.raw(sql));
77
133
  },
78
134
  });
79
135
  }
80
136
  ```
81
137
 
82
- ### It works with Drizzle
83
-
84
- If you're using Drizzle, you can skip the schema step and use the one you already have!
138
+ ### If you don't want your whole Drizzle schema available
85
139
 
86
- Just pass it through, and `agentSql` will respect your schema.
140
+ You can also exclude tables if you don't want agents to see them:
87
141
 
88
142
  ```ts
89
143
  import { defineSchemaFromDrizzle } from "agent-sql/drizzle";
90
- import * as drizzleSchema from "@/db/schema";
91
-
92
- const schema = defineSchemaFromDrizzle(drizzleSchema);
93
144
 
94
- // The rest as before...
95
- const agentSql = createAgentSql(schema, { "user.id": userId });
96
- ```
97
-
98
- You can also exclude tables if you don't want agents to see them:
99
-
100
- ```ts
101
145
  const schema = defineSchemaFromDrizzle(drizzleSchema, {
102
146
  exclude: ["api_keys"],
103
147
  });
package/dist/index.mjs CHANGED
@@ -7451,7 +7451,10 @@ semantics.addOperation("toAST()", {
7451
7451
  });
7452
7452
  function parseSql(expr) {
7453
7453
  const matchResult = result.match(expr);
7454
- if (matchResult.failed()) return Err(new ParseError(matchResult.message));
7454
+ if (matchResult.failed()) {
7455
+ const [message] = matchResult.message.split("\nExpected");
7456
+ return Err(new ParseError(`Got invalid SQL. Some language features are deliberately disabled. Re-write your query without them.\n${message}`));
7457
+ }
7455
7458
  try {
7456
7459
  return Ok(semantics(matchResult).toAST());
7457
7460
  } catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-sql",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "A starter for creating a TypeScript package.",
5
5
  "keywords": [
6
6
  "agent",
@@ -34,6 +34,7 @@
34
34
  "build": "vp pack",
35
35
  "dev": "vp pack --watch",
36
36
  "test": "vp test",
37
+ "coverage": "vp test --coverage",
37
38
  "check": "vp check",
38
39
  "prepublishOnly": "vp run build",
39
40
  "ohm": "ohm generateBundles --withTypes --esm 'src/sql.ohm'",
@@ -46,11 +47,10 @@
46
47
  "@electric-sql/pglite": "^0.4.1",
47
48
  "@ohm-js/cli": "^2.0.1",
48
49
  "@types/node": "^25.5.0",
49
- "@types/pg": "^8.18.0",
50
50
  "@typescript/native-preview": "7.0.0-dev.20260316.1",
51
+ "@vitest/coverage-v8": "^4.1.0",
51
52
  "bumpp": "^11.0.1",
52
53
  "drizzle-orm": "^0.45.1",
53
- "pg": "^8.20.0",
54
54
  "tsx": "^4.21.0",
55
55
  "typescript": "^5.9.3",
56
56
  "vite-plus": "^0.1.11"
@@ -69,5 +69,6 @@
69
69
  "vite": "npm:@voidzero-dev/vite-plus-core@latest",
70
70
  "vitest": "npm:@voidzero-dev/vite-plus-test@latest"
71
71
  }
72
- }
72
+ },
73
+ "logo": "https://cdn.jsdelivr.net/gh/carderne/agent-sql@main/docs/agent-sql.png"
73
74
  }