agent-sql 0.2.0 → 0.2.1
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/LICENSE +21 -0
- package/README.md +5 -12
- package/dist/index.d.mts +5 -26
- package/dist/index.mjs +76 -82
- package/package.json +7 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Chris Arderne
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -55,17 +55,14 @@ import { db } from "@/db";
|
|
|
55
55
|
// Only the tables listed will be permitted
|
|
56
56
|
// Joins can only use the FKs defined here
|
|
57
57
|
const schema = defineSchema({
|
|
58
|
-
user: { id },
|
|
59
|
-
msg: { userId: { user: "id" } },
|
|
58
|
+
user: { id: null },
|
|
59
|
+
msg: { userId: { ft: "user", fc: "id" } },
|
|
60
60
|
});
|
|
61
61
|
|
|
62
62
|
function makeSqlTool(userId: string) {
|
|
63
63
|
// Create a sanitiser function for this tenant
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
value: userId,
|
|
67
|
-
schema,
|
|
68
|
-
});
|
|
64
|
+
// Specify one or more column->value pairs that will be enforced
|
|
65
|
+
const agentSql = createAgentSql(schema, { "user.id": userId });
|
|
69
66
|
|
|
70
67
|
return tool({
|
|
71
68
|
description: "Run raw SQL against the DB",
|
|
@@ -95,11 +92,7 @@ import * as drizzleSchema from "@/db/schema";
|
|
|
95
92
|
const schema = defineSchemaFromDrizzle(drizzleSchema);
|
|
96
93
|
|
|
97
94
|
// The rest as before...
|
|
98
|
-
const agentSql = createAgentSql({
|
|
99
|
-
column: "user.id",
|
|
100
|
-
value: userId,
|
|
101
|
-
schema,
|
|
102
|
-
});
|
|
95
|
+
const agentSql = createAgentSql(schema, { "user.id": userId });
|
|
103
96
|
```
|
|
104
97
|
|
|
105
98
|
You can also exclude tables if you don't want agents to see them:
|
package/dist/index.d.mts
CHANGED
|
@@ -10,10 +10,9 @@ interface GuardCol {
|
|
|
10
10
|
type WhereGuard = GuardCol & {
|
|
11
11
|
value: GuardVal;
|
|
12
12
|
};
|
|
13
|
-
declare function addGuards(ast: SelectStatement, guard: WhereGuard, limit?: number): Result<SelectStatement>;
|
|
14
|
-
//#endregion
|
|
15
|
-
//#region src/namespec.d.ts
|
|
16
13
|
type OneOrTwoDots<S extends string> = S extends `${infer A}.${infer B}.${infer C}` ? A extends `${string}.${string}` ? never : B extends `${string}.${string}` ? never : C extends `${string}.${string}` ? never : S : S extends `${infer A}.${infer B}` ? A extends `${string}.${string}` ? never : B extends `${string}.${string}` ? never : S : never;
|
|
14
|
+
type SchemaGuardKeys<T> = { [Table in keyof T & string]: `${Table}.${keyof T[Table] & string}` }[keyof T & string];
|
|
15
|
+
declare function applyGuards(ast: SelectStatement, guards: WhereGuard[], limit?: number): Result<SelectStatement>;
|
|
17
16
|
//#endregion
|
|
18
17
|
//#region src/output.d.ts
|
|
19
18
|
declare function outputSql(ast: SelectStatement): string;
|
|
@@ -29,33 +28,13 @@ declare function agentSql<S extends string>(sql: string, column: S & OneOrTwoDot
|
|
|
29
28
|
schema?: Schema;
|
|
30
29
|
limit?: number;
|
|
31
30
|
}): string;
|
|
32
|
-
declare function createAgentSql<S extends
|
|
33
|
-
column: S & OneOrTwoDots<S>;
|
|
34
|
-
value: GuardVal;
|
|
35
|
-
schema?: Schema;
|
|
31
|
+
declare function createAgentSql<T extends Schema, S extends SchemaGuardKeys<T>>(schema: T, guards: Record<S, GuardVal>, opts: {
|
|
36
32
|
limit?: number;
|
|
37
33
|
throws: false;
|
|
38
34
|
}): (expr: string) => Result<string>;
|
|
39
|
-
declare function createAgentSql<S extends
|
|
40
|
-
column: S & OneOrTwoDots<S>;
|
|
41
|
-
value: GuardVal;
|
|
42
|
-
schema?: Schema;
|
|
35
|
+
declare function createAgentSql<T extends Schema, S extends SchemaGuardKeys<T>>(schema: T, guards: Record<S, GuardVal>, opts?: {
|
|
43
36
|
limit?: number;
|
|
44
37
|
throws?: true;
|
|
45
38
|
}): (expr: string) => string;
|
|
46
|
-
declare function createAgentSql<S extends string>(_: {
|
|
47
|
-
column: S & OneOrTwoDots<S>;
|
|
48
|
-
value?: undefined;
|
|
49
|
-
schema?: Schema;
|
|
50
|
-
limit?: number;
|
|
51
|
-
throws: false;
|
|
52
|
-
}): (guardVal: GuardVal) => (expr: string) => Result<string>;
|
|
53
|
-
declare function createAgentSql<S extends string>(_: {
|
|
54
|
-
column: S & OneOrTwoDots<S>;
|
|
55
|
-
value?: undefined;
|
|
56
|
-
schema?: Schema;
|
|
57
|
-
limit?: number;
|
|
58
|
-
throws?: true;
|
|
59
|
-
}): (guardVal: GuardVal) => (expr: string) => string;
|
|
60
39
|
//#endregion
|
|
61
|
-
export { agentSql, createAgentSql, defineSchema, outputSql, parseSql,
|
|
40
|
+
export { agentSql, createAgentSql, defineSchema, outputSql, parseSql, applyGuards as sanitiseSql };
|
package/dist/index.mjs
CHANGED
|
@@ -247,54 +247,86 @@ function returnOrThrow(result, throws) {
|
|
|
247
247
|
//#endregion
|
|
248
248
|
//#region src/guard.ts
|
|
249
249
|
const DEFAULT_LIMIT = 1e4;
|
|
250
|
-
function
|
|
251
|
-
const ast2 = addWhereGuard(ast,
|
|
250
|
+
function applyGuards(ast, guards, limit = DEFAULT_LIMIT) {
|
|
251
|
+
const ast2 = addWhereGuard(ast, guards);
|
|
252
252
|
if (!ast2.ok) return ast2;
|
|
253
253
|
return addLimitGuard(ast2.data, limit);
|
|
254
254
|
}
|
|
255
|
-
function
|
|
256
|
-
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const newClause = {
|
|
264
|
-
type: "where_comparison",
|
|
265
|
-
operator: "=",
|
|
266
|
-
left: {
|
|
267
|
-
type: "where_value",
|
|
268
|
-
kind: "column_ref",
|
|
269
|
-
ref: {
|
|
270
|
-
type: "column_ref",
|
|
271
|
-
schema,
|
|
272
|
-
table,
|
|
273
|
-
name: column
|
|
274
|
-
}
|
|
275
|
-
},
|
|
276
|
-
right: typeof value === "string" ? {
|
|
277
|
-
type: "where_value",
|
|
278
|
-
kind: "string",
|
|
279
|
-
value
|
|
280
|
-
} : {
|
|
281
|
-
type: "where_value",
|
|
282
|
-
kind: "integer",
|
|
255
|
+
function resolveGuards(guards) {
|
|
256
|
+
if (guards.length === 0) return Err(new AgentSqlError("At least one guard must be provided."));
|
|
257
|
+
const result = [];
|
|
258
|
+
for (const [column, value] of Object.entries(guards)) {
|
|
259
|
+
const guardCol = resolveSingleGuardCol(column);
|
|
260
|
+
if (!guardCol.ok) return guardCol;
|
|
261
|
+
result.push({
|
|
262
|
+
...guardCol.data,
|
|
283
263
|
value
|
|
284
|
-
}
|
|
285
|
-
}
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
return Ok(result);
|
|
267
|
+
}
|
|
268
|
+
function resolveSingleGuardCol(column) {
|
|
269
|
+
const [a, b, c] = column.split(".");
|
|
270
|
+
if (a === void 0 || b === void 0) return Err(new AgentSqlError(`Malformed column string: '${column}'. Pass 'table.column'.`));
|
|
271
|
+
if (c === void 0) return Ok({
|
|
272
|
+
table: a,
|
|
273
|
+
column: b
|
|
274
|
+
});
|
|
275
|
+
return Err(new AgentSqlError("Specifying guard as schema.table.name not yet supported"));
|
|
276
|
+
}
|
|
277
|
+
function addWhereGuard(ast, guards) {
|
|
278
|
+
for (const guard of guards) {
|
|
279
|
+
const tableRef = {
|
|
280
|
+
type: "table_ref",
|
|
281
|
+
schema: guard.schema,
|
|
282
|
+
name: guard.table
|
|
283
|
+
};
|
|
284
|
+
if (!checkIfTableRefExists(ast, tableRef)) return Err(new SanitiseError(`The table '${handleTableRef(tableRef)}' must appear in the FROM or JOIN clauses.`));
|
|
285
|
+
}
|
|
286
|
+
const [first, ...rest] = guards.map((guard) => {
|
|
287
|
+
const { schema, table, column, value } = guard;
|
|
288
|
+
return {
|
|
289
|
+
type: "where_comparison",
|
|
290
|
+
operator: "=",
|
|
291
|
+
left: {
|
|
292
|
+
type: "where_value",
|
|
293
|
+
kind: "column_ref",
|
|
294
|
+
ref: {
|
|
295
|
+
type: "column_ref",
|
|
296
|
+
schema,
|
|
297
|
+
table,
|
|
298
|
+
name: column
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
right: typeof value === "string" ? {
|
|
302
|
+
type: "where_value",
|
|
303
|
+
kind: "string",
|
|
304
|
+
value
|
|
305
|
+
} : {
|
|
306
|
+
type: "where_value",
|
|
307
|
+
kind: "integer",
|
|
308
|
+
value
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
});
|
|
312
|
+
if (!first) return Err(new AgentSqlError("No guards were provided"));
|
|
313
|
+
const combined = rest.reduce((acc, clause) => ({
|
|
314
|
+
type: "where_and",
|
|
315
|
+
left: acc,
|
|
316
|
+
right: clause
|
|
317
|
+
}), first);
|
|
286
318
|
return Ok({
|
|
287
319
|
...ast,
|
|
288
320
|
where: ast.where ? {
|
|
289
321
|
type: "where_root",
|
|
290
322
|
inner: {
|
|
291
323
|
type: "where_and",
|
|
292
|
-
left:
|
|
324
|
+
left: combined,
|
|
293
325
|
right: ast.where.inner
|
|
294
326
|
}
|
|
295
327
|
} : {
|
|
296
328
|
type: "where_root",
|
|
297
|
-
inner:
|
|
329
|
+
inner: combined
|
|
298
330
|
}
|
|
299
331
|
});
|
|
300
332
|
}
|
|
@@ -358,21 +390,6 @@ function getJoinTableRef(joinTableName, left, right) {
|
|
|
358
390
|
};
|
|
359
391
|
}
|
|
360
392
|
//#endregion
|
|
361
|
-
//#region src/namespec.ts
|
|
362
|
-
function getQualifiedColumnFromString(column) {
|
|
363
|
-
const [a, b, c] = column.split(".");
|
|
364
|
-
if (a === void 0 || b === void 0) throw new AgentSqlError(`Malformed column string: '${column}'. Pass 'table.column'.`);
|
|
365
|
-
if (c === void 0) return Ok({
|
|
366
|
-
table: a,
|
|
367
|
-
column: b
|
|
368
|
-
});
|
|
369
|
-
return Ok({
|
|
370
|
-
schema: a,
|
|
371
|
-
table: b,
|
|
372
|
-
column: c
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
//#endregion
|
|
376
393
|
//#region src/sql.ohm-bundle.js
|
|
377
394
|
const result = makeRecipe([
|
|
378
395
|
"grammar",
|
|
@@ -6433,60 +6450,37 @@ function parseSql(expr) {
|
|
|
6433
6450
|
//#region src/index.ts
|
|
6434
6451
|
function agentSql(sql, column, value, { schema, limit } = {}) {
|
|
6435
6452
|
return privateAgentSql(sql, {
|
|
6436
|
-
column,
|
|
6437
|
-
value,
|
|
6453
|
+
guards: { [column]: value },
|
|
6438
6454
|
schema,
|
|
6439
6455
|
limit,
|
|
6440
6456
|
throws: true
|
|
6441
6457
|
});
|
|
6442
6458
|
}
|
|
6443
|
-
function createAgentSql(
|
|
6444
|
-
|
|
6445
|
-
|
|
6446
|
-
value,
|
|
6459
|
+
function createAgentSql(schema, guards, { limit, throws = true } = {}) {
|
|
6460
|
+
return (expr) => throws ? privateAgentSql(expr, {
|
|
6461
|
+
guards,
|
|
6447
6462
|
schema,
|
|
6448
6463
|
limit,
|
|
6449
6464
|
throws
|
|
6450
6465
|
}) : privateAgentSql(expr, {
|
|
6451
|
-
|
|
6452
|
-
value,
|
|
6466
|
+
guards,
|
|
6453
6467
|
schema,
|
|
6454
6468
|
limit,
|
|
6455
6469
|
throws
|
|
6456
6470
|
});
|
|
6457
|
-
function factory(guardVal) {
|
|
6458
|
-
return throws ? createAgentSql({
|
|
6459
|
-
column,
|
|
6460
|
-
schema,
|
|
6461
|
-
value: guardVal,
|
|
6462
|
-
limit,
|
|
6463
|
-
throws
|
|
6464
|
-
}) : createAgentSql({
|
|
6465
|
-
column,
|
|
6466
|
-
schema,
|
|
6467
|
-
value: guardVal,
|
|
6468
|
-
limit,
|
|
6469
|
-
throws
|
|
6470
|
-
});
|
|
6471
|
-
}
|
|
6472
|
-
return factory;
|
|
6473
6471
|
}
|
|
6474
|
-
function privateAgentSql(sql, {
|
|
6475
|
-
const
|
|
6476
|
-
if (!
|
|
6472
|
+
function privateAgentSql(sql, { guards: guardsRaw, schema, limit, throws }) {
|
|
6473
|
+
const guards = resolveGuards(guardsRaw);
|
|
6474
|
+
if (!guards.ok) throw guards.error;
|
|
6477
6475
|
const ast = parseSql(sql);
|
|
6478
6476
|
if (!ast.ok) return returnOrThrow(ast, throws);
|
|
6479
6477
|
const ast2 = checkJoins(ast.data, schema);
|
|
6480
6478
|
if (!ast2.ok) return returnOrThrow(ast2, throws);
|
|
6481
|
-
const
|
|
6482
|
-
...guardCol.data,
|
|
6483
|
-
value
|
|
6484
|
-
};
|
|
6485
|
-
const san = addGuards(ast2.data, where, limit);
|
|
6479
|
+
const san = applyGuards(ast2.data, guards.data, limit);
|
|
6486
6480
|
if (!san.ok) return returnOrThrow(san, throws);
|
|
6487
6481
|
const res = outputSql(san.data);
|
|
6488
6482
|
if (throws) return res;
|
|
6489
6483
|
return Ok(res);
|
|
6490
6484
|
}
|
|
6491
6485
|
//#endregion
|
|
6492
|
-
export { agentSql, createAgentSql, defineSchema, outputSql, parseSql,
|
|
6486
|
+
export { agentSql, createAgentSql, defineSchema, outputSql, parseSql, applyGuards as sanitiseSql };
|
package/package.json
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-sql",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "A starter for creating a TypeScript package.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"agent",
|
|
7
|
+
"ai",
|
|
8
|
+
"postgres",
|
|
9
|
+
"sql"
|
|
10
|
+
],
|
|
5
11
|
"homepage": "https://github.com/carderne/agent-sql#readme",
|
|
6
12
|
"bugs": {
|
|
7
13
|
"url": "https://github.com/carderne/agent-sql/issues"
|