agent-sql 0.2.3 → 0.3.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/dist/index.d.mts +4 -0
- package/dist/index.mjs +163 -42
- package/package.json +2 -2
package/dist/index.d.mts
CHANGED
|
@@ -26,18 +26,21 @@ declare function parseSql(expr: string): Result<SelectStatement>;
|
|
|
26
26
|
//#region src/index.d.ts
|
|
27
27
|
declare function agentSql<S extends string>(sql: string, column: S & OneOrTwoDots<S>, value: GuardVal, {
|
|
28
28
|
schema,
|
|
29
|
+
autoJoin,
|
|
29
30
|
limit,
|
|
30
31
|
pretty,
|
|
31
32
|
db,
|
|
32
33
|
allowExtraFunctions
|
|
33
34
|
}?: {
|
|
34
35
|
schema?: Schema;
|
|
36
|
+
autoJoin?: boolean;
|
|
35
37
|
limit?: number;
|
|
36
38
|
pretty?: boolean;
|
|
37
39
|
db?: DbType;
|
|
38
40
|
allowExtraFunctions?: string[];
|
|
39
41
|
}): string;
|
|
40
42
|
declare function createAgentSql<T extends Schema, S extends SchemaGuardKeys<T>>(schema: T, guards: Record<S, GuardVal>, opts: {
|
|
43
|
+
autoJoin?: boolean;
|
|
41
44
|
limit?: number;
|
|
42
45
|
pretty?: boolean;
|
|
43
46
|
throws: false;
|
|
@@ -45,6 +48,7 @@ declare function createAgentSql<T extends Schema, S extends SchemaGuardKeys<T>>(
|
|
|
45
48
|
allowExtraFunctions?: string[];
|
|
46
49
|
}): (expr: string) => Result<string>;
|
|
47
50
|
declare function createAgentSql<T extends Schema, S extends SchemaGuardKeys<T>>(schema: T, guards: Record<S, GuardVal>, opts?: {
|
|
51
|
+
autoJoin?: boolean;
|
|
48
52
|
limit?: number;
|
|
49
53
|
pretty?: boolean;
|
|
50
54
|
throws?: true;
|
package/dist/index.mjs
CHANGED
|
@@ -774,6 +774,160 @@ function checkWhereExpr(expr, allowed) {
|
|
|
774
774
|
}
|
|
775
775
|
}
|
|
776
776
|
//#endregion
|
|
777
|
+
//#region src/joins.ts
|
|
778
|
+
function defineSchema(schema) {
|
|
779
|
+
return schema;
|
|
780
|
+
}
|
|
781
|
+
function checkJoins(ast, schema) {
|
|
782
|
+
if (schema === void 0) {
|
|
783
|
+
if (ast.joins.length > 0) return Err(new SanitiseError("No joins allowed when using simple API without schema."));
|
|
784
|
+
return Ok(ast);
|
|
785
|
+
}
|
|
786
|
+
if (!(ast.from.table.name in schema)) return Err(new SanitiseError(`Table ${ast.from.table.name} is not allowed`));
|
|
787
|
+
for (const join of ast.joins) {
|
|
788
|
+
const joinSettings = schema[join.table.name];
|
|
789
|
+
if (joinSettings === void 0) return Err(new SanitiseError(`Table ${join.table.name} is not allowed`));
|
|
790
|
+
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"));
|
|
791
|
+
const { joining, foreign } = getJoinTableRef(join.table.name, join.condition.expr.left.ref, join.condition.expr.right.ref);
|
|
792
|
+
const joinTableCol = joinSettings[joining.name];
|
|
793
|
+
if (joinTableCol === void 0) return Err(new SanitiseError(`Tried to join using ${join.table.name}.${joining.name}`));
|
|
794
|
+
if (joinTableCol === null) {
|
|
795
|
+
const foreignTableSettings = schema[foreign.table];
|
|
796
|
+
if (foreignTableSettings === void 0) return Err(new SanitiseError(`Table ${foreign.name} is not allowed`));
|
|
797
|
+
const foreignCol = foreignTableSettings[foreign.name];
|
|
798
|
+
if (foreignCol === void 0 || foreignCol === null) return Err(new SanitiseError(`Tried to join using ${foreign.table}.${foreign.name}`));
|
|
799
|
+
if (joining.table !== foreignCol.ft || joining.name !== foreignCol.fc) return Err(new SanitiseError(`Tried to join using ${joining.table}.${joining.name}`));
|
|
800
|
+
} else if (foreign.table !== joinTableCol.ft || foreign.name !== joinTableCol.fc) return Err(new SanitiseError(`Tried to join using ${foreign.table}.${foreign.name}`));
|
|
801
|
+
}
|
|
802
|
+
return Ok(ast);
|
|
803
|
+
}
|
|
804
|
+
function getJoinTableRef(joinTableName, left, right) {
|
|
805
|
+
if (left.table === joinTableName) return {
|
|
806
|
+
joining: left,
|
|
807
|
+
foreign: right
|
|
808
|
+
};
|
|
809
|
+
return {
|
|
810
|
+
joining: right,
|
|
811
|
+
foreign: left
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
//#endregion
|
|
815
|
+
//#region src/graph.ts
|
|
816
|
+
function insertNeededGuardJoins(ast, schema, guards, autoJoin) {
|
|
817
|
+
if (schema === void 0) return Ok(ast);
|
|
818
|
+
if (!autoJoin) return Ok(ast);
|
|
819
|
+
return resolveGraphForJoins(ast, schema, guards);
|
|
820
|
+
}
|
|
821
|
+
function resolveGraphForJoins(ast, schema, guards) {
|
|
822
|
+
const haveTables = /* @__PURE__ */ new Set();
|
|
823
|
+
haveTables.add(ast.from.table.name);
|
|
824
|
+
for (const join of ast.joins) haveTables.add(join.table.name);
|
|
825
|
+
const adj = buildAdjacency(schema);
|
|
826
|
+
const newJoins = [];
|
|
827
|
+
for (const guard of guards) {
|
|
828
|
+
if (haveTables.has(guard.table)) continue;
|
|
829
|
+
const path = bfsPath(adj, haveTables, guard.table);
|
|
830
|
+
if (path === null) return Err(new SanitiseError(`No join path from query tables to guard table '${guard.table}'`));
|
|
831
|
+
let current = [...haveTables].find((t) => path[0] && (path[0].tableA === t || path[0].tableB === t));
|
|
832
|
+
for (const edge of path) {
|
|
833
|
+
const neighbor = edge.tableA === current ? edge.tableB : edge.tableA;
|
|
834
|
+
if (!haveTables.has(neighbor)) {
|
|
835
|
+
newJoins.push(edgeToJoin(edge, neighbor));
|
|
836
|
+
haveTables.add(neighbor);
|
|
837
|
+
}
|
|
838
|
+
current = neighbor;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
if (newJoins.length === 0) return Ok(ast);
|
|
842
|
+
return Ok({
|
|
843
|
+
...ast,
|
|
844
|
+
joins: [...ast.joins, ...newJoins]
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
function buildAdjacency(schema) {
|
|
848
|
+
const adj = /* @__PURE__ */ new Map();
|
|
849
|
+
const tables = schema;
|
|
850
|
+
for (const [tableName, cols] of Object.entries(tables)) {
|
|
851
|
+
if (!adj.has(tableName)) adj.set(tableName, []);
|
|
852
|
+
for (const [colName, def] of Object.entries(cols)) if (def && typeof def === "object" && "ft" in def && "fc" in def) {
|
|
853
|
+
const edge = {
|
|
854
|
+
tableA: tableName,
|
|
855
|
+
colA: colName,
|
|
856
|
+
tableB: def.ft,
|
|
857
|
+
colB: def.fc
|
|
858
|
+
};
|
|
859
|
+
adj.get(tableName).push(edge);
|
|
860
|
+
if (!adj.has(edge.tableB)) adj.set(edge.tableB, []);
|
|
861
|
+
adj.get(edge.tableB).push(edge);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
return adj;
|
|
865
|
+
}
|
|
866
|
+
function bfsPath(adj, startTables, target) {
|
|
867
|
+
if (startTables.has(target)) return [];
|
|
868
|
+
const visited = new Set(startTables);
|
|
869
|
+
const queue = [];
|
|
870
|
+
for (const t of startTables) queue.push([t, []]);
|
|
871
|
+
while (queue.length > 0) {
|
|
872
|
+
const [current, path] = queue.shift();
|
|
873
|
+
for (const edge of adj.get(current) ?? []) {
|
|
874
|
+
const neighbor = edge.tableA === current ? edge.tableB : edge.tableA;
|
|
875
|
+
if (visited.has(neighbor)) continue;
|
|
876
|
+
visited.add(neighbor);
|
|
877
|
+
const newPath = [...path, edge];
|
|
878
|
+
if (neighbor === target) return newPath;
|
|
879
|
+
queue.push([neighbor, newPath]);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
return null;
|
|
883
|
+
}
|
|
884
|
+
function edgeToJoin(edge, fromTable) {
|
|
885
|
+
const [localTable, localCol, foreignTable, foreignCol] = edge.tableA === fromTable ? [
|
|
886
|
+
edge.tableA,
|
|
887
|
+
edge.colA,
|
|
888
|
+
edge.tableB,
|
|
889
|
+
edge.colB
|
|
890
|
+
] : [
|
|
891
|
+
edge.tableB,
|
|
892
|
+
edge.colB,
|
|
893
|
+
edge.tableA,
|
|
894
|
+
edge.colA
|
|
895
|
+
];
|
|
896
|
+
return {
|
|
897
|
+
type: "join",
|
|
898
|
+
joinType: "inner",
|
|
899
|
+
table: {
|
|
900
|
+
type: "table_ref",
|
|
901
|
+
name: localTable
|
|
902
|
+
},
|
|
903
|
+
condition: {
|
|
904
|
+
type: "join_on",
|
|
905
|
+
expr: {
|
|
906
|
+
type: "where_comparison",
|
|
907
|
+
operator: "=",
|
|
908
|
+
left: {
|
|
909
|
+
type: "where_value",
|
|
910
|
+
kind: "column_ref",
|
|
911
|
+
ref: {
|
|
912
|
+
type: "column_ref",
|
|
913
|
+
table: localTable,
|
|
914
|
+
name: localCol
|
|
915
|
+
}
|
|
916
|
+
},
|
|
917
|
+
right: {
|
|
918
|
+
type: "where_value",
|
|
919
|
+
kind: "column_ref",
|
|
920
|
+
ref: {
|
|
921
|
+
type: "column_ref",
|
|
922
|
+
table: foreignTable,
|
|
923
|
+
name: foreignCol
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
//#endregion
|
|
777
931
|
//#region src/utils.ts
|
|
778
932
|
function unreachable(x) {
|
|
779
933
|
throw new Error(`Unhandled variant: ${JSON.stringify(x)}`);
|
|
@@ -1108,44 +1262,6 @@ function tableEquals(a, b) {
|
|
|
1108
1262
|
return a.schema == b.schema && a.name == b.name;
|
|
1109
1263
|
}
|
|
1110
1264
|
//#endregion
|
|
1111
|
-
//#region src/joins.ts
|
|
1112
|
-
function defineSchema(schema) {
|
|
1113
|
-
return schema;
|
|
1114
|
-
}
|
|
1115
|
-
function checkJoins(ast, schema) {
|
|
1116
|
-
if (schema === void 0) {
|
|
1117
|
-
if (ast.joins.length > 0) return Err(new SanitiseError("No joins allowed when using simple API without schema."));
|
|
1118
|
-
return Ok(ast);
|
|
1119
|
-
}
|
|
1120
|
-
if (!(ast.from.table.name in schema)) return Err(new SanitiseError(`Table ${ast.from.table.name} is not allowed`));
|
|
1121
|
-
for (const join of ast.joins) {
|
|
1122
|
-
const joinSettings = schema[join.table.name];
|
|
1123
|
-
if (joinSettings === void 0) return Err(new SanitiseError(`Table ${join.table.name} is not allowed`));
|
|
1124
|
-
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"));
|
|
1125
|
-
const { joining, foreign } = getJoinTableRef(join.table.name, join.condition.expr.left.ref, join.condition.expr.right.ref);
|
|
1126
|
-
const joinTableCol = joinSettings[joining.name];
|
|
1127
|
-
if (joinTableCol === void 0) return Err(new SanitiseError(`Tried to join using ${join.table.name}.${joining.name}`));
|
|
1128
|
-
if (joinTableCol === null) {
|
|
1129
|
-
const foreignTableSettings = schema[foreign.table];
|
|
1130
|
-
if (foreignTableSettings === void 0) return Err(new SanitiseError(`Table ${foreign.name} is not allowed`));
|
|
1131
|
-
const foreignCol = foreignTableSettings[foreign.name];
|
|
1132
|
-
if (foreignCol === void 0 || foreignCol === null) return Err(new SanitiseError(`Tried to join using ${foreign.table}.${foreign.name}`));
|
|
1133
|
-
if (joining.table !== foreignCol.ft || joining.name !== foreignCol.fc) return Err(new SanitiseError(`Tried to join using ${joining.table}.${joining.name}`));
|
|
1134
|
-
} else if (foreign.table !== joinTableCol.ft || foreign.name !== joinTableCol.fc) return Err(new SanitiseError(`Tried to join using ${foreign.table}.${foreign.name}`));
|
|
1135
|
-
}
|
|
1136
|
-
return Ok(ast);
|
|
1137
|
-
}
|
|
1138
|
-
function getJoinTableRef(joinTableName, left, right) {
|
|
1139
|
-
if (left.table === joinTableName) return {
|
|
1140
|
-
joining: left,
|
|
1141
|
-
foreign: right
|
|
1142
|
-
};
|
|
1143
|
-
return {
|
|
1144
|
-
joining: right,
|
|
1145
|
-
foreign: left
|
|
1146
|
-
};
|
|
1147
|
-
}
|
|
1148
|
-
//#endregion
|
|
1149
1265
|
//#region src/sql.ohm-bundle.js
|
|
1150
1266
|
const result = makeRecipe([
|
|
1151
1267
|
"grammar",
|
|
@@ -7204,10 +7320,11 @@ function parseSql(expr) {
|
|
|
7204
7320
|
}
|
|
7205
7321
|
//#endregion
|
|
7206
7322
|
//#region src/index.ts
|
|
7207
|
-
function agentSql(sql, column, value, { schema, limit = DEFAULT_LIMIT, pretty = false, db = DEFAULT_DB, allowExtraFunctions = [] } = {}) {
|
|
7323
|
+
function agentSql(sql, column, value, { schema, autoJoin = true, limit = DEFAULT_LIMIT, pretty = false, db = DEFAULT_DB, allowExtraFunctions = [] } = {}) {
|
|
7208
7324
|
return privateAgentSql(sql, {
|
|
7209
7325
|
guards: { [column]: value },
|
|
7210
7326
|
schema,
|
|
7327
|
+
autoJoin,
|
|
7211
7328
|
limit,
|
|
7212
7329
|
pretty,
|
|
7213
7330
|
db,
|
|
@@ -7215,10 +7332,11 @@ function agentSql(sql, column, value, { schema, limit = DEFAULT_LIMIT, pretty =
|
|
|
7215
7332
|
throws: true
|
|
7216
7333
|
});
|
|
7217
7334
|
}
|
|
7218
|
-
function createAgentSql(schema, guards, { limit = DEFAULT_LIMIT, pretty = false, db = DEFAULT_DB, allowExtraFunctions = [], throws = true } = {}) {
|
|
7335
|
+
function createAgentSql(schema, guards, { autoJoin = true, limit = DEFAULT_LIMIT, pretty = false, db = DEFAULT_DB, allowExtraFunctions = [], throws = true } = {}) {
|
|
7219
7336
|
return (expr) => throws ? privateAgentSql(expr, {
|
|
7220
7337
|
guards,
|
|
7221
7338
|
schema,
|
|
7339
|
+
autoJoin,
|
|
7222
7340
|
limit,
|
|
7223
7341
|
pretty,
|
|
7224
7342
|
db,
|
|
@@ -7227,6 +7345,7 @@ function createAgentSql(schema, guards, { limit = DEFAULT_LIMIT, pretty = false,
|
|
|
7227
7345
|
}) : privateAgentSql(expr, {
|
|
7228
7346
|
guards,
|
|
7229
7347
|
schema,
|
|
7348
|
+
autoJoin,
|
|
7230
7349
|
limit,
|
|
7231
7350
|
pretty,
|
|
7232
7351
|
db,
|
|
@@ -7234,7 +7353,7 @@ function createAgentSql(schema, guards, { limit = DEFAULT_LIMIT, pretty = false,
|
|
|
7234
7353
|
throws
|
|
7235
7354
|
});
|
|
7236
7355
|
}
|
|
7237
|
-
function privateAgentSql(sql, { guards: guardsRaw, schema, limit, pretty, db, allowExtraFunctions, throws }) {
|
|
7356
|
+
function privateAgentSql(sql, { guards: guardsRaw, schema, autoJoin, limit, pretty, db, allowExtraFunctions, throws }) {
|
|
7238
7357
|
const guards = resolveGuards(guardsRaw);
|
|
7239
7358
|
if (!guards.ok) throw guards.error;
|
|
7240
7359
|
const ast = parseSql(sql);
|
|
@@ -7243,7 +7362,9 @@ function privateAgentSql(sql, { guards: guardsRaw, schema, limit, pretty, db, al
|
|
|
7243
7362
|
if (!ast2.ok) return returnOrThrow(ast2, throws);
|
|
7244
7363
|
const ast3 = checkFunctions(ast2.data, db, allowExtraFunctions);
|
|
7245
7364
|
if (!ast3.ok) return returnOrThrow(ast3, throws);
|
|
7246
|
-
const
|
|
7365
|
+
const ast4 = insertNeededGuardJoins(ast3.data, schema, guards.data, autoJoin);
|
|
7366
|
+
if (!ast4.ok) return returnOrThrow(ast4, throws);
|
|
7367
|
+
const san = applyGuards(ast4.data, guards.data, limit);
|
|
7247
7368
|
if (!san.ok) return returnOrThrow(san, throws);
|
|
7248
7369
|
const res = outputSql(san.data, pretty);
|
|
7249
7370
|
if (throws) return res;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-sql",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "A starter for creating a TypeScript package.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"test": "vp test",
|
|
37
37
|
"check": "vp check",
|
|
38
38
|
"prepublishOnly": "vp run build",
|
|
39
|
-
"
|
|
39
|
+
"ohm": "ohm generateBundles --withTypes --esm 'src/sql.ohm'",
|
|
40
40
|
"script": "vp exec tsx"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|