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 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 san = applyGuards(ast3.data, guards.data, limit);
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.2.3",
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
- "gen:types": "ohm generateBundles --withTypes --esm 'src/sql.ohm'",
39
+ "ohm": "ohm generateBundles --withTypes --esm 'src/sql.ohm'",
40
40
  "script": "vp exec tsx"
41
41
  },
42
42
  "dependencies": {