agent-sql 0.1.2 → 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 +58 -21
- package/dist/drizzle.d.mts +22 -0
- package/dist/drizzle.mjs +66 -0
- package/dist/index.d.mts +23 -305
- package/dist/index.mjs +197 -157
- package/dist/joins-Cu_0yAgN.d.mts +266 -0
- package/package.json +17 -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
|
@@ -8,10 +8,14 @@ agent-sql works by fully parsing the supplied SQL query into an AST.
|
|
|
8
8
|
The grammar ONLY accepts `SELECT` statements. Anything else is an error.
|
|
9
9
|
CTEs and other complex things that we aren't confident of securing: error.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
and
|
|
11
|
+
It ensures that that the needed tenant table is somewhere in the query,
|
|
12
|
+
and adds a `WHERE` clause ensuring that only values from the supplied ID are returned.
|
|
13
|
+
Then it checks that the tables and `JOIN`s follow the schema, preventing sneaky joins.
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
Finally, we throw in a `LIMIT` clause (configurable) to prevent accidental LLM denial-of-service.
|
|
16
|
+
|
|
17
|
+
Apparently this is how [Trigger.dev does it](https://x.com/mattaitken/status/2033928542975639785).
|
|
18
|
+
And [Cloudflare](https://x.com/thomas_ankcorn/status/2033931057133748330).
|
|
15
19
|
|
|
16
20
|
## Quickstart
|
|
17
21
|
|
|
@@ -20,34 +24,45 @@ npm install agent-sql
|
|
|
20
24
|
```
|
|
21
25
|
|
|
22
26
|
```ts
|
|
23
|
-
import {
|
|
27
|
+
import { agentSql } from "agent-sql";
|
|
24
28
|
|
|
25
|
-
const sql =
|
|
26
|
-
tables: { users: {} },
|
|
27
|
-
where: { table: "users", col: "tenant_id", value: "acme" },
|
|
28
|
-
});
|
|
29
|
+
const sql = agentSql(`SELECT * FROM msg`, "msg.user_id", 123);
|
|
29
30
|
|
|
30
31
|
console.log(sql);
|
|
31
|
-
// SELECT
|
|
32
|
-
// FROM
|
|
33
|
-
// WHERE
|
|
34
|
-
// LIMIT
|
|
32
|
+
// SELECT *
|
|
33
|
+
// FROM msg
|
|
34
|
+
// WHERE msg.user_id = 123
|
|
35
|
+
// LIMIT 10000
|
|
35
36
|
```
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
`agent-sql` parses the SQL, enforces a mandatory equality filter on the given column as the outermost `AND` condition (so it cannot be short-circuited by agent-supplied `OR` clauses), and returns the sanitised SQL string.
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
### Define once, use many times
|
|
43
|
+
|
|
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.
|
|
38
47
|
|
|
39
48
|
```ts
|
|
40
|
-
import {
|
|
49
|
+
import { createAgentSql, defineSchema } from "agent-sql";
|
|
41
50
|
import { tool } from "ai";
|
|
42
51
|
import { sql } from "drizzle-orm";
|
|
43
52
|
import { db } from "@/db";
|
|
44
53
|
|
|
45
|
-
|
|
54
|
+
// Define your schema.
|
|
55
|
+
// Only the tables listed will be permitted
|
|
56
|
+
// Joins can only use the FKs defined here
|
|
57
|
+
const schema = defineSchema({
|
|
58
|
+
user: { id: null },
|
|
59
|
+
msg: { userId: { ft: "user", fc: "id" } },
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
function makeSqlTool(userId: string) {
|
|
46
63
|
// Create a sanitiser function for this tenant
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
where: { table: "org", col: "id", value: orgId },
|
|
50
|
-
});
|
|
64
|
+
// Specify one or more column->value pairs that will be enforced
|
|
65
|
+
const agentSql = createAgentSql(schema, { "user.id": userId });
|
|
51
66
|
|
|
52
67
|
return tool({
|
|
53
68
|
description: "Run raw SQL against the DB",
|
|
@@ -55,7 +70,7 @@ function makeSqlTool(orgId: string) {
|
|
|
55
70
|
execute: async ({ query }) => {
|
|
56
71
|
// The LLM can pass any query it likes, we'll sanitise it if possible
|
|
57
72
|
// and return helpful error messages if not
|
|
58
|
-
const sanitised =
|
|
73
|
+
const sanitised = agentSql(query);
|
|
59
74
|
// Now we can throw that straight at the db and be confident it'll only
|
|
60
75
|
// return data from the specified tenant
|
|
61
76
|
return db.execute(sql.raw(sanitised));
|
|
@@ -64,7 +79,29 @@ function makeSqlTool(orgId: string) {
|
|
|
64
79
|
}
|
|
65
80
|
```
|
|
66
81
|
|
|
67
|
-
|
|
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!
|
|
85
|
+
|
|
86
|
+
Just pass it through, and `agentSql` will respect your schema.
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { defineSchemaFromDrizzle } from "agent-sql/drizzle";
|
|
90
|
+
import * as drizzleSchema from "@/db/schema";
|
|
91
|
+
|
|
92
|
+
const schema = defineSchemaFromDrizzle(drizzleSchema);
|
|
93
|
+
|
|
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
|
+
const schema = defineSchemaFromDrizzle(drizzleSchema, {
|
|
102
|
+
exclude: ["api_keys"],
|
|
103
|
+
});
|
|
104
|
+
```
|
|
68
105
|
|
|
69
106
|
## Development
|
|
70
107
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { t as Schema } from "./joins-Cu_0yAgN.mjs";
|
|
2
|
+
import { Table, TableConfig } from "drizzle-orm";
|
|
3
|
+
|
|
4
|
+
//#region src/drizzle.d.ts
|
|
5
|
+
/** Extract all table name strings from a drizzle schema module. */
|
|
6
|
+
type TableNames<T> = { [K in keyof T]: T[K] extends Table<infer C extends TableConfig> ? C["name"] : never }[keyof T];
|
|
7
|
+
/**
|
|
8
|
+
* Build an agent-sql schema from a drizzle-orm schema module.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* ```ts
|
|
12
|
+
* import * as drizzleSchema from "./schema";
|
|
13
|
+
* const schema = defineSchemaFromDrizzle(drizzleSchema);
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
declare function defineSchemaFromDrizzle<T extends Record<string, unknown>>(drizzleSchema: T, {
|
|
17
|
+
exclude
|
|
18
|
+
}?: {
|
|
19
|
+
exclude?: TableNames<T>[];
|
|
20
|
+
}): Schema;
|
|
21
|
+
//#endregion
|
|
22
|
+
export { defineSchemaFromDrizzle };
|
package/dist/drizzle.mjs
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { getTableName, isTable } from "drizzle-orm";
|
|
2
|
+
//#region src/drizzle.ts
|
|
3
|
+
/**
|
|
4
|
+
* Convert a camelCase string to snake_case.
|
|
5
|
+
* This matches drizzle-orm's internal conversion for columns without explicit DB names.
|
|
6
|
+
*/
|
|
7
|
+
function camelToSnake(input) {
|
|
8
|
+
return (input.replace(/['\u2019]/g, "").match(/[\da-z]+|[A-Z]+(?![a-z])|[A-Z][\da-z]+/g) ?? []).map((w) => w.toLowerCase()).join("_");
|
|
9
|
+
}
|
|
10
|
+
/** Get the DB column name, applying camelCase → snake_case when the key was used as name. */
|
|
11
|
+
function getDbColumnName(col) {
|
|
12
|
+
return col.keyAsName ? camelToSnake(col.name) : col.name;
|
|
13
|
+
}
|
|
14
|
+
const ColumnsSymbol = Symbol.for("drizzle:Columns");
|
|
15
|
+
const InlineForeignKeysSymbol = Symbol.for("drizzle:PgInlineForeignKeys");
|
|
16
|
+
/**
|
|
17
|
+
* Build an agent-sql schema from a drizzle-orm schema module.
|
|
18
|
+
*
|
|
19
|
+
* Usage:
|
|
20
|
+
* ```ts
|
|
21
|
+
* import * as drizzleSchema from "./schema";
|
|
22
|
+
* const schema = defineSchemaFromDrizzle(drizzleSchema);
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
function defineSchemaFromDrizzle(drizzleSchema, { exclude } = {}) {
|
|
26
|
+
const schema = {};
|
|
27
|
+
const excluded = new Set(exclude);
|
|
28
|
+
const tables = [];
|
|
29
|
+
for (const value of Object.values(drizzleSchema)) if (isTable(value) && !excluded.has(getTableName(value))) tables.push(value);
|
|
30
|
+
const fkMap = /* @__PURE__ */ new Map();
|
|
31
|
+
for (const table of tables) {
|
|
32
|
+
const tableName = getTableName(table);
|
|
33
|
+
const fks = table[InlineForeignKeysSymbol] ?? [];
|
|
34
|
+
const colFks = /* @__PURE__ */ new Map();
|
|
35
|
+
for (const fk of fks) {
|
|
36
|
+
const ref = fk.reference();
|
|
37
|
+
const fromCol = ref.columns[0];
|
|
38
|
+
const foreignTableName = getTableName(ref.foreignTable);
|
|
39
|
+
if (excluded.has(foreignTableName)) colFks.set(getDbColumnName(fromCol), "excluded");
|
|
40
|
+
else {
|
|
41
|
+
const toCol = ref.foreignColumns[0];
|
|
42
|
+
colFks.set(getDbColumnName(fromCol), {
|
|
43
|
+
ft: foreignTableName,
|
|
44
|
+
fc: getDbColumnName(toCol)
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
fkMap.set(tableName, colFks);
|
|
49
|
+
}
|
|
50
|
+
for (const table of tables) {
|
|
51
|
+
const tableName = getTableName(table);
|
|
52
|
+
const columns = Object.values(table[ColumnsSymbol]);
|
|
53
|
+
const colFks = fkMap.get(tableName);
|
|
54
|
+
const tableSchema = {};
|
|
55
|
+
for (const col of columns) {
|
|
56
|
+
const dbName = getDbColumnName(col);
|
|
57
|
+
const fk = colFks.get(dbName);
|
|
58
|
+
if (fk === "excluded") continue;
|
|
59
|
+
tableSchema[dbName] = fk ?? null;
|
|
60
|
+
}
|
|
61
|
+
schema[tableName] = tableSchema;
|
|
62
|
+
}
|
|
63
|
+
return schema;
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
export { defineSchemaFromDrizzle };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,267 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
from: SelectFrom;
|
|
7
|
-
joins: JoinClause[];
|
|
8
|
-
where: WhereRoot | null;
|
|
9
|
-
groupBy: GroupByClause | null;
|
|
10
|
-
having: HavingClause | null;
|
|
11
|
-
orderBy: OrderByClause | null;
|
|
12
|
-
limit: LimitClause | null;
|
|
13
|
-
offset: OffsetClause | null;
|
|
14
|
-
}
|
|
15
|
-
interface Distinct {
|
|
16
|
-
readonly type: "distinct";
|
|
17
|
-
}
|
|
18
|
-
/** PostgreSQL: DISTINCT ON (col1, col2, ...) */
|
|
19
|
-
interface DistinctOn {
|
|
20
|
-
readonly type: "distinct_on";
|
|
21
|
-
columns: WhereValue[];
|
|
22
|
-
}
|
|
23
|
-
interface GroupByClause {
|
|
24
|
-
readonly type: "group_by";
|
|
25
|
-
items: WhereValue[];
|
|
26
|
-
}
|
|
27
|
-
interface HavingClause {
|
|
28
|
-
readonly type: "having";
|
|
29
|
-
expr: WhereExpr;
|
|
30
|
-
}
|
|
31
|
-
interface OrderByClause {
|
|
32
|
-
readonly type: "order_by";
|
|
33
|
-
items: OrderByItem[];
|
|
34
|
-
}
|
|
35
|
-
type SortDirection = "asc" | "desc";
|
|
36
|
-
type NullsOrder = "nulls_first" | "nulls_last";
|
|
37
|
-
interface OrderByItem {
|
|
38
|
-
readonly type: "order_by_item";
|
|
39
|
-
expr: WhereValue;
|
|
40
|
-
direction?: SortDirection;
|
|
41
|
-
nulls?: NullsOrder;
|
|
42
|
-
}
|
|
43
|
-
interface OffsetClause {
|
|
44
|
-
readonly type: "offset";
|
|
45
|
-
value: number;
|
|
46
|
-
}
|
|
47
|
-
type JoinType = "inner" | "inner_outer" | "left" | "left_outer" | "right" | "right_outer" | "full" | "full_outer" | "cross" | "natural";
|
|
48
|
-
type JoinCondition = {
|
|
49
|
-
readonly type: "join_on";
|
|
50
|
-
expr: WhereExpr;
|
|
51
|
-
} | {
|
|
52
|
-
readonly type: "join_using";
|
|
53
|
-
columns: string[];
|
|
54
|
-
};
|
|
55
|
-
interface JoinClause {
|
|
56
|
-
readonly type: "join";
|
|
57
|
-
joinType: JoinType;
|
|
58
|
-
table: TableRef;
|
|
59
|
-
condition: JoinCondition | null;
|
|
60
|
-
}
|
|
61
|
-
type SelectFrom = {
|
|
62
|
-
readonly type: "select_from";
|
|
63
|
-
table: TableRef;
|
|
64
|
-
};
|
|
65
|
-
type LimitClause = {
|
|
66
|
-
readonly type: "limit";
|
|
67
|
-
value: number;
|
|
68
|
-
};
|
|
69
|
-
type WhereRoot = {
|
|
70
|
-
readonly type: "where_root";
|
|
71
|
-
inner: WhereExpr;
|
|
72
|
-
};
|
|
73
|
-
type WhereExpr = WhereAnd | WhereOr | WhereNot | WhereComparison | WhereIsNull | WhereIsBool | WhereBetween | WhereIn | WhereLike | WhereTsMatch;
|
|
74
|
-
interface WhereAnd {
|
|
75
|
-
readonly type: "where_and";
|
|
76
|
-
left: WhereExpr;
|
|
77
|
-
right: WhereExpr;
|
|
78
|
-
}
|
|
79
|
-
interface WhereOr {
|
|
80
|
-
readonly type: "where_or";
|
|
81
|
-
left: WhereExpr;
|
|
82
|
-
right: WhereExpr;
|
|
83
|
-
}
|
|
84
|
-
type ComparisonOperator = "=" | "<>" | "!=" | "<" | ">" | "<=" | ">=";
|
|
85
|
-
interface WhereNot {
|
|
86
|
-
readonly type: "where_not";
|
|
87
|
-
expr: WhereExpr;
|
|
88
|
-
}
|
|
89
|
-
interface WhereIsNull {
|
|
90
|
-
readonly type: "where_is_null";
|
|
91
|
-
not: boolean;
|
|
92
|
-
expr: WhereValue;
|
|
93
|
-
}
|
|
94
|
-
type IsBoolTarget = boolean | "unknown";
|
|
95
|
-
interface WhereIsBool {
|
|
96
|
-
readonly type: "where_is_bool";
|
|
97
|
-
not: boolean;
|
|
98
|
-
expr: WhereValue;
|
|
99
|
-
target: IsBoolTarget;
|
|
100
|
-
}
|
|
101
|
-
interface WhereBetween {
|
|
102
|
-
readonly type: "where_between";
|
|
103
|
-
not: boolean;
|
|
104
|
-
expr: WhereValue;
|
|
105
|
-
low: WhereValue;
|
|
106
|
-
high: WhereValue;
|
|
107
|
-
}
|
|
108
|
-
interface WhereIn {
|
|
109
|
-
readonly type: "where_in";
|
|
110
|
-
not: boolean;
|
|
111
|
-
expr: WhereValue;
|
|
112
|
-
list: WhereValue[];
|
|
113
|
-
}
|
|
114
|
-
/** "ilike" is PostgreSQL-specific (case-insensitive LIKE) */
|
|
115
|
-
type LikeOp = "like" | "ilike";
|
|
116
|
-
interface WhereLike {
|
|
117
|
-
readonly type: "where_like";
|
|
118
|
-
not: boolean;
|
|
119
|
-
op: LikeOp;
|
|
120
|
-
expr: WhereValue;
|
|
121
|
-
pattern: WhereValue;
|
|
122
|
-
}
|
|
123
|
-
/** PostgreSQL: JSONB operators */
|
|
124
|
-
type JsonbOp = "->" | "->>" | "#>" | "#>>" | "?" | "?|" | "?&" | "@>";
|
|
125
|
-
/** PostgreSQL: JSONB binary operator expression */
|
|
126
|
-
interface WhereJsonbOp {
|
|
127
|
-
readonly type: "where_jsonb_op";
|
|
128
|
-
op: JsonbOp;
|
|
129
|
-
left: WhereValue;
|
|
130
|
-
right: WhereValue;
|
|
131
|
-
}
|
|
132
|
-
/** PostgreSQL: text search match (@@) */
|
|
133
|
-
interface WhereTsMatch {
|
|
134
|
-
readonly type: "where_ts_match";
|
|
135
|
-
left: WhereValue;
|
|
136
|
-
right: WhereValue;
|
|
137
|
-
}
|
|
138
|
-
/** pgvector: distance operators */
|
|
139
|
-
type PgvectorOp = "<->" | "<#>" | "<=>" | "<+>" | "<~>" | "<%>";
|
|
140
|
-
/** pgvector: distance operator expression */
|
|
141
|
-
interface WherePgvectorOp {
|
|
142
|
-
readonly type: "where_pgvector_op";
|
|
143
|
-
op: PgvectorOp;
|
|
144
|
-
left: WhereValue;
|
|
145
|
-
right: WhereValue;
|
|
146
|
-
}
|
|
147
|
-
type ArithOp = "+" | "-" | "*" | "/" | "%" | "||";
|
|
148
|
-
interface WhereArith {
|
|
149
|
-
readonly type: "where_arith";
|
|
150
|
-
op: ArithOp;
|
|
151
|
-
left: WhereValue;
|
|
152
|
-
right: WhereValue;
|
|
153
|
-
}
|
|
154
|
-
interface WhereUnaryMinus {
|
|
155
|
-
readonly type: "where_unary_minus";
|
|
156
|
-
expr: WhereValue;
|
|
157
|
-
}
|
|
158
|
-
interface CaseWhen {
|
|
159
|
-
condition: WhereValue;
|
|
160
|
-
result: WhereValue;
|
|
161
|
-
}
|
|
162
|
-
interface CaseExpr {
|
|
163
|
-
readonly type: "case_expr";
|
|
164
|
-
subject: WhereValue | null;
|
|
165
|
-
whens: CaseWhen[];
|
|
166
|
-
else: WhereValue | null;
|
|
167
|
-
}
|
|
168
|
-
interface CastExpr {
|
|
169
|
-
readonly type: "cast_expr";
|
|
170
|
-
expr: WhereValue;
|
|
171
|
-
typeName: string;
|
|
172
|
-
}
|
|
173
|
-
type WhereValue = {
|
|
174
|
-
readonly type: "where_value";
|
|
175
|
-
kind: "string";
|
|
176
|
-
value: string;
|
|
177
|
-
} | {
|
|
178
|
-
readonly type: "where_value";
|
|
179
|
-
kind: "integer";
|
|
180
|
-
value: number;
|
|
181
|
-
} | {
|
|
182
|
-
readonly type: "where_value";
|
|
183
|
-
kind: "float";
|
|
184
|
-
value: number;
|
|
185
|
-
} | {
|
|
186
|
-
readonly type: "where_value";
|
|
187
|
-
kind: "bool";
|
|
188
|
-
value: boolean;
|
|
189
|
-
} | {
|
|
190
|
-
readonly type: "where_value";
|
|
191
|
-
kind: "null";
|
|
192
|
-
} | {
|
|
193
|
-
readonly type: "where_value";
|
|
194
|
-
kind: "column_ref";
|
|
195
|
-
ref: ColumnRef;
|
|
196
|
-
} | {
|
|
197
|
-
readonly type: "where_value";
|
|
198
|
-
kind: "func_call";
|
|
199
|
-
func: FuncCall;
|
|
200
|
-
} | WhereArith | WhereUnaryMinus | WhereJsonbOp | WherePgvectorOp | CaseExpr | CastExpr;
|
|
201
|
-
interface WhereComparison {
|
|
202
|
-
readonly type: "where_comparison";
|
|
203
|
-
operator: ComparisonOperator;
|
|
204
|
-
left: WhereValue;
|
|
205
|
-
right: WhereValue;
|
|
206
|
-
}
|
|
207
|
-
interface ColumnRef {
|
|
208
|
-
readonly type: "column_ref";
|
|
209
|
-
schema?: string;
|
|
210
|
-
table?: string;
|
|
211
|
-
name: string;
|
|
212
|
-
}
|
|
213
|
-
type FuncCallArg = {
|
|
214
|
-
kind: "wildcard";
|
|
215
|
-
} | {
|
|
216
|
-
kind: "args";
|
|
217
|
-
distinct: boolean;
|
|
218
|
-
args: WhereValue[];
|
|
219
|
-
};
|
|
220
|
-
interface FuncCall {
|
|
221
|
-
readonly type: "func_call";
|
|
222
|
-
name: string;
|
|
223
|
-
args: FuncCallArg;
|
|
224
|
-
}
|
|
225
|
-
interface ColumnExpr {
|
|
226
|
-
readonly type: "column_expr";
|
|
227
|
-
kind: "wildcard" | "qualified_wildcard" | "expr";
|
|
228
|
-
table?: string;
|
|
229
|
-
expr?: WhereValue;
|
|
230
|
-
}
|
|
231
|
-
interface Column {
|
|
232
|
-
readonly type: "column";
|
|
233
|
-
expr: ColumnExpr;
|
|
234
|
-
alias?: Alias;
|
|
235
|
-
}
|
|
236
|
-
interface Alias {
|
|
237
|
-
readonly type: "alias";
|
|
238
|
-
name: string;
|
|
239
|
-
}
|
|
240
|
-
interface TableRef {
|
|
241
|
-
readonly type: "table_ref";
|
|
1
|
+
import { i as SelectStatement, n as defineSchema, r as Result, t as Schema } from "./joins-Cu_0yAgN.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/guard.d.ts
|
|
4
|
+
type GuardVal = string | number;
|
|
5
|
+
interface GuardCol {
|
|
242
6
|
schema?: string;
|
|
243
|
-
|
|
7
|
+
table: string;
|
|
8
|
+
column: string;
|
|
244
9
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
type Failure<E = unknown> = {
|
|
248
|
-
ok: false;
|
|
249
|
-
error: E;
|
|
250
|
-
unwrap(): never;
|
|
251
|
-
};
|
|
252
|
-
type Success<T = void> = {
|
|
253
|
-
ok: true;
|
|
254
|
-
data: T;
|
|
255
|
-
unwrap(): T;
|
|
10
|
+
type WhereGuard = GuardCol & {
|
|
11
|
+
value: GuardVal;
|
|
256
12
|
};
|
|
257
|
-
type
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
type TableDefs = ReturnType<typeof defineTables>;
|
|
261
|
-
declare function defineTables<T extends { [Table in keyof T]: Record<string, null | { [FK in keyof T & string]: {
|
|
262
|
-
ft: FK;
|
|
263
|
-
fc: keyof T[FK] & string;
|
|
264
|
-
} }[keyof T & string]> }>(tables: T): T;
|
|
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>;
|
|
265
16
|
//#endregion
|
|
266
17
|
//#region src/output.d.ts
|
|
267
18
|
declare function outputSql(ast: SelectStatement): string;
|
|
@@ -269,54 +20,21 @@ declare function outputSql(ast: SelectStatement): string;
|
|
|
269
20
|
//#region src/parse.d.ts
|
|
270
21
|
declare function parseSql(expr: string): Result<SelectStatement>;
|
|
271
22
|
//#endregion
|
|
272
|
-
//#region src/sanitise.d.ts
|
|
273
|
-
interface GuardCol {
|
|
274
|
-
schema?: string;
|
|
275
|
-
table: string;
|
|
276
|
-
col: string;
|
|
277
|
-
}
|
|
278
|
-
interface WhereGuard {
|
|
279
|
-
schema?: string;
|
|
280
|
-
table: string;
|
|
281
|
-
col: string;
|
|
282
|
-
value: string | number;
|
|
283
|
-
}
|
|
284
|
-
declare function sanitiseSql(ast: SelectStatement, guard: WhereGuard): Result<SelectStatement>;
|
|
285
|
-
//#endregion
|
|
286
23
|
//#region src/index.d.ts
|
|
287
|
-
declare function
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
|
|
24
|
+
declare function agentSql<S extends string>(sql: string, column: S & OneOrTwoDots<S>, value: GuardVal, {
|
|
25
|
+
schema,
|
|
26
|
+
limit
|
|
27
|
+
}?: {
|
|
28
|
+
schema?: Schema;
|
|
29
|
+
limit?: number;
|
|
293
30
|
}): string;
|
|
294
|
-
declare function
|
|
295
|
-
|
|
296
|
-
where
|
|
297
|
-
}: {
|
|
298
|
-
tables: TableDefs;
|
|
299
|
-
where: WhereGuard;
|
|
300
|
-
}): Result<string>;
|
|
301
|
-
declare function sanitiserFactory(_: {
|
|
302
|
-
tables: TableDefs;
|
|
303
|
-
where: WhereGuard;
|
|
31
|
+
declare function createAgentSql<T extends Schema, S extends SchemaGuardKeys<T>>(schema: T, guards: Record<S, GuardVal>, opts: {
|
|
32
|
+
limit?: number;
|
|
304
33
|
throws: false;
|
|
305
34
|
}): (expr: string) => Result<string>;
|
|
306
|
-
declare function
|
|
307
|
-
|
|
308
|
-
where: WhereGuard;
|
|
35
|
+
declare function createAgentSql<T extends Schema, S extends SchemaGuardKeys<T>>(schema: T, guards: Record<S, GuardVal>, opts?: {
|
|
36
|
+
limit?: number;
|
|
309
37
|
throws?: true;
|
|
310
38
|
}): (expr: string) => string;
|
|
311
|
-
declare function makeSanitiserFactory(_: {
|
|
312
|
-
tables: TableDefs;
|
|
313
|
-
guardCol: GuardCol;
|
|
314
|
-
throws: false;
|
|
315
|
-
}): (guardVal: string | number) => (expr: string) => Result<string>;
|
|
316
|
-
declare function makeSanitiserFactory(_: {
|
|
317
|
-
tables: TableDefs;
|
|
318
|
-
guardCol: GuardCol;
|
|
319
|
-
throws?: true;
|
|
320
|
-
}): (guardVal: string | number) => (expr: string) => string;
|
|
321
39
|
//#endregion
|
|
322
|
-
export {
|
|
40
|
+
export { agentSql, createAgentSql, defineSchema, outputSql, parseSql, applyGuards as sanitiseSql };
|
package/dist/index.mjs
CHANGED
|
@@ -6,65 +6,9 @@ var ParseError = class extends Error {
|
|
|
6
6
|
var SanitiseError = class extends Error {
|
|
7
7
|
type = "sanitise_error";
|
|
8
8
|
};
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return {
|
|
13
|
-
ok: false,
|
|
14
|
-
error,
|
|
15
|
-
unwrap() {
|
|
16
|
-
throw new Error(String(error));
|
|
17
|
-
}
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
function Ok(data) {
|
|
21
|
-
return {
|
|
22
|
-
ok: true,
|
|
23
|
-
data,
|
|
24
|
-
unwrap() {
|
|
25
|
-
return data;
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
function returnOrThrow(result, throws) {
|
|
30
|
-
if (!throws) return result;
|
|
31
|
-
if (result.ok) return result.data;
|
|
32
|
-
throw result.error;
|
|
33
|
-
}
|
|
34
|
-
//#endregion
|
|
35
|
-
//#region src/graph.ts
|
|
36
|
-
function defineTables(tables) {
|
|
37
|
-
return tables;
|
|
38
|
-
}
|
|
39
|
-
function checkJoins(ast, tables) {
|
|
40
|
-
if (!(ast.from.table.name in tables)) return Err(new SanitiseError(`Table ${ast.from.table.name} is not allowed`));
|
|
41
|
-
for (const join of ast.joins) {
|
|
42
|
-
const joinSettings = tables[join.table.name];
|
|
43
|
-
if (joinSettings === void 0) return Err(new SanitiseError(`Table ${join.table.name} is not allowed`));
|
|
44
|
-
if (join.condition === null || join.condition.type === "join_using" || join.condition.expr.type !== "where_comparison" || join.condition.expr.operator !== "=" || join.condition.expr.left.type !== "where_value" || join.condition.expr.left.kind !== "column_ref" || join.condition.expr.right.type !== "where_value" || join.condition.expr.right.kind !== "column_ref") return Err(new SanitiseError("Only JOIN ON column_ref = column_ref supported"));
|
|
45
|
-
const { joining, foreign } = getJoinTableRef(join.table.name, join.condition.expr.left.ref, join.condition.expr.right.ref);
|
|
46
|
-
const joinTableCol = joinSettings[joining.name];
|
|
47
|
-
if (joinTableCol === void 0) return Err(new SanitiseError(`Tried to join using ${join.table.name}.${joining.name}`));
|
|
48
|
-
if (joinTableCol === null) {
|
|
49
|
-
const foreignTableSettings = tables[foreign.table];
|
|
50
|
-
if (foreignTableSettings === void 0) return Err(new SanitiseError(`Table ${foreign.name} is not allowed`));
|
|
51
|
-
const foreignCol = foreignTableSettings[foreign.name];
|
|
52
|
-
if (foreignCol === void 0 || foreignCol === null) return Err(new SanitiseError(`Tried to join using ${foreign.table}.${foreign.name}`));
|
|
53
|
-
if (joining.table !== foreignCol.ft || joining.name !== foreignCol.fc) return Err(new SanitiseError(`Tried to join using ${joining.table}.${joining.name}`));
|
|
54
|
-
} else if (foreign.table !== joinTableCol.ft || foreign.name !== joinTableCol.fc) return Err(new SanitiseError(`Tried to join using ${foreign.table}.${foreign.name}`));
|
|
55
|
-
}
|
|
56
|
-
return Ok(ast);
|
|
57
|
-
}
|
|
58
|
-
function getJoinTableRef(joinTableName, left, right) {
|
|
59
|
-
if (left.table === joinTableName) return {
|
|
60
|
-
joining: left,
|
|
61
|
-
foreign: right
|
|
62
|
-
};
|
|
63
|
-
return {
|
|
64
|
-
joining: right,
|
|
65
|
-
foreign: left
|
|
66
|
-
};
|
|
67
|
-
}
|
|
9
|
+
var AgentSqlError = class extends Error {
|
|
10
|
+
type = "agent_sql_error";
|
|
11
|
+
};
|
|
68
12
|
//#endregion
|
|
69
13
|
//#region src/utils.ts
|
|
70
14
|
function unreachable(x) {
|
|
@@ -276,6 +220,176 @@ function handleColumnExpr(node) {
|
|
|
276
220
|
}
|
|
277
221
|
}
|
|
278
222
|
//#endregion
|
|
223
|
+
//#region src/result.ts
|
|
224
|
+
function Err(error) {
|
|
225
|
+
return {
|
|
226
|
+
ok: false,
|
|
227
|
+
error,
|
|
228
|
+
unwrap() {
|
|
229
|
+
throw new Error(String(error));
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function Ok(data) {
|
|
234
|
+
return {
|
|
235
|
+
ok: true,
|
|
236
|
+
data,
|
|
237
|
+
unwrap() {
|
|
238
|
+
return data;
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function returnOrThrow(result, throws) {
|
|
243
|
+
if (!throws) return result;
|
|
244
|
+
if (result.ok) return result.data;
|
|
245
|
+
throw result.error;
|
|
246
|
+
}
|
|
247
|
+
//#endregion
|
|
248
|
+
//#region src/guard.ts
|
|
249
|
+
const DEFAULT_LIMIT = 1e4;
|
|
250
|
+
function applyGuards(ast, guards, limit = DEFAULT_LIMIT) {
|
|
251
|
+
const ast2 = addWhereGuard(ast, guards);
|
|
252
|
+
if (!ast2.ok) return ast2;
|
|
253
|
+
return addLimitGuard(ast2.data, limit);
|
|
254
|
+
}
|
|
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,
|
|
263
|
+
value
|
|
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);
|
|
318
|
+
return Ok({
|
|
319
|
+
...ast,
|
|
320
|
+
where: ast.where ? {
|
|
321
|
+
type: "where_root",
|
|
322
|
+
inner: {
|
|
323
|
+
type: "where_and",
|
|
324
|
+
left: combined,
|
|
325
|
+
right: ast.where.inner
|
|
326
|
+
}
|
|
327
|
+
} : {
|
|
328
|
+
type: "where_root",
|
|
329
|
+
inner: combined
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
function addLimitGuard(ast, limit) {
|
|
334
|
+
const limitClause = {
|
|
335
|
+
type: "limit",
|
|
336
|
+
value: limit
|
|
337
|
+
};
|
|
338
|
+
if (ast.limit === null) return Ok({
|
|
339
|
+
...ast,
|
|
340
|
+
limit: limitClause
|
|
341
|
+
});
|
|
342
|
+
if (ast.limit.value > limit) return Ok({
|
|
343
|
+
...ast,
|
|
344
|
+
limit: limitClause
|
|
345
|
+
});
|
|
346
|
+
return Ok(ast);
|
|
347
|
+
}
|
|
348
|
+
function checkIfTableRefExists(ast, tableRef) {
|
|
349
|
+
return tableEquals(ast.from.table, tableRef) || ast.joins.some((join) => tableEquals(join.table, tableRef));
|
|
350
|
+
}
|
|
351
|
+
function tableEquals(a, b) {
|
|
352
|
+
return a.schema == b.schema && a.name == b.name;
|
|
353
|
+
}
|
|
354
|
+
//#endregion
|
|
355
|
+
//#region src/joins.ts
|
|
356
|
+
function defineSchema(schema) {
|
|
357
|
+
return schema;
|
|
358
|
+
}
|
|
359
|
+
function checkJoins(ast, schema) {
|
|
360
|
+
if (schema === void 0) {
|
|
361
|
+
if (ast.joins.length > 0) return Err(new SanitiseError("No joins allowed when using simple API without schema."));
|
|
362
|
+
return Ok(ast);
|
|
363
|
+
}
|
|
364
|
+
if (!(ast.from.table.name in schema)) return Err(new SanitiseError(`Table ${ast.from.table.name} is not allowed`));
|
|
365
|
+
for (const join of ast.joins) {
|
|
366
|
+
const joinSettings = schema[join.table.name];
|
|
367
|
+
if (joinSettings === void 0) return Err(new SanitiseError(`Table ${join.table.name} is not allowed`));
|
|
368
|
+
if (join.condition === null || join.condition.type === "join_using" || join.condition.expr.type !== "where_comparison" || join.condition.expr.operator !== "=" || join.condition.expr.left.type !== "where_value" || join.condition.expr.left.kind !== "column_ref" || join.condition.expr.right.type !== "where_value" || join.condition.expr.right.kind !== "column_ref") return Err(new SanitiseError("Only JOIN ON column_ref = column_ref supported"));
|
|
369
|
+
const { joining, foreign } = getJoinTableRef(join.table.name, join.condition.expr.left.ref, join.condition.expr.right.ref);
|
|
370
|
+
const joinTableCol = joinSettings[joining.name];
|
|
371
|
+
if (joinTableCol === void 0) return Err(new SanitiseError(`Tried to join using ${join.table.name}.${joining.name}`));
|
|
372
|
+
if (joinTableCol === null) {
|
|
373
|
+
const foreignTableSettings = schema[foreign.table];
|
|
374
|
+
if (foreignTableSettings === void 0) return Err(new SanitiseError(`Table ${foreign.name} is not allowed`));
|
|
375
|
+
const foreignCol = foreignTableSettings[foreign.name];
|
|
376
|
+
if (foreignCol === void 0 || foreignCol === null) return Err(new SanitiseError(`Tried to join using ${foreign.table}.${foreign.name}`));
|
|
377
|
+
if (joining.table !== foreignCol.ft || joining.name !== foreignCol.fc) return Err(new SanitiseError(`Tried to join using ${joining.table}.${joining.name}`));
|
|
378
|
+
} else if (foreign.table !== joinTableCol.ft || foreign.name !== joinTableCol.fc) return Err(new SanitiseError(`Tried to join using ${foreign.table}.${foreign.name}`));
|
|
379
|
+
}
|
|
380
|
+
return Ok(ast);
|
|
381
|
+
}
|
|
382
|
+
function getJoinTableRef(joinTableName, left, right) {
|
|
383
|
+
if (left.table === joinTableName) return {
|
|
384
|
+
joining: left,
|
|
385
|
+
foreign: right
|
|
386
|
+
};
|
|
387
|
+
return {
|
|
388
|
+
joining: right,
|
|
389
|
+
foreign: left
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
//#endregion
|
|
279
393
|
//#region src/sql.ohm-bundle.js
|
|
280
394
|
const result = makeRecipe([
|
|
281
395
|
"grammar",
|
|
@@ -6333,114 +6447,40 @@ function parseSql(expr) {
|
|
|
6333
6447
|
}
|
|
6334
6448
|
}
|
|
6335
6449
|
//#endregion
|
|
6336
|
-
//#region src/
|
|
6337
|
-
function
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
type: "table_ref",
|
|
6450
|
+
//#region src/index.ts
|
|
6451
|
+
function agentSql(sql, column, value, { schema, limit } = {}) {
|
|
6452
|
+
return privateAgentSql(sql, {
|
|
6453
|
+
guards: { [column]: value },
|
|
6341
6454
|
schema,
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
if (!checkIfTableRefExists(ast, tableRef)) return Err(new SanitiseError(`The table '${handleTableRef(tableRef)}' must appear in the FROM or JOIN clauses.`));
|
|
6345
|
-
const newClause = {
|
|
6346
|
-
type: "where_comparison",
|
|
6347
|
-
operator: "=",
|
|
6348
|
-
left: {
|
|
6349
|
-
type: "where_value",
|
|
6350
|
-
kind: "column_ref",
|
|
6351
|
-
ref: {
|
|
6352
|
-
type: "column_ref",
|
|
6353
|
-
schema,
|
|
6354
|
-
table,
|
|
6355
|
-
name: col
|
|
6356
|
-
}
|
|
6357
|
-
},
|
|
6358
|
-
right: typeof value === "string" ? {
|
|
6359
|
-
type: "where_value",
|
|
6360
|
-
kind: "string",
|
|
6361
|
-
value
|
|
6362
|
-
} : {
|
|
6363
|
-
type: "where_value",
|
|
6364
|
-
kind: "integer",
|
|
6365
|
-
value
|
|
6366
|
-
}
|
|
6367
|
-
};
|
|
6368
|
-
return Ok({
|
|
6369
|
-
...ast,
|
|
6370
|
-
where: ast.where ? {
|
|
6371
|
-
type: "where_root",
|
|
6372
|
-
inner: {
|
|
6373
|
-
type: "where_and",
|
|
6374
|
-
left: newClause,
|
|
6375
|
-
right: ast.where.inner
|
|
6376
|
-
}
|
|
6377
|
-
} : {
|
|
6378
|
-
type: "where_root",
|
|
6379
|
-
inner: newClause
|
|
6380
|
-
}
|
|
6455
|
+
limit,
|
|
6456
|
+
throws: true
|
|
6381
6457
|
});
|
|
6382
6458
|
}
|
|
6383
|
-
function
|
|
6384
|
-
return
|
|
6385
|
-
|
|
6386
|
-
|
|
6387
|
-
|
|
6459
|
+
function createAgentSql(schema, guards, { limit, throws = true } = {}) {
|
|
6460
|
+
return (expr) => throws ? privateAgentSql(expr, {
|
|
6461
|
+
guards,
|
|
6462
|
+
schema,
|
|
6463
|
+
limit,
|
|
6464
|
+
throws
|
|
6465
|
+
}) : privateAgentSql(expr, {
|
|
6466
|
+
guards,
|
|
6467
|
+
schema,
|
|
6468
|
+
limit,
|
|
6469
|
+
throws
|
|
6470
|
+
});
|
|
6388
6471
|
}
|
|
6389
|
-
|
|
6390
|
-
|
|
6391
|
-
|
|
6472
|
+
function privateAgentSql(sql, { guards: guardsRaw, schema, limit, throws }) {
|
|
6473
|
+
const guards = resolveGuards(guardsRaw);
|
|
6474
|
+
if (!guards.ok) throw guards.error;
|
|
6392
6475
|
const ast = parseSql(sql);
|
|
6393
6476
|
if (!ast.ok) return returnOrThrow(ast, throws);
|
|
6394
|
-
const ast2 = checkJoins(ast.data,
|
|
6477
|
+
const ast2 = checkJoins(ast.data, schema);
|
|
6395
6478
|
if (!ast2.ok) return returnOrThrow(ast2, throws);
|
|
6396
|
-
const san =
|
|
6479
|
+
const san = applyGuards(ast2.data, guards.data, limit);
|
|
6397
6480
|
if (!san.ok) return returnOrThrow(san, throws);
|
|
6398
6481
|
const res = outputSql(san.data);
|
|
6399
6482
|
if (throws) return res;
|
|
6400
6483
|
return Ok(res);
|
|
6401
6484
|
}
|
|
6402
|
-
function sanitise(sql, { tables, where }) {
|
|
6403
|
-
return privateSanitise(sql, {
|
|
6404
|
-
tables,
|
|
6405
|
-
where,
|
|
6406
|
-
throws: true
|
|
6407
|
-
});
|
|
6408
|
-
}
|
|
6409
|
-
function safeSanitise(sql, { tables, where }) {
|
|
6410
|
-
return privateSanitise(sql, {
|
|
6411
|
-
tables,
|
|
6412
|
-
where,
|
|
6413
|
-
throws: false
|
|
6414
|
-
});
|
|
6415
|
-
}
|
|
6416
|
-
function sanitiserFactory({ tables, where, throws = true }) {
|
|
6417
|
-
return (expr) => throws ? privateSanitise(expr, {
|
|
6418
|
-
tables,
|
|
6419
|
-
where,
|
|
6420
|
-
throws
|
|
6421
|
-
}) : privateSanitise(expr, {
|
|
6422
|
-
tables,
|
|
6423
|
-
where,
|
|
6424
|
-
throws
|
|
6425
|
-
});
|
|
6426
|
-
}
|
|
6427
|
-
function makeSanitiserFactory({ tables, guardCol, throws = true }) {
|
|
6428
|
-
function factory(guardVal) {
|
|
6429
|
-
const where = {
|
|
6430
|
-
...guardCol,
|
|
6431
|
-
value: guardVal
|
|
6432
|
-
};
|
|
6433
|
-
return throws ? sanitiserFactory({
|
|
6434
|
-
tables,
|
|
6435
|
-
where,
|
|
6436
|
-
throws
|
|
6437
|
-
}) : sanitiserFactory({
|
|
6438
|
-
tables,
|
|
6439
|
-
where,
|
|
6440
|
-
throws
|
|
6441
|
-
});
|
|
6442
|
-
}
|
|
6443
|
-
return factory;
|
|
6444
|
-
}
|
|
6445
6485
|
//#endregion
|
|
6446
|
-
export {
|
|
6486
|
+
export { agentSql, createAgentSql, defineSchema, outputSql, parseSql, applyGuards as sanitiseSql };
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
//#region src/ast.d.ts
|
|
2
|
+
interface SelectStatement {
|
|
3
|
+
readonly type: "select";
|
|
4
|
+
distinct: Distinct | DistinctOn | null;
|
|
5
|
+
columns: Column[];
|
|
6
|
+
from: SelectFrom;
|
|
7
|
+
joins: JoinClause[];
|
|
8
|
+
where: WhereRoot | null;
|
|
9
|
+
groupBy: GroupByClause | null;
|
|
10
|
+
having: HavingClause | null;
|
|
11
|
+
orderBy: OrderByClause | null;
|
|
12
|
+
limit: LimitClause | null;
|
|
13
|
+
offset: OffsetClause | null;
|
|
14
|
+
}
|
|
15
|
+
interface Distinct {
|
|
16
|
+
readonly type: "distinct";
|
|
17
|
+
}
|
|
18
|
+
/** PostgreSQL: DISTINCT ON (col1, col2, ...) */
|
|
19
|
+
interface DistinctOn {
|
|
20
|
+
readonly type: "distinct_on";
|
|
21
|
+
columns: WhereValue[];
|
|
22
|
+
}
|
|
23
|
+
interface GroupByClause {
|
|
24
|
+
readonly type: "group_by";
|
|
25
|
+
items: WhereValue[];
|
|
26
|
+
}
|
|
27
|
+
interface HavingClause {
|
|
28
|
+
readonly type: "having";
|
|
29
|
+
expr: WhereExpr;
|
|
30
|
+
}
|
|
31
|
+
interface OrderByClause {
|
|
32
|
+
readonly type: "order_by";
|
|
33
|
+
items: OrderByItem[];
|
|
34
|
+
}
|
|
35
|
+
type SortDirection = "asc" | "desc";
|
|
36
|
+
type NullsOrder = "nulls_first" | "nulls_last";
|
|
37
|
+
interface OrderByItem {
|
|
38
|
+
readonly type: "order_by_item";
|
|
39
|
+
expr: WhereValue;
|
|
40
|
+
direction?: SortDirection;
|
|
41
|
+
nulls?: NullsOrder;
|
|
42
|
+
}
|
|
43
|
+
interface OffsetClause {
|
|
44
|
+
readonly type: "offset";
|
|
45
|
+
value: number;
|
|
46
|
+
}
|
|
47
|
+
type JoinType = "inner" | "inner_outer" | "left" | "left_outer" | "right" | "right_outer" | "full" | "full_outer" | "cross" | "natural";
|
|
48
|
+
type JoinCondition = {
|
|
49
|
+
readonly type: "join_on";
|
|
50
|
+
expr: WhereExpr;
|
|
51
|
+
} | {
|
|
52
|
+
readonly type: "join_using";
|
|
53
|
+
columns: string[];
|
|
54
|
+
};
|
|
55
|
+
interface JoinClause {
|
|
56
|
+
readonly type: "join";
|
|
57
|
+
joinType: JoinType;
|
|
58
|
+
table: TableRef;
|
|
59
|
+
condition: JoinCondition | null;
|
|
60
|
+
}
|
|
61
|
+
type SelectFrom = {
|
|
62
|
+
readonly type: "select_from";
|
|
63
|
+
table: TableRef;
|
|
64
|
+
};
|
|
65
|
+
type LimitClause = {
|
|
66
|
+
readonly type: "limit";
|
|
67
|
+
value: number;
|
|
68
|
+
};
|
|
69
|
+
type WhereRoot = {
|
|
70
|
+
readonly type: "where_root";
|
|
71
|
+
inner: WhereExpr;
|
|
72
|
+
};
|
|
73
|
+
type WhereExpr = WhereAnd | WhereOr | WhereNot | WhereComparison | WhereIsNull | WhereIsBool | WhereBetween | WhereIn | WhereLike | WhereTsMatch;
|
|
74
|
+
interface WhereAnd {
|
|
75
|
+
readonly type: "where_and";
|
|
76
|
+
left: WhereExpr;
|
|
77
|
+
right: WhereExpr;
|
|
78
|
+
}
|
|
79
|
+
interface WhereOr {
|
|
80
|
+
readonly type: "where_or";
|
|
81
|
+
left: WhereExpr;
|
|
82
|
+
right: WhereExpr;
|
|
83
|
+
}
|
|
84
|
+
type ComparisonOperator = "=" | "<>" | "!=" | "<" | ">" | "<=" | ">=";
|
|
85
|
+
interface WhereNot {
|
|
86
|
+
readonly type: "where_not";
|
|
87
|
+
expr: WhereExpr;
|
|
88
|
+
}
|
|
89
|
+
interface WhereIsNull {
|
|
90
|
+
readonly type: "where_is_null";
|
|
91
|
+
not: boolean;
|
|
92
|
+
expr: WhereValue;
|
|
93
|
+
}
|
|
94
|
+
type IsBoolTarget = boolean | "unknown";
|
|
95
|
+
interface WhereIsBool {
|
|
96
|
+
readonly type: "where_is_bool";
|
|
97
|
+
not: boolean;
|
|
98
|
+
expr: WhereValue;
|
|
99
|
+
target: IsBoolTarget;
|
|
100
|
+
}
|
|
101
|
+
interface WhereBetween {
|
|
102
|
+
readonly type: "where_between";
|
|
103
|
+
not: boolean;
|
|
104
|
+
expr: WhereValue;
|
|
105
|
+
low: WhereValue;
|
|
106
|
+
high: WhereValue;
|
|
107
|
+
}
|
|
108
|
+
interface WhereIn {
|
|
109
|
+
readonly type: "where_in";
|
|
110
|
+
not: boolean;
|
|
111
|
+
expr: WhereValue;
|
|
112
|
+
list: WhereValue[];
|
|
113
|
+
}
|
|
114
|
+
/** "ilike" is PostgreSQL-specific (case-insensitive LIKE) */
|
|
115
|
+
type LikeOp = "like" | "ilike";
|
|
116
|
+
interface WhereLike {
|
|
117
|
+
readonly type: "where_like";
|
|
118
|
+
not: boolean;
|
|
119
|
+
op: LikeOp;
|
|
120
|
+
expr: WhereValue;
|
|
121
|
+
pattern: WhereValue;
|
|
122
|
+
}
|
|
123
|
+
/** PostgreSQL: JSONB operators */
|
|
124
|
+
type JsonbOp = "->" | "->>" | "#>" | "#>>" | "?" | "?|" | "?&" | "@>";
|
|
125
|
+
/** PostgreSQL: JSONB binary operator expression */
|
|
126
|
+
interface WhereJsonbOp {
|
|
127
|
+
readonly type: "where_jsonb_op";
|
|
128
|
+
op: JsonbOp;
|
|
129
|
+
left: WhereValue;
|
|
130
|
+
right: WhereValue;
|
|
131
|
+
}
|
|
132
|
+
/** PostgreSQL: text search match (@@) */
|
|
133
|
+
interface WhereTsMatch {
|
|
134
|
+
readonly type: "where_ts_match";
|
|
135
|
+
left: WhereValue;
|
|
136
|
+
right: WhereValue;
|
|
137
|
+
}
|
|
138
|
+
/** pgvector: distance operators */
|
|
139
|
+
type PgvectorOp = "<->" | "<#>" | "<=>" | "<+>" | "<~>" | "<%>";
|
|
140
|
+
/** pgvector: distance operator expression */
|
|
141
|
+
interface WherePgvectorOp {
|
|
142
|
+
readonly type: "where_pgvector_op";
|
|
143
|
+
op: PgvectorOp;
|
|
144
|
+
left: WhereValue;
|
|
145
|
+
right: WhereValue;
|
|
146
|
+
}
|
|
147
|
+
type ArithOp = "+" | "-" | "*" | "/" | "%" | "||";
|
|
148
|
+
interface WhereArith {
|
|
149
|
+
readonly type: "where_arith";
|
|
150
|
+
op: ArithOp;
|
|
151
|
+
left: WhereValue;
|
|
152
|
+
right: WhereValue;
|
|
153
|
+
}
|
|
154
|
+
interface WhereUnaryMinus {
|
|
155
|
+
readonly type: "where_unary_minus";
|
|
156
|
+
expr: WhereValue;
|
|
157
|
+
}
|
|
158
|
+
interface CaseWhen {
|
|
159
|
+
condition: WhereValue;
|
|
160
|
+
result: WhereValue;
|
|
161
|
+
}
|
|
162
|
+
interface CaseExpr {
|
|
163
|
+
readonly type: "case_expr";
|
|
164
|
+
subject: WhereValue | null;
|
|
165
|
+
whens: CaseWhen[];
|
|
166
|
+
else: WhereValue | null;
|
|
167
|
+
}
|
|
168
|
+
interface CastExpr {
|
|
169
|
+
readonly type: "cast_expr";
|
|
170
|
+
expr: WhereValue;
|
|
171
|
+
typeName: string;
|
|
172
|
+
}
|
|
173
|
+
type WhereValue = {
|
|
174
|
+
readonly type: "where_value";
|
|
175
|
+
kind: "string";
|
|
176
|
+
value: string;
|
|
177
|
+
} | {
|
|
178
|
+
readonly type: "where_value";
|
|
179
|
+
kind: "integer";
|
|
180
|
+
value: number;
|
|
181
|
+
} | {
|
|
182
|
+
readonly type: "where_value";
|
|
183
|
+
kind: "float";
|
|
184
|
+
value: number;
|
|
185
|
+
} | {
|
|
186
|
+
readonly type: "where_value";
|
|
187
|
+
kind: "bool";
|
|
188
|
+
value: boolean;
|
|
189
|
+
} | {
|
|
190
|
+
readonly type: "where_value";
|
|
191
|
+
kind: "null";
|
|
192
|
+
} | {
|
|
193
|
+
readonly type: "where_value";
|
|
194
|
+
kind: "column_ref";
|
|
195
|
+
ref: ColumnRef;
|
|
196
|
+
} | {
|
|
197
|
+
readonly type: "where_value";
|
|
198
|
+
kind: "func_call";
|
|
199
|
+
func: FuncCall;
|
|
200
|
+
} | WhereArith | WhereUnaryMinus | WhereJsonbOp | WherePgvectorOp | CaseExpr | CastExpr;
|
|
201
|
+
interface WhereComparison {
|
|
202
|
+
readonly type: "where_comparison";
|
|
203
|
+
operator: ComparisonOperator;
|
|
204
|
+
left: WhereValue;
|
|
205
|
+
right: WhereValue;
|
|
206
|
+
}
|
|
207
|
+
interface ColumnRef {
|
|
208
|
+
readonly type: "column_ref";
|
|
209
|
+
schema?: string;
|
|
210
|
+
table?: string;
|
|
211
|
+
name: string;
|
|
212
|
+
}
|
|
213
|
+
type FuncCallArg = {
|
|
214
|
+
kind: "wildcard";
|
|
215
|
+
} | {
|
|
216
|
+
kind: "args";
|
|
217
|
+
distinct: boolean;
|
|
218
|
+
args: WhereValue[];
|
|
219
|
+
};
|
|
220
|
+
interface FuncCall {
|
|
221
|
+
readonly type: "func_call";
|
|
222
|
+
name: string;
|
|
223
|
+
args: FuncCallArg;
|
|
224
|
+
}
|
|
225
|
+
interface ColumnExpr {
|
|
226
|
+
readonly type: "column_expr";
|
|
227
|
+
kind: "wildcard" | "qualified_wildcard" | "expr";
|
|
228
|
+
table?: string;
|
|
229
|
+
expr?: WhereValue;
|
|
230
|
+
}
|
|
231
|
+
interface Column {
|
|
232
|
+
readonly type: "column";
|
|
233
|
+
expr: ColumnExpr;
|
|
234
|
+
alias?: Alias;
|
|
235
|
+
}
|
|
236
|
+
interface Alias {
|
|
237
|
+
readonly type: "alias";
|
|
238
|
+
name: string;
|
|
239
|
+
}
|
|
240
|
+
interface TableRef {
|
|
241
|
+
readonly type: "table_ref";
|
|
242
|
+
schema?: string;
|
|
243
|
+
name: string;
|
|
244
|
+
}
|
|
245
|
+
//#endregion
|
|
246
|
+
//#region src/result.d.ts
|
|
247
|
+
type Failure<E = unknown> = {
|
|
248
|
+
ok: false;
|
|
249
|
+
error: E;
|
|
250
|
+
unwrap(): never;
|
|
251
|
+
};
|
|
252
|
+
type Success<T = void> = {
|
|
253
|
+
ok: true;
|
|
254
|
+
data: T;
|
|
255
|
+
unwrap(): T;
|
|
256
|
+
};
|
|
257
|
+
type Result<T = void, E = Error> = Failure<E> | Success<T>;
|
|
258
|
+
//#endregion
|
|
259
|
+
//#region src/joins.d.ts
|
|
260
|
+
type Schema = ReturnType<typeof defineSchema>;
|
|
261
|
+
declare function defineSchema<T extends { [Table in keyof T]: Record<string, null | { [FK in keyof T & string]: {
|
|
262
|
+
ft: FK;
|
|
263
|
+
fc: keyof T[FK] & string;
|
|
264
|
+
} }[keyof T & string]> }>(schema: T): T;
|
|
265
|
+
//#endregion
|
|
266
|
+
export { SelectStatement as i, defineSchema as n, Result as r, Schema as t };
|
package/package.json
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-sql",
|
|
3
|
-
"version": "0.1
|
|
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"
|
|
@@ -18,6 +24,7 @@
|
|
|
18
24
|
"type": "module",
|
|
19
25
|
"exports": {
|
|
20
26
|
".": "./dist/index.mjs",
|
|
27
|
+
"./drizzle": "./dist/drizzle.mjs",
|
|
21
28
|
"./package.json": "./package.json"
|
|
22
29
|
},
|
|
23
30
|
"publishConfig": {
|
|
@@ -42,11 +49,20 @@
|
|
|
42
49
|
"@types/pg": "^8.18.0",
|
|
43
50
|
"@typescript/native-preview": "7.0.0-dev.20260316.1",
|
|
44
51
|
"bumpp": "^11.0.1",
|
|
52
|
+
"drizzle-orm": "^0.45.1",
|
|
45
53
|
"pg": "^8.20.0",
|
|
46
54
|
"tsx": "^4.21.0",
|
|
47
55
|
"typescript": "^5.9.3",
|
|
48
56
|
"vite-plus": "^0.1.11"
|
|
49
57
|
},
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"drizzle-orm": ">=0.45"
|
|
60
|
+
},
|
|
61
|
+
"peerDependenciesMeta": {
|
|
62
|
+
"drizzle-orm": {
|
|
63
|
+
"optional": true
|
|
64
|
+
}
|
|
65
|
+
},
|
|
50
66
|
"packageManager": "pnpm@10.32.1",
|
|
51
67
|
"pnpm": {
|
|
52
68
|
"overrides": {
|