@geekmidas/db 10.0.0-alpha.15 → 10.0.0-alpha.151
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 +17 -0
- package/dist/errors.d.cts.map +1 -1
- package/dist/errors.d.mts.map +1 -1
- package/dist/introspect/DataBrowser.cjs +117 -0
- package/dist/introspect/DataBrowser.cjs.map +1 -0
- package/dist/introspect/DataBrowser.d.cts +51 -0
- package/dist/introspect/DataBrowser.d.cts.map +1 -0
- package/dist/introspect/DataBrowser.d.mts +51 -0
- package/dist/introspect/DataBrowser.d.mts.map +1 -0
- package/dist/introspect/DataBrowser.mjs +116 -0
- package/dist/introspect/DataBrowser.mjs.map +1 -0
- package/dist/introspect/errors.cjs +48 -0
- package/dist/introspect/errors.cjs.map +1 -0
- package/dist/introspect/errors.d.cts +27 -0
- package/dist/introspect/errors.d.cts.map +1 -0
- package/dist/introspect/errors.d.mts +27 -0
- package/dist/introspect/errors.d.mts.map +1 -0
- package/dist/introspect/errors.mjs +45 -0
- package/dist/introspect/errors.mjs.map +1 -0
- package/dist/introspect/filtering.cjs +101 -0
- package/dist/introspect/filtering.cjs.map +1 -0
- package/dist/introspect/filtering.d.cts +29 -0
- package/dist/introspect/filtering.d.cts.map +1 -0
- package/dist/introspect/filtering.d.mts +29 -0
- package/dist/introspect/filtering.d.mts.map +1 -0
- package/dist/introspect/filtering.mjs +98 -0
- package/dist/introspect/filtering.mjs.map +1 -0
- package/dist/introspect/handler.cjs +159 -0
- package/dist/introspect/handler.cjs.map +1 -0
- package/dist/introspect/handler.d.cts +43 -0
- package/dist/introspect/handler.d.cts.map +1 -0
- package/dist/introspect/handler.d.mts +43 -0
- package/dist/introspect/handler.d.mts.map +1 -0
- package/dist/introspect/handler.mjs +158 -0
- package/dist/introspect/handler.mjs.map +1 -0
- package/dist/introspect/index.cjs +22 -0
- package/dist/introspect/index.d.cts +8 -0
- package/dist/introspect/index.d.mts +8 -0
- package/dist/introspect/index.mjs +8 -0
- package/dist/introspect/introspection.cjs +149 -0
- package/dist/introspect/introspection.cjs.map +1 -0
- package/dist/introspect/introspection.d.cts +25 -0
- package/dist/introspect/introspection.d.cts.map +1 -0
- package/dist/introspect/introspection.d.mts +25 -0
- package/dist/introspect/introspection.d.mts.map +1 -0
- package/dist/introspect/introspection.mjs +146 -0
- package/dist/introspect/introspection.mjs.map +1 -0
- package/dist/introspect/types.cjs +24 -0
- package/dist/introspect/types.cjs.map +1 -0
- package/dist/introspect/types.d.cts +113 -0
- package/dist/introspect/types.d.cts.map +1 -0
- package/dist/introspect/types.d.mts +113 -0
- package/dist/introspect/types.d.mts.map +1 -0
- package/dist/introspect/types.mjs +23 -0
- package/dist/introspect/types.mjs.map +1 -0
- package/dist/kysely/pagination.cjs +1 -0
- package/dist/kysely/pagination.cjs.map +1 -1
- package/dist/kysely/pagination.d.cts +2 -2
- package/dist/kysely/pagination.d.cts.map +1 -1
- package/dist/kysely/pagination.d.mts +2 -2
- package/dist/kysely/pagination.d.mts.map +1 -1
- package/dist/kysely/pagination.mjs +2 -2
- package/dist/kysely/pagination.mjs.map +1 -1
- package/dist/objection/pagination.cjs +1 -0
- package/dist/objection/pagination.cjs.map +1 -1
- package/dist/objection/pagination.d.cts +2 -2
- package/dist/objection/pagination.d.cts.map +1 -1
- package/dist/objection/pagination.d.mts +2 -2
- package/dist/objection/pagination.d.mts.map +1 -1
- package/dist/objection/pagination.mjs +2 -2
- package/dist/objection/pagination.mjs.map +1 -1
- package/dist/pagination-CrU89125.d.cts +42 -0
- package/dist/pagination-CrU89125.d.cts.map +1 -0
- package/dist/pagination-CrU89125.d.mts +42 -0
- package/dist/pagination-CrU89125.d.mts.map +1 -0
- package/dist/pagination.cjs +17 -1
- package/dist/pagination.cjs.map +1 -1
- package/dist/pagination.d.cts +2 -30
- package/dist/pagination.d.mts +2 -30
- package/dist/pagination.mjs +17 -2
- package/dist/pagination.mjs.map +1 -1
- package/dist/pg/roles.cjs +42 -4
- package/dist/pg/roles.cjs.map +1 -1
- package/dist/pg/roles.d.cts +19 -0
- package/dist/pg/roles.d.cts.map +1 -1
- package/dist/pg/roles.d.mts +19 -0
- package/dist/pg/roles.d.mts.map +1 -1
- package/dist/pg/roles.mjs +42 -4
- package/dist/pg/roles.mjs.map +1 -1
- package/package.json +13 -2
- package/dist/pagination.d.cts.map +0 -1
- package/dist/pagination.d.mts.map +0 -1
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
require("../pagination.cjs");
|
|
3
|
+
const require_introspect_errors = require("./errors.cjs");
|
|
4
|
+
require("./types.cjs");
|
|
5
|
+
//#region src/introspect/filtering.ts
|
|
6
|
+
const NULLS = ["is_null", "is_not_null"];
|
|
7
|
+
const EQUALITY = [
|
|
8
|
+
"eq",
|
|
9
|
+
"neq",
|
|
10
|
+
...NULLS
|
|
11
|
+
];
|
|
12
|
+
const ORDERED = [
|
|
13
|
+
...EQUALITY,
|
|
14
|
+
"gt",
|
|
15
|
+
"gte",
|
|
16
|
+
"lt",
|
|
17
|
+
"lte"
|
|
18
|
+
];
|
|
19
|
+
const MEMBERSHIP = ["in", "nin"];
|
|
20
|
+
/** Which operators make sense for each column type. */
|
|
21
|
+
const OPERATORS = {
|
|
22
|
+
string: [
|
|
23
|
+
...EQUALITY,
|
|
24
|
+
"like",
|
|
25
|
+
"ilike",
|
|
26
|
+
...MEMBERSHIP
|
|
27
|
+
],
|
|
28
|
+
number: [...ORDERED, ...MEMBERSHIP],
|
|
29
|
+
boolean: EQUALITY,
|
|
30
|
+
date: ORDERED,
|
|
31
|
+
datetime: ORDERED,
|
|
32
|
+
uuid: [...EQUALITY, ...MEMBERSHIP],
|
|
33
|
+
json: NULLS,
|
|
34
|
+
binary: NULLS,
|
|
35
|
+
unknown: EQUALITY
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Whether a filter's operator applies to the column it names.
|
|
39
|
+
*/
|
|
40
|
+
function validateFilter(filter, column) {
|
|
41
|
+
if (OPERATORS[column.type].includes(filter.operator)) return { valid: true };
|
|
42
|
+
return {
|
|
43
|
+
valid: false,
|
|
44
|
+
error: new require_introspect_errors.UnsupportedFilterOperator(filter.operator, column.name, column.type)
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Adds each filter to the query as a `where`, after checking the column exists
|
|
49
|
+
* and the operator applies to it.
|
|
50
|
+
*
|
|
51
|
+
* @throws {ColumnNotFound} a filter names a column the table does not have
|
|
52
|
+
* @throws {UnsupportedFilterOperator} an operator does not apply to its column
|
|
53
|
+
*/
|
|
54
|
+
function applyFilters(query, filters, table) {
|
|
55
|
+
let result = query;
|
|
56
|
+
for (const filter of filters) {
|
|
57
|
+
const column = table.columns.find((c) => c.name === filter.column);
|
|
58
|
+
if (!column) throw new require_introspect_errors.ColumnNotFound(filter.column, table.name);
|
|
59
|
+
const validation = validateFilter(filter, column);
|
|
60
|
+
if (!validation.valid) throw validation.error;
|
|
61
|
+
result = applyFilterCondition(result, filter);
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
function applyFilterCondition(query, filter) {
|
|
66
|
+
const column = filter.column;
|
|
67
|
+
const { value } = filter;
|
|
68
|
+
switch (filter.operator) {
|
|
69
|
+
case "eq": return query.where(column, "=", value);
|
|
70
|
+
case "neq": return query.where(column, "!=", value);
|
|
71
|
+
case "gt": return query.where(column, ">", value);
|
|
72
|
+
case "gte": return query.where(column, ">=", value);
|
|
73
|
+
case "lt": return query.where(column, "<", value);
|
|
74
|
+
case "lte": return query.where(column, "<=", value);
|
|
75
|
+
case "like": return query.where(column, "like", value);
|
|
76
|
+
case "ilike": return query.where(column, "ilike", value);
|
|
77
|
+
case "in": return query.where(column, "in", value);
|
|
78
|
+
case "nin": return query.where(column, "not in", value);
|
|
79
|
+
case "is_null": return query.where(column, "is", null);
|
|
80
|
+
case "is_not_null": return query.where(column, "is not", null);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Adds an `order by` per sort, in the order given.
|
|
85
|
+
*
|
|
86
|
+
* @throws {ColumnNotFound} a sort names a column the table does not have
|
|
87
|
+
*/
|
|
88
|
+
function applySorting(query, sorts, table) {
|
|
89
|
+
let result = query;
|
|
90
|
+
for (const sort of sorts) {
|
|
91
|
+
if (!table.columns.some((c) => c.name === sort.column)) throw new require_introspect_errors.ColumnNotFound(sort.column, table.name);
|
|
92
|
+
result = result.orderBy(sort.column, sort.direction === "asc" ? "asc" : "desc");
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
//#endregion
|
|
97
|
+
exports.applyFilters = applyFilters;
|
|
98
|
+
exports.applySorting = applySorting;
|
|
99
|
+
exports.validateFilter = validateFilter;
|
|
100
|
+
|
|
101
|
+
//# sourceMappingURL=filtering.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filtering.cjs","names":["UnsupportedFilterOperator","ColumnNotFound"],"sources":["../../src/introspect/filtering.ts"],"sourcesContent":["import type { SelectQueryBuilder } from 'kysely';\nimport { Direction } from '../pagination';\nimport { ColumnNotFound, UnsupportedFilterOperator } from './errors';\nimport {\n\ttype ColumnInfo,\n\ttype ColumnType,\n\ttype FilterCondition,\n\tFilterOperator,\n\ttype SortConfig,\n\ttype TableInfo,\n} from './types';\n\nconst NULLS = [FilterOperator.IsNull, FilterOperator.IsNotNull];\nconst EQUALITY = [FilterOperator.Eq, FilterOperator.Neq, ...NULLS];\nconst ORDERED = [\n\t...EQUALITY,\n\tFilterOperator.Gt,\n\tFilterOperator.Gte,\n\tFilterOperator.Lt,\n\tFilterOperator.Lte,\n];\nconst MEMBERSHIP = [FilterOperator.In, FilterOperator.Nin];\n\n/** Which operators make sense for each column type. */\nconst OPERATORS: Record<ColumnType, FilterOperator[]> = {\n\tstring: [\n\t\t...EQUALITY,\n\t\tFilterOperator.Like,\n\t\tFilterOperator.Ilike,\n\t\t...MEMBERSHIP,\n\t],\n\tnumber: [...ORDERED, ...MEMBERSHIP],\n\tboolean: EQUALITY,\n\tdate: ORDERED,\n\tdatetime: ORDERED,\n\tuuid: [...EQUALITY, ...MEMBERSHIP],\n\tjson: NULLS,\n\tbinary: NULLS,\n\tunknown: EQUALITY,\n};\n\n/**\n * Whether a filter's operator applies to the column it names.\n */\nexport function validateFilter(\n\tfilter: FilterCondition,\n\tcolumn: ColumnInfo,\n): { valid: true } | { valid: false; error: UnsupportedFilterOperator } {\n\tif (OPERATORS[column.type].includes(filter.operator)) {\n\t\treturn { valid: true };\n\t}\n\n\treturn {\n\t\tvalid: false,\n\t\terror: new UnsupportedFilterOperator(\n\t\t\tfilter.operator,\n\t\t\tcolumn.name,\n\t\t\tcolumn.type,\n\t\t),\n\t};\n}\n\n/**\n * Adds each filter to the query as a `where`, after checking the column exists\n * and the operator applies to it.\n *\n * @throws {ColumnNotFound} a filter names a column the table does not have\n * @throws {UnsupportedFilterOperator} an operator does not apply to its column\n */\nexport function applyFilters<DB, TB extends keyof DB, O>(\n\tquery: SelectQueryBuilder<DB, TB, O>,\n\tfilters: FilterCondition[],\n\ttable: TableInfo,\n): SelectQueryBuilder<DB, TB, O> {\n\tlet result = query;\n\n\tfor (const filter of filters) {\n\t\tconst column = table.columns.find((c) => c.name === filter.column);\n\t\tif (!column) {\n\t\t\tthrow new ColumnNotFound(filter.column, table.name);\n\t\t}\n\n\t\tconst validation = validateFilter(filter, column);\n\t\tif (!validation.valid) {\n\t\t\tthrow validation.error;\n\t\t}\n\n\t\tresult = applyFilterCondition(result, filter);\n\t}\n\n\treturn result;\n}\n\nfunction applyFilterCondition<DB, TB extends keyof DB, O>(\n\tquery: SelectQueryBuilder<DB, TB, O>,\n\tfilter: FilterCondition,\n): SelectQueryBuilder<DB, TB, O> {\n\t// The column was checked against the introspected table, which the query's\n\t// static types know nothing about.\n\tconst column = filter.column as any;\n\tconst { value } = filter;\n\n\tswitch (filter.operator) {\n\t\tcase FilterOperator.Eq:\n\t\t\treturn query.where(column, '=', value);\n\t\tcase FilterOperator.Neq:\n\t\t\treturn query.where(column, '!=', value);\n\t\tcase FilterOperator.Gt:\n\t\t\treturn query.where(column, '>', value);\n\t\tcase FilterOperator.Gte:\n\t\t\treturn query.where(column, '>=', value);\n\t\tcase FilterOperator.Lt:\n\t\t\treturn query.where(column, '<', value);\n\t\tcase FilterOperator.Lte:\n\t\t\treturn query.where(column, '<=', value);\n\t\tcase FilterOperator.Like:\n\t\t\treturn query.where(column, 'like', value);\n\t\tcase FilterOperator.Ilike:\n\t\t\treturn query.where(column, 'ilike', value);\n\t\tcase FilterOperator.In:\n\t\t\treturn query.where(column, 'in', value as unknown[]);\n\t\tcase FilterOperator.Nin:\n\t\t\treturn query.where(column, 'not in', value as unknown[]);\n\t\tcase FilterOperator.IsNull:\n\t\t\treturn query.where(column, 'is', null);\n\t\tcase FilterOperator.IsNotNull:\n\t\t\treturn query.where(column, 'is not', null);\n\t}\n}\n\n/**\n * Adds an `order by` per sort, in the order given.\n *\n * @throws {ColumnNotFound} a sort names a column the table does not have\n */\nexport function applySorting<DB, TB extends keyof DB, O>(\n\tquery: SelectQueryBuilder<DB, TB, O>,\n\tsorts: SortConfig[],\n\ttable: TableInfo,\n): SelectQueryBuilder<DB, TB, O> {\n\tlet result = query;\n\n\tfor (const sort of sorts) {\n\t\tif (!table.columns.some((c) => c.name === sort.column)) {\n\t\t\tthrow new ColumnNotFound(sort.column, table.name);\n\t\t}\n\n\t\tresult = result.orderBy(\n\t\t\tsort.column as any,\n\t\t\tsort.direction === Direction.Asc ? 'asc' : 'desc',\n\t\t);\n\t}\n\n\treturn result;\n}\n"],"mappings":";;;;;AAYA,MAAM,QAAQ,CAAA,WAAA,aAAgD;AAC9D,MAAM,WAAW;;;CAAwC,GAAG;AAAK;AACjE,MAAM,UAAU;CACf,GAAG;;;;;AAKJ;AACA,MAAM,aAAa,CAAA,MAAA,KAAsC;;AAGzD,MAAM,YAAkD;CACvD,QAAQ;EACP,GAAG;;;EAGH,GAAG;CACJ;CACA,QAAQ,CAAC,GAAG,SAAS,GAAG,UAAU;CAClC,SAAS;CACT,MAAM;CACN,UAAU;CACV,MAAM,CAAC,GAAG,UAAU,GAAG,UAAU;CACjC,MAAM;CACN,QAAQ;CACR,SAAS;AACV;;;;AAKA,SAAgB,eACf,QACA,QACuE;CACvE,IAAI,UAAU,OAAO,KAAK,CAAC,SAAS,OAAO,QAAQ,GAClD,OAAO,EAAE,OAAO,KAAK;CAGtB,OAAO;EACN,OAAO;EACP,OAAO,IAAIA,0BAAAA,0BACV,OAAO,UACP,OAAO,MACP,OAAO,IACR;CACD;AACD;;;;;;;;AASA,SAAgB,aACf,OACA,SACA,OACgC;CAChC,IAAI,SAAS;CAEb,KAAK,MAAM,UAAU,SAAS;EAC7B,MAAM,SAAS,MAAM,QAAQ,MAAM,MAAM,EAAE,SAAS,OAAO,MAAM;EACjE,IAAI,CAAC,QACJ,MAAM,IAAIC,0BAAAA,eAAe,OAAO,QAAQ,MAAM,IAAI;EAGnD,MAAM,aAAa,eAAe,QAAQ,MAAM;EAChD,IAAI,CAAC,WAAW,OACf,MAAM,WAAW;EAGlB,SAAS,qBAAqB,QAAQ,MAAM;CAC7C;CAEA,OAAO;AACR;AAEA,SAAS,qBACR,OACA,QACgC;CAGhC,MAAM,SAAS,OAAO;CACtB,MAAM,EAAE,UAAU;CAElB,QAAQ,OAAO,UAAf;EACC,KAAA,MACC,OAAO,MAAM,MAAM,QAAQ,KAAK,KAAK;EACtC,KAAA,OACC,OAAO,MAAM,MAAM,QAAQ,MAAM,KAAK;EACvC,KAAA,MACC,OAAO,MAAM,MAAM,QAAQ,KAAK,KAAK;EACtC,KAAA,OACC,OAAO,MAAM,MAAM,QAAQ,MAAM,KAAK;EACvC,KAAA,MACC,OAAO,MAAM,MAAM,QAAQ,KAAK,KAAK;EACtC,KAAA,OACC,OAAO,MAAM,MAAM,QAAQ,MAAM,KAAK;EACvC,KAAA,QACC,OAAO,MAAM,MAAM,QAAQ,QAAQ,KAAK;EACzC,KAAA,SACC,OAAO,MAAM,MAAM,QAAQ,SAAS,KAAK;EAC1C,KAAA,MACC,OAAO,MAAM,MAAM,QAAQ,MAAM,KAAkB;EACpD,KAAA,OACC,OAAO,MAAM,MAAM,QAAQ,UAAU,KAAkB;EACxD,KAAA,WACC,OAAO,MAAM,MAAM,QAAQ,MAAM,IAAI;EACtC,KAAA,eACC,OAAO,MAAM,MAAM,QAAQ,UAAU,IAAI;CAC3C;AACD;;;;;;AAOA,SAAgB,aACf,OACA,OACA,OACgC;CAChC,IAAI,SAAS;CAEb,KAAK,MAAM,QAAQ,OAAO;EACzB,IAAI,CAAC,MAAM,QAAQ,MAAM,MAAM,EAAE,SAAS,KAAK,MAAM,GACpD,MAAM,IAAIA,0BAAAA,eAAe,KAAK,QAAQ,MAAM,IAAI;EAGjD,SAAS,OAAO,QACf,KAAK,QACL,KAAK,cAAA,QAA8B,QAAQ,MAC5C;CACD;CAEA,OAAO;AACR"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ColumnInfo, FilterCondition, SortConfig, TableInfo } from "./types.cjs";
|
|
2
|
+
import { UnsupportedFilterOperator } from "./errors.cjs";
|
|
3
|
+
import { SelectQueryBuilder } from "kysely";
|
|
4
|
+
//#region src/introspect/filtering.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Whether a filter's operator applies to the column it names.
|
|
7
|
+
*/
|
|
8
|
+
export declare function validateFilter(filter: FilterCondition, column: ColumnInfo): {
|
|
9
|
+
valid: true;
|
|
10
|
+
} | {
|
|
11
|
+
valid: false;
|
|
12
|
+
error: UnsupportedFilterOperator;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Adds each filter to the query as a `where`, after checking the column exists
|
|
16
|
+
* and the operator applies to it.
|
|
17
|
+
*
|
|
18
|
+
* @throws {ColumnNotFound} a filter names a column the table does not have
|
|
19
|
+
* @throws {UnsupportedFilterOperator} an operator does not apply to its column
|
|
20
|
+
*/
|
|
21
|
+
export declare function applyFilters<DB, TB extends keyof DB, O>(query: SelectQueryBuilder<DB, TB, O>, filters: FilterCondition[], table: TableInfo): SelectQueryBuilder<DB, TB, O>;
|
|
22
|
+
/**
|
|
23
|
+
* Adds an `order by` per sort, in the order given.
|
|
24
|
+
*
|
|
25
|
+
* @throws {ColumnNotFound} a sort names a column the table does not have
|
|
26
|
+
*/
|
|
27
|
+
export declare function applySorting<DB, TB extends keyof DB, O>(query: SelectQueryBuilder<DB, TB, O>, sorts: SortConfig[], table: TableInfo): SelectQueryBuilder<DB, TB, O>;
|
|
28
|
+
//#endregion
|
|
29
|
+
//# sourceMappingURL=filtering.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filtering.d.cts","names":[],"sources":["../../src/introspect/filtering.ts"],"mappings":";;;;;;;wBA4CgB,eACf,QAAQ,iBACR,QAAQ;EACJ;;EAAkB;EAAc,OAAO;;;;;;;;;wBAsB5B,aAAa,IAAI,iBAAiB,IAAI,GACrD,OAAO,mBAAmB,IAAI,IAAI,IAClC,SAAS,mBACT,OAAO,YACL,mBAAmB,IAAI,IAAI;;;;;;wBA8Dd,aAAa,IAAI,iBAAiB,IAAI,GACrD,OAAO,mBAAmB,IAAI,IAAI,IAClC,OAAO,cACP,OAAO,YACL,mBAAmB,IAAI,IAAI"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ColumnInfo, FilterCondition, SortConfig, TableInfo } from "./types.mjs";
|
|
2
|
+
import { UnsupportedFilterOperator } from "./errors.mjs";
|
|
3
|
+
import { SelectQueryBuilder } from "kysely";
|
|
4
|
+
//#region src/introspect/filtering.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Whether a filter's operator applies to the column it names.
|
|
7
|
+
*/
|
|
8
|
+
export declare function validateFilter(filter: FilterCondition, column: ColumnInfo): {
|
|
9
|
+
valid: true;
|
|
10
|
+
} | {
|
|
11
|
+
valid: false;
|
|
12
|
+
error: UnsupportedFilterOperator;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Adds each filter to the query as a `where`, after checking the column exists
|
|
16
|
+
* and the operator applies to it.
|
|
17
|
+
*
|
|
18
|
+
* @throws {ColumnNotFound} a filter names a column the table does not have
|
|
19
|
+
* @throws {UnsupportedFilterOperator} an operator does not apply to its column
|
|
20
|
+
*/
|
|
21
|
+
export declare function applyFilters<DB, TB extends keyof DB, O>(query: SelectQueryBuilder<DB, TB, O>, filters: FilterCondition[], table: TableInfo): SelectQueryBuilder<DB, TB, O>;
|
|
22
|
+
/**
|
|
23
|
+
* Adds an `order by` per sort, in the order given.
|
|
24
|
+
*
|
|
25
|
+
* @throws {ColumnNotFound} a sort names a column the table does not have
|
|
26
|
+
*/
|
|
27
|
+
export declare function applySorting<DB, TB extends keyof DB, O>(query: SelectQueryBuilder<DB, TB, O>, sorts: SortConfig[], table: TableInfo): SelectQueryBuilder<DB, TB, O>;
|
|
28
|
+
//#endregion
|
|
29
|
+
//# sourceMappingURL=filtering.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filtering.d.mts","names":[],"sources":["../../src/introspect/filtering.ts"],"mappings":";;;;;;;wBA4CgB,eACf,QAAQ,iBACR,QAAQ;EACJ;;EAAkB;EAAc,OAAO;;;;;;;;;wBAsB5B,aAAa,IAAI,iBAAiB,IAAI,GACrD,OAAO,mBAAmB,IAAI,IAAI,IAClC,SAAS,mBACT,OAAO,YACL,mBAAmB,IAAI,IAAI;;;;;;wBA8Dd,aAAa,IAAI,iBAAiB,IAAI,GACrD,OAAO,mBAAmB,IAAI,IAAI,IAClC,OAAO,cACP,OAAO,YACL,mBAAmB,IAAI,IAAI"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import "../pagination.mjs";
|
|
2
|
+
import { ColumnNotFound, UnsupportedFilterOperator } from "./errors.mjs";
|
|
3
|
+
import "./types.mjs";
|
|
4
|
+
//#region src/introspect/filtering.ts
|
|
5
|
+
const NULLS = ["is_null", "is_not_null"];
|
|
6
|
+
const EQUALITY = [
|
|
7
|
+
"eq",
|
|
8
|
+
"neq",
|
|
9
|
+
...NULLS
|
|
10
|
+
];
|
|
11
|
+
const ORDERED = [
|
|
12
|
+
...EQUALITY,
|
|
13
|
+
"gt",
|
|
14
|
+
"gte",
|
|
15
|
+
"lt",
|
|
16
|
+
"lte"
|
|
17
|
+
];
|
|
18
|
+
const MEMBERSHIP = ["in", "nin"];
|
|
19
|
+
/** Which operators make sense for each column type. */
|
|
20
|
+
const OPERATORS = {
|
|
21
|
+
string: [
|
|
22
|
+
...EQUALITY,
|
|
23
|
+
"like",
|
|
24
|
+
"ilike",
|
|
25
|
+
...MEMBERSHIP
|
|
26
|
+
],
|
|
27
|
+
number: [...ORDERED, ...MEMBERSHIP],
|
|
28
|
+
boolean: EQUALITY,
|
|
29
|
+
date: ORDERED,
|
|
30
|
+
datetime: ORDERED,
|
|
31
|
+
uuid: [...EQUALITY, ...MEMBERSHIP],
|
|
32
|
+
json: NULLS,
|
|
33
|
+
binary: NULLS,
|
|
34
|
+
unknown: EQUALITY
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Whether a filter's operator applies to the column it names.
|
|
38
|
+
*/
|
|
39
|
+
function validateFilter(filter, column) {
|
|
40
|
+
if (OPERATORS[column.type].includes(filter.operator)) return { valid: true };
|
|
41
|
+
return {
|
|
42
|
+
valid: false,
|
|
43
|
+
error: new UnsupportedFilterOperator(filter.operator, column.name, column.type)
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Adds each filter to the query as a `where`, after checking the column exists
|
|
48
|
+
* and the operator applies to it.
|
|
49
|
+
*
|
|
50
|
+
* @throws {ColumnNotFound} a filter names a column the table does not have
|
|
51
|
+
* @throws {UnsupportedFilterOperator} an operator does not apply to its column
|
|
52
|
+
*/
|
|
53
|
+
function applyFilters(query, filters, table) {
|
|
54
|
+
let result = query;
|
|
55
|
+
for (const filter of filters) {
|
|
56
|
+
const column = table.columns.find((c) => c.name === filter.column);
|
|
57
|
+
if (!column) throw new ColumnNotFound(filter.column, table.name);
|
|
58
|
+
const validation = validateFilter(filter, column);
|
|
59
|
+
if (!validation.valid) throw validation.error;
|
|
60
|
+
result = applyFilterCondition(result, filter);
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
function applyFilterCondition(query, filter) {
|
|
65
|
+
const column = filter.column;
|
|
66
|
+
const { value } = filter;
|
|
67
|
+
switch (filter.operator) {
|
|
68
|
+
case "eq": return query.where(column, "=", value);
|
|
69
|
+
case "neq": return query.where(column, "!=", value);
|
|
70
|
+
case "gt": return query.where(column, ">", value);
|
|
71
|
+
case "gte": return query.where(column, ">=", value);
|
|
72
|
+
case "lt": return query.where(column, "<", value);
|
|
73
|
+
case "lte": return query.where(column, "<=", value);
|
|
74
|
+
case "like": return query.where(column, "like", value);
|
|
75
|
+
case "ilike": return query.where(column, "ilike", value);
|
|
76
|
+
case "in": return query.where(column, "in", value);
|
|
77
|
+
case "nin": return query.where(column, "not in", value);
|
|
78
|
+
case "is_null": return query.where(column, "is", null);
|
|
79
|
+
case "is_not_null": return query.where(column, "is not", null);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Adds an `order by` per sort, in the order given.
|
|
84
|
+
*
|
|
85
|
+
* @throws {ColumnNotFound} a sort names a column the table does not have
|
|
86
|
+
*/
|
|
87
|
+
function applySorting(query, sorts, table) {
|
|
88
|
+
let result = query;
|
|
89
|
+
for (const sort of sorts) {
|
|
90
|
+
if (!table.columns.some((c) => c.name === sort.column)) throw new ColumnNotFound(sort.column, table.name);
|
|
91
|
+
result = result.orderBy(sort.column, sort.direction === "asc" ? "asc" : "desc");
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
//#endregion
|
|
96
|
+
export { applyFilters, applySorting, validateFilter };
|
|
97
|
+
|
|
98
|
+
//# sourceMappingURL=filtering.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filtering.mjs","names":[],"sources":["../../src/introspect/filtering.ts"],"sourcesContent":["import type { SelectQueryBuilder } from 'kysely';\nimport { Direction } from '../pagination';\nimport { ColumnNotFound, UnsupportedFilterOperator } from './errors';\nimport {\n\ttype ColumnInfo,\n\ttype ColumnType,\n\ttype FilterCondition,\n\tFilterOperator,\n\ttype SortConfig,\n\ttype TableInfo,\n} from './types';\n\nconst NULLS = [FilterOperator.IsNull, FilterOperator.IsNotNull];\nconst EQUALITY = [FilterOperator.Eq, FilterOperator.Neq, ...NULLS];\nconst ORDERED = [\n\t...EQUALITY,\n\tFilterOperator.Gt,\n\tFilterOperator.Gte,\n\tFilterOperator.Lt,\n\tFilterOperator.Lte,\n];\nconst MEMBERSHIP = [FilterOperator.In, FilterOperator.Nin];\n\n/** Which operators make sense for each column type. */\nconst OPERATORS: Record<ColumnType, FilterOperator[]> = {\n\tstring: [\n\t\t...EQUALITY,\n\t\tFilterOperator.Like,\n\t\tFilterOperator.Ilike,\n\t\t...MEMBERSHIP,\n\t],\n\tnumber: [...ORDERED, ...MEMBERSHIP],\n\tboolean: EQUALITY,\n\tdate: ORDERED,\n\tdatetime: ORDERED,\n\tuuid: [...EQUALITY, ...MEMBERSHIP],\n\tjson: NULLS,\n\tbinary: NULLS,\n\tunknown: EQUALITY,\n};\n\n/**\n * Whether a filter's operator applies to the column it names.\n */\nexport function validateFilter(\n\tfilter: FilterCondition,\n\tcolumn: ColumnInfo,\n): { valid: true } | { valid: false; error: UnsupportedFilterOperator } {\n\tif (OPERATORS[column.type].includes(filter.operator)) {\n\t\treturn { valid: true };\n\t}\n\n\treturn {\n\t\tvalid: false,\n\t\terror: new UnsupportedFilterOperator(\n\t\t\tfilter.operator,\n\t\t\tcolumn.name,\n\t\t\tcolumn.type,\n\t\t),\n\t};\n}\n\n/**\n * Adds each filter to the query as a `where`, after checking the column exists\n * and the operator applies to it.\n *\n * @throws {ColumnNotFound} a filter names a column the table does not have\n * @throws {UnsupportedFilterOperator} an operator does not apply to its column\n */\nexport function applyFilters<DB, TB extends keyof DB, O>(\n\tquery: SelectQueryBuilder<DB, TB, O>,\n\tfilters: FilterCondition[],\n\ttable: TableInfo,\n): SelectQueryBuilder<DB, TB, O> {\n\tlet result = query;\n\n\tfor (const filter of filters) {\n\t\tconst column = table.columns.find((c) => c.name === filter.column);\n\t\tif (!column) {\n\t\t\tthrow new ColumnNotFound(filter.column, table.name);\n\t\t}\n\n\t\tconst validation = validateFilter(filter, column);\n\t\tif (!validation.valid) {\n\t\t\tthrow validation.error;\n\t\t}\n\n\t\tresult = applyFilterCondition(result, filter);\n\t}\n\n\treturn result;\n}\n\nfunction applyFilterCondition<DB, TB extends keyof DB, O>(\n\tquery: SelectQueryBuilder<DB, TB, O>,\n\tfilter: FilterCondition,\n): SelectQueryBuilder<DB, TB, O> {\n\t// The column was checked against the introspected table, which the query's\n\t// static types know nothing about.\n\tconst column = filter.column as any;\n\tconst { value } = filter;\n\n\tswitch (filter.operator) {\n\t\tcase FilterOperator.Eq:\n\t\t\treturn query.where(column, '=', value);\n\t\tcase FilterOperator.Neq:\n\t\t\treturn query.where(column, '!=', value);\n\t\tcase FilterOperator.Gt:\n\t\t\treturn query.where(column, '>', value);\n\t\tcase FilterOperator.Gte:\n\t\t\treturn query.where(column, '>=', value);\n\t\tcase FilterOperator.Lt:\n\t\t\treturn query.where(column, '<', value);\n\t\tcase FilterOperator.Lte:\n\t\t\treturn query.where(column, '<=', value);\n\t\tcase FilterOperator.Like:\n\t\t\treturn query.where(column, 'like', value);\n\t\tcase FilterOperator.Ilike:\n\t\t\treturn query.where(column, 'ilike', value);\n\t\tcase FilterOperator.In:\n\t\t\treturn query.where(column, 'in', value as unknown[]);\n\t\tcase FilterOperator.Nin:\n\t\t\treturn query.where(column, 'not in', value as unknown[]);\n\t\tcase FilterOperator.IsNull:\n\t\t\treturn query.where(column, 'is', null);\n\t\tcase FilterOperator.IsNotNull:\n\t\t\treturn query.where(column, 'is not', null);\n\t}\n}\n\n/**\n * Adds an `order by` per sort, in the order given.\n *\n * @throws {ColumnNotFound} a sort names a column the table does not have\n */\nexport function applySorting<DB, TB extends keyof DB, O>(\n\tquery: SelectQueryBuilder<DB, TB, O>,\n\tsorts: SortConfig[],\n\ttable: TableInfo,\n): SelectQueryBuilder<DB, TB, O> {\n\tlet result = query;\n\n\tfor (const sort of sorts) {\n\t\tif (!table.columns.some((c) => c.name === sort.column)) {\n\t\t\tthrow new ColumnNotFound(sort.column, table.name);\n\t\t}\n\n\t\tresult = result.orderBy(\n\t\t\tsort.column as any,\n\t\t\tsort.direction === Direction.Asc ? 'asc' : 'desc',\n\t\t);\n\t}\n\n\treturn result;\n}\n"],"mappings":";;;;AAYA,MAAM,QAAQ,CAAA,WAAA,aAAgD;AAC9D,MAAM,WAAW;;;CAAwC,GAAG;AAAK;AACjE,MAAM,UAAU;CACf,GAAG;;;;;AAKJ;AACA,MAAM,aAAa,CAAA,MAAA,KAAsC;;AAGzD,MAAM,YAAkD;CACvD,QAAQ;EACP,GAAG;;;EAGH,GAAG;CACJ;CACA,QAAQ,CAAC,GAAG,SAAS,GAAG,UAAU;CAClC,SAAS;CACT,MAAM;CACN,UAAU;CACV,MAAM,CAAC,GAAG,UAAU,GAAG,UAAU;CACjC,MAAM;CACN,QAAQ;CACR,SAAS;AACV;;;;AAKA,SAAgB,eACf,QACA,QACuE;CACvE,IAAI,UAAU,OAAO,KAAK,CAAC,SAAS,OAAO,QAAQ,GAClD,OAAO,EAAE,OAAO,KAAK;CAGtB,OAAO;EACN,OAAO;EACP,OAAO,IAAI,0BACV,OAAO,UACP,OAAO,MACP,OAAO,IACR;CACD;AACD;;;;;;;;AASA,SAAgB,aACf,OACA,SACA,OACgC;CAChC,IAAI,SAAS;CAEb,KAAK,MAAM,UAAU,SAAS;EAC7B,MAAM,SAAS,MAAM,QAAQ,MAAM,MAAM,EAAE,SAAS,OAAO,MAAM;EACjE,IAAI,CAAC,QACJ,MAAM,IAAI,eAAe,OAAO,QAAQ,MAAM,IAAI;EAGnD,MAAM,aAAa,eAAe,QAAQ,MAAM;EAChD,IAAI,CAAC,WAAW,OACf,MAAM,WAAW;EAGlB,SAAS,qBAAqB,QAAQ,MAAM;CAC7C;CAEA,OAAO;AACR;AAEA,SAAS,qBACR,OACA,QACgC;CAGhC,MAAM,SAAS,OAAO;CACtB,MAAM,EAAE,UAAU;CAElB,QAAQ,OAAO,UAAf;EACC,KAAA,MACC,OAAO,MAAM,MAAM,QAAQ,KAAK,KAAK;EACtC,KAAA,OACC,OAAO,MAAM,MAAM,QAAQ,MAAM,KAAK;EACvC,KAAA,MACC,OAAO,MAAM,MAAM,QAAQ,KAAK,KAAK;EACtC,KAAA,OACC,OAAO,MAAM,MAAM,QAAQ,MAAM,KAAK;EACvC,KAAA,MACC,OAAO,MAAM,MAAM,QAAQ,KAAK,KAAK;EACtC,KAAA,OACC,OAAO,MAAM,MAAM,QAAQ,MAAM,KAAK;EACvC,KAAA,QACC,OAAO,MAAM,MAAM,QAAQ,QAAQ,KAAK;EACzC,KAAA,SACC,OAAO,MAAM,MAAM,QAAQ,SAAS,KAAK;EAC1C,KAAA,MACC,OAAO,MAAM,MAAM,QAAQ,MAAM,KAAkB;EACpD,KAAA,OACC,OAAO,MAAM,MAAM,QAAQ,UAAU,KAAkB;EACxD,KAAA,WACC,OAAO,MAAM,MAAM,QAAQ,MAAM,IAAI;EACtC,KAAA,eACC,OAAO,MAAM,MAAM,QAAQ,UAAU,IAAI;CAC3C;AACD;;;;;;AAOA,SAAgB,aACf,OACA,OACA,OACgC;CAChC,IAAI,SAAS;CAEb,KAAK,MAAM,QAAQ,OAAO;EACzB,IAAI,CAAC,MAAM,QAAQ,MAAM,MAAM,EAAE,SAAS,KAAK,MAAM,GACpD,MAAM,IAAI,eAAe,KAAK,QAAQ,MAAM,IAAI;EAGjD,SAAS,OAAO,QACf,KAAK,QACL,KAAK,cAAA,QAA8B,QAAQ,MAC5C;CACD;CAEA,OAAO;AACR"}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_pagination = require("../pagination.cjs");
|
|
3
|
+
const require_introspect_errors = require("./errors.cjs");
|
|
4
|
+
const require_introspect_types = require("./types.cjs");
|
|
5
|
+
const require_introspect_DataBrowser = require("./DataBrowser.cjs");
|
|
6
|
+
//#region src/introspect/handler.ts
|
|
7
|
+
const OPERATORS = new Set(Object.values(require_introspect_types.FilterOperator));
|
|
8
|
+
/**
|
|
9
|
+
* A read-only JSON API over a Postgres database's structure and rows.
|
|
10
|
+
*
|
|
11
|
+
* A plain `(Request) => Promise<Response>` rather than a router, so it mounts
|
|
12
|
+
* in anything that speaks web `Request`s — Hono, Bun, Deno, a Node adapter —
|
|
13
|
+
* and `@geekmidas/db` takes on no HTTP framework to provide it.
|
|
14
|
+
*
|
|
15
|
+
* | Route | Returns |
|
|
16
|
+
* | ----------------------------- | ----------------------------------------------- |
|
|
17
|
+
* | `GET /schemas` | `{ schemas, browsable }` |
|
|
18
|
+
* | `GET /tables?schema=&refresh=`| `{ tables }` with columns and foreign keys |
|
|
19
|
+
* | `GET /tables/:name?schema=` | one table |
|
|
20
|
+
* | `GET /tables/:name/rows` | a page of rows: `{ rows, hasMore, nextCursor }` |
|
|
21
|
+
*
|
|
22
|
+
* Rows take `pageSize` (≤ 100), `cursor`, `sort=col:asc,col2:desc` and filters
|
|
23
|
+
* as `filter[column][operator]=value` — `in`/`nin` take a comma-separated
|
|
24
|
+
* list, `is_null`/`is_not_null` take no value.
|
|
25
|
+
*
|
|
26
|
+
* It exposes every row of every browsable table to whoever can reach it. Mount
|
|
27
|
+
* it only where that is the developer: `gkm dev` serves it at `/__gkm/db` and
|
|
28
|
+
* nothing in a production build does.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* const handler = createIntrospectionHandler({ db, basePath: '/__gkm/db' });
|
|
33
|
+
* app.all('/__gkm/db/*', (c) => handler(c.req.raw));
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
function createIntrospectionHandler(options) {
|
|
37
|
+
const browser = new require_introspect_DataBrowser.DataBrowser(options);
|
|
38
|
+
const basePath = (options.basePath ?? "").replace(/\/+$/, "");
|
|
39
|
+
return async (request) => {
|
|
40
|
+
if (request.method !== "GET") return json({
|
|
41
|
+
error: "MethodNotAllowed",
|
|
42
|
+
message: "The database API is read-only; only GET is served."
|
|
43
|
+
}, 405, { allow: "GET" });
|
|
44
|
+
const url = new URL(request.url);
|
|
45
|
+
const path = url.pathname.startsWith(basePath) ? url.pathname.slice(basePath.length) : url.pathname;
|
|
46
|
+
const segments = path.split("/").filter(Boolean).map(decodeURIComponent);
|
|
47
|
+
const schema = url.searchParams.get("schema") ?? void 0;
|
|
48
|
+
try {
|
|
49
|
+
if (segments.length === 1 && segments[0] === "schemas") return json({
|
|
50
|
+
schemas: await browser.listSchemas(),
|
|
51
|
+
browsable: browser.schemas
|
|
52
|
+
});
|
|
53
|
+
if (segments.length === 1 && segments[0] === "tables") {
|
|
54
|
+
const refresh = url.searchParams.get("refresh") === "true";
|
|
55
|
+
const { tables, updatedAt } = await browser.getSchema(refresh);
|
|
56
|
+
return json({
|
|
57
|
+
tables: schema ? tables.filter((t) => t.schema === schema) : tables,
|
|
58
|
+
updatedAt
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (segments[0] === "tables" && segments[1] && segments.length === 2) {
|
|
62
|
+
const table = await browser.getTableInfo(segments[1], schema);
|
|
63
|
+
if (!table) throw new require_introspect_errors.TableNotFound(segments[1], schema ?? browser.schemas[0] ?? "public");
|
|
64
|
+
return json(table);
|
|
65
|
+
}
|
|
66
|
+
if (segments[0] === "tables" && segments[1] && segments[2] === "rows" && segments.length === 3) {
|
|
67
|
+
const pageSize = Number.parseInt(url.searchParams.get("pageSize") ?? "", 10);
|
|
68
|
+
const filters = parseFilters(url.searchParams);
|
|
69
|
+
const sort = parseSort(url.searchParams.get("sort"));
|
|
70
|
+
return json(await browser.query({
|
|
71
|
+
table: segments[1],
|
|
72
|
+
schema,
|
|
73
|
+
pageSize: Number.isNaN(pageSize) ? void 0 : pageSize,
|
|
74
|
+
cursor: url.searchParams.get("cursor") || void 0,
|
|
75
|
+
filters: filters.length > 0 ? filters : void 0,
|
|
76
|
+
sort: sort.length > 0 ? sort : void 0
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
return json({
|
|
80
|
+
error: "NotFound",
|
|
81
|
+
message: `No database route at '${path || "/"}'. Routes: /schemas, /tables, /tables/:name, /tables/:name/rows.`
|
|
82
|
+
}, 404);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
const status = statusFor(error);
|
|
85
|
+
if (status === void 0) throw error;
|
|
86
|
+
return json({
|
|
87
|
+
error: error.name,
|
|
88
|
+
message: error.message
|
|
89
|
+
}, status);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/** The caller's mistakes, answered; anything else is the server's and thrown. */
|
|
94
|
+
function statusFor(error) {
|
|
95
|
+
if (error instanceof require_introspect_errors.TableNotFound) return 404;
|
|
96
|
+
if (error instanceof require_introspect_errors.ColumnNotFound || error instanceof require_introspect_errors.UnsupportedFilterOperator || error instanceof require_pagination.InvalidCursor) return 400;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* `filter[column][operator]=value`. An unknown operator is ignored rather than
|
|
100
|
+
* rejected, as a misspelt one in a hand-typed URL is easier to see in the
|
|
101
|
+
* results than to debug from an error.
|
|
102
|
+
*/
|
|
103
|
+
function parseFilters(params) {
|
|
104
|
+
const filters = [];
|
|
105
|
+
for (const [key, value] of params) {
|
|
106
|
+
const match = key.match(/^filter\[(\w+)\]\[(\w+)\]$/);
|
|
107
|
+
const column = match?.[1];
|
|
108
|
+
const operator = match?.[2];
|
|
109
|
+
if (!column || !operator || !OPERATORS.has(operator)) continue;
|
|
110
|
+
const op = operator;
|
|
111
|
+
if (op === "in" || op === "nin") filters.push({
|
|
112
|
+
column,
|
|
113
|
+
operator: op,
|
|
114
|
+
value: value.split(",")
|
|
115
|
+
});
|
|
116
|
+
else if (op === "is_null" || op === "is_not_null") filters.push({
|
|
117
|
+
column,
|
|
118
|
+
operator: op
|
|
119
|
+
});
|
|
120
|
+
else filters.push({
|
|
121
|
+
column,
|
|
122
|
+
operator: op,
|
|
123
|
+
value: parseScalar(value)
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return filters;
|
|
127
|
+
}
|
|
128
|
+
function parseScalar(value) {
|
|
129
|
+
if (value === "true") return true;
|
|
130
|
+
if (value === "false") return false;
|
|
131
|
+
if (value !== "" && !Number.isNaN(Number(value))) return Number(value);
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
/** `sort=name:asc,created_at:desc`; a column with no direction is ascending. */
|
|
135
|
+
function parseSort(param) {
|
|
136
|
+
if (!param) return [];
|
|
137
|
+
return param.split(",").flatMap((part) => {
|
|
138
|
+
const [column, direction] = part.split(":");
|
|
139
|
+
if (!column) return [];
|
|
140
|
+
return [{
|
|
141
|
+
column,
|
|
142
|
+
direction: direction === "desc" ? "desc" : "asc"
|
|
143
|
+
}];
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
function json(body, status = 200, headers = {}) {
|
|
147
|
+
const text = JSON.stringify(body, (_key, value) => typeof value === "bigint" ? value.toString() : value);
|
|
148
|
+
return new Response(text, {
|
|
149
|
+
status,
|
|
150
|
+
headers: {
|
|
151
|
+
"content-type": "application/json; charset=utf-8",
|
|
152
|
+
...headers
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
//#endregion
|
|
157
|
+
exports.createIntrospectionHandler = createIntrospectionHandler;
|
|
158
|
+
|
|
159
|
+
//# sourceMappingURL=handler.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.cjs","names":["FilterOperator","DataBrowser","TableNotFound","ColumnNotFound","UnsupportedFilterOperator","InvalidCursor"],"sources":["../../src/introspect/handler.ts"],"sourcesContent":["import { Direction, InvalidCursor } from '../pagination';\nimport { DataBrowser } from './DataBrowser';\nimport {\n\tColumnNotFound,\n\tTableNotFound,\n\tUnsupportedFilterOperator,\n} from './errors';\nimport {\n\ttype DataBrowserOptions,\n\ttype FilterCondition,\n\tFilterOperator,\n\ttype SortConfig,\n} from './types';\n\nexport interface IntrospectionHandlerOptions<DB = unknown>\n\textends DataBrowserOptions<DB> {\n\t/**\n\t * The path the handler is mounted under, e.g. `/__gkm/db`. Stripped from\n\t * each request's path before routing, so the handler can be mounted\n\t * anywhere without a router of its own.\n\t */\n\tbasePath?: string;\n}\n\n/** A fetch-style handler: a web `Request` in, a JSON `Response` out. */\nexport type IntrospectionHandler = (request: Request) => Promise<Response>;\n\nconst OPERATORS = new Set<string>(Object.values(FilterOperator));\n\n/**\n * A read-only JSON API over a Postgres database's structure and rows.\n *\n * A plain `(Request) => Promise<Response>` rather than a router, so it mounts\n * in anything that speaks web `Request`s — Hono, Bun, Deno, a Node adapter —\n * and `@geekmidas/db` takes on no HTTP framework to provide it.\n *\n * | Route | Returns |\n * | ----------------------------- | ----------------------------------------------- |\n * | `GET /schemas` | `{ schemas, browsable }` |\n * | `GET /tables?schema=&refresh=`| `{ tables }` with columns and foreign keys |\n * | `GET /tables/:name?schema=` | one table |\n * | `GET /tables/:name/rows` | a page of rows: `{ rows, hasMore, nextCursor }` |\n *\n * Rows take `pageSize` (≤ 100), `cursor`, `sort=col:asc,col2:desc` and filters\n * as `filter[column][operator]=value` — `in`/`nin` take a comma-separated\n * list, `is_null`/`is_not_null` take no value.\n *\n * It exposes every row of every browsable table to whoever can reach it. Mount\n * it only where that is the developer: `gkm dev` serves it at `/__gkm/db` and\n * nothing in a production build does.\n *\n * @example\n * ```typescript\n * const handler = createIntrospectionHandler({ db, basePath: '/__gkm/db' });\n * app.all('/__gkm/db/*', (c) => handler(c.req.raw));\n * ```\n */\nexport function createIntrospectionHandler<DB>(\n\toptions: IntrospectionHandlerOptions<DB>,\n): IntrospectionHandler {\n\tconst browser = new DataBrowser(options);\n\tconst basePath = (options.basePath ?? '').replace(/\\/+$/, '');\n\n\treturn async (request) => {\n\t\tif (request.method !== 'GET') {\n\t\t\treturn json(\n\t\t\t\t{\n\t\t\t\t\terror: 'MethodNotAllowed',\n\t\t\t\t\tmessage: 'The database API is read-only; only GET is served.',\n\t\t\t\t},\n\t\t\t\t405,\n\t\t\t\t{ allow: 'GET' },\n\t\t\t);\n\t\t}\n\n\t\tconst url = new URL(request.url);\n\t\tconst path = url.pathname.startsWith(basePath)\n\t\t\t? url.pathname.slice(basePath.length)\n\t\t\t: url.pathname;\n\t\tconst segments = path.split('/').filter(Boolean).map(decodeURIComponent);\n\t\tconst schema = url.searchParams.get('schema') ?? undefined;\n\n\t\ttry {\n\t\t\tif (segments.length === 1 && segments[0] === 'schemas') {\n\t\t\t\treturn json({\n\t\t\t\t\tschemas: await browser.listSchemas(),\n\t\t\t\t\tbrowsable: browser.schemas,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (segments.length === 1 && segments[0] === 'tables') {\n\t\t\t\tconst refresh = url.searchParams.get('refresh') === 'true';\n\t\t\t\tconst { tables, updatedAt } = await browser.getSchema(refresh);\n\t\t\t\treturn json({\n\t\t\t\t\ttables: schema ? tables.filter((t) => t.schema === schema) : tables,\n\t\t\t\t\tupdatedAt,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (segments[0] === 'tables' && segments[1] && segments.length === 2) {\n\t\t\t\tconst table = await browser.getTableInfo(segments[1], schema);\n\t\t\t\tif (!table) {\n\t\t\t\t\tthrow new TableNotFound(\n\t\t\t\t\t\tsegments[1],\n\t\t\t\t\t\tschema ?? browser.schemas[0] ?? 'public',\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn json(table);\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\tsegments[0] === 'tables' &&\n\t\t\t\tsegments[1] &&\n\t\t\t\tsegments[2] === 'rows' &&\n\t\t\t\tsegments.length === 3\n\t\t\t) {\n\t\t\t\tconst pageSize = Number.parseInt(\n\t\t\t\t\turl.searchParams.get('pageSize') ?? '',\n\t\t\t\t\t10,\n\t\t\t\t);\n\t\t\t\tconst filters = parseFilters(url.searchParams);\n\t\t\t\tconst sort = parseSort(url.searchParams.get('sort'));\n\n\t\t\t\treturn json(\n\t\t\t\t\tawait browser.query({\n\t\t\t\t\t\ttable: segments[1],\n\t\t\t\t\t\tschema,\n\t\t\t\t\t\tpageSize: Number.isNaN(pageSize) ? undefined : pageSize,\n\t\t\t\t\t\tcursor: url.searchParams.get('cursor') || undefined,\n\t\t\t\t\t\tfilters: filters.length > 0 ? filters : undefined,\n\t\t\t\t\t\tsort: sort.length > 0 ? sort : undefined,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t}\n\n\t\t\treturn json(\n\t\t\t\t{\n\t\t\t\t\terror: 'NotFound',\n\t\t\t\t\tmessage: `No database route at '${path || '/'}'. Routes: /schemas, /tables, /tables/:name, /tables/:name/rows.`,\n\t\t\t\t},\n\t\t\t\t404,\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tconst status = statusFor(error);\n\t\t\tif (status === undefined) throw error;\n\t\t\treturn json(\n\t\t\t\t{ error: (error as Error).name, message: (error as Error).message },\n\t\t\t\tstatus,\n\t\t\t);\n\t\t}\n\t};\n}\n\n/** The caller's mistakes, answered; anything else is the server's and thrown. */\nfunction statusFor(error: unknown): number | undefined {\n\tif (error instanceof TableNotFound) return 404;\n\tif (\n\t\terror instanceof ColumnNotFound ||\n\t\terror instanceof UnsupportedFilterOperator ||\n\t\terror instanceof InvalidCursor\n\t) {\n\t\treturn 400;\n\t}\n\treturn undefined;\n}\n\n/**\n * `filter[column][operator]=value`. An unknown operator is ignored rather than\n * rejected, as a misspelt one in a hand-typed URL is easier to see in the\n * results than to debug from an error.\n */\nfunction parseFilters(params: URLSearchParams): FilterCondition[] {\n\tconst filters: FilterCondition[] = [];\n\n\tfor (const [key, value] of params) {\n\t\tconst match = key.match(/^filter\\[(\\w+)\\]\\[(\\w+)\\]$/);\n\t\tconst column = match?.[1];\n\t\tconst operator = match?.[2];\n\t\tif (!column || !operator || !OPERATORS.has(operator)) continue;\n\n\t\tconst op = operator as FilterOperator;\n\t\tif (op === FilterOperator.In || op === FilterOperator.Nin) {\n\t\t\tfilters.push({ column, operator: op, value: value.split(',') });\n\t\t} else if (\n\t\t\top === FilterOperator.IsNull ||\n\t\t\top === FilterOperator.IsNotNull\n\t\t) {\n\t\t\tfilters.push({ column, operator: op });\n\t\t} else {\n\t\t\tfilters.push({ column, operator: op, value: parseScalar(value) });\n\t\t}\n\t}\n\n\treturn filters;\n}\n\nfunction parseScalar(value: string): unknown {\n\tif (value === 'true') return true;\n\tif (value === 'false') return false;\n\tif (value !== '' && !Number.isNaN(Number(value))) return Number(value);\n\treturn value;\n}\n\n/** `sort=name:asc,created_at:desc`; a column with no direction is ascending. */\nfunction parseSort(param: string | null): SortConfig[] {\n\tif (!param) return [];\n\n\treturn param.split(',').flatMap((part) => {\n\t\tconst [column, direction] = part.split(':');\n\t\tif (!column) return [];\n\t\treturn [\n\t\t\t{\n\t\t\t\tcolumn,\n\t\t\t\tdirection: direction === 'desc' ? Direction.Desc : Direction.Asc,\n\t\t\t},\n\t\t];\n\t});\n}\n\nfunction json(\n\tbody: unknown,\n\tstatus = 200,\n\theaders: Record<string, string> = {},\n): Response {\n\t// A row can hold a bigint when the pg client is configured to parse int8,\n\t// and JSON.stringify throws on one.\n\tconst text = JSON.stringify(body, (_key, value) =>\n\t\ttypeof value === 'bigint' ? value.toString() : value,\n\t);\n\treturn new Response(text, {\n\t\tstatus,\n\t\theaders: { 'content-type': 'application/json; charset=utf-8', ...headers },\n\t});\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { DataBrowserOptions } from "./types.cjs";
|
|
2
|
+
//#region src/introspect/handler.d.ts
|
|
3
|
+
export interface IntrospectionHandlerOptions<DB = unknown> extends DataBrowserOptions<DB> {
|
|
4
|
+
/**
|
|
5
|
+
* The path the handler is mounted under, e.g. `/__gkm/db`. Stripped from
|
|
6
|
+
* each request's path before routing, so the handler can be mounted
|
|
7
|
+
* anywhere without a router of its own.
|
|
8
|
+
*/
|
|
9
|
+
basePath?: string;
|
|
10
|
+
}
|
|
11
|
+
/** A fetch-style handler: a web `Request` in, a JSON `Response` out. */
|
|
12
|
+
export type IntrospectionHandler = (request: Request) => Promise<Response>;
|
|
13
|
+
/**
|
|
14
|
+
* A read-only JSON API over a Postgres database's structure and rows.
|
|
15
|
+
*
|
|
16
|
+
* A plain `(Request) => Promise<Response>` rather than a router, so it mounts
|
|
17
|
+
* in anything that speaks web `Request`s — Hono, Bun, Deno, a Node adapter —
|
|
18
|
+
* and `@geekmidas/db` takes on no HTTP framework to provide it.
|
|
19
|
+
*
|
|
20
|
+
* | Route | Returns |
|
|
21
|
+
* | ----------------------------- | ----------------------------------------------- |
|
|
22
|
+
* | `GET /schemas` | `{ schemas, browsable }` |
|
|
23
|
+
* | `GET /tables?schema=&refresh=`| `{ tables }` with columns and foreign keys |
|
|
24
|
+
* | `GET /tables/:name?schema=` | one table |
|
|
25
|
+
* | `GET /tables/:name/rows` | a page of rows: `{ rows, hasMore, nextCursor }` |
|
|
26
|
+
*
|
|
27
|
+
* Rows take `pageSize` (≤ 100), `cursor`, `sort=col:asc,col2:desc` and filters
|
|
28
|
+
* as `filter[column][operator]=value` — `in`/`nin` take a comma-separated
|
|
29
|
+
* list, `is_null`/`is_not_null` take no value.
|
|
30
|
+
*
|
|
31
|
+
* It exposes every row of every browsable table to whoever can reach it. Mount
|
|
32
|
+
* it only where that is the developer: `gkm dev` serves it at `/__gkm/db` and
|
|
33
|
+
* nothing in a production build does.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* const handler = createIntrospectionHandler({ db, basePath: '/__gkm/db' });
|
|
38
|
+
* app.all('/__gkm/db/*', (c) => handler(c.req.raw));
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export declare function createIntrospectionHandler<DB>(options: IntrospectionHandlerOptions<DB>): IntrospectionHandler;
|
|
42
|
+
//#endregion
|
|
43
|
+
//# sourceMappingURL=handler.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.d.cts","names":[],"sources":["../../src/introspect/handler.ts"],"mappings":";;iBAciB,4BAA4B,sBACpC,mBAAmB;;;;;;EAM3B;;;YAIW,wBAAwB,SAAS,YAAY,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBAgCjD,2BAA2B,IAC1C,SAAS,4BAA4B,MACnC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { DataBrowserOptions } from "./types.mjs";
|
|
2
|
+
//#region src/introspect/handler.d.ts
|
|
3
|
+
export interface IntrospectionHandlerOptions<DB = unknown> extends DataBrowserOptions<DB> {
|
|
4
|
+
/**
|
|
5
|
+
* The path the handler is mounted under, e.g. `/__gkm/db`. Stripped from
|
|
6
|
+
* each request's path before routing, so the handler can be mounted
|
|
7
|
+
* anywhere without a router of its own.
|
|
8
|
+
*/
|
|
9
|
+
basePath?: string;
|
|
10
|
+
}
|
|
11
|
+
/** A fetch-style handler: a web `Request` in, a JSON `Response` out. */
|
|
12
|
+
export type IntrospectionHandler = (request: Request) => Promise<Response>;
|
|
13
|
+
/**
|
|
14
|
+
* A read-only JSON API over a Postgres database's structure and rows.
|
|
15
|
+
*
|
|
16
|
+
* A plain `(Request) => Promise<Response>` rather than a router, so it mounts
|
|
17
|
+
* in anything that speaks web `Request`s — Hono, Bun, Deno, a Node adapter —
|
|
18
|
+
* and `@geekmidas/db` takes on no HTTP framework to provide it.
|
|
19
|
+
*
|
|
20
|
+
* | Route | Returns |
|
|
21
|
+
* | ----------------------------- | ----------------------------------------------- |
|
|
22
|
+
* | `GET /schemas` | `{ schemas, browsable }` |
|
|
23
|
+
* | `GET /tables?schema=&refresh=`| `{ tables }` with columns and foreign keys |
|
|
24
|
+
* | `GET /tables/:name?schema=` | one table |
|
|
25
|
+
* | `GET /tables/:name/rows` | a page of rows: `{ rows, hasMore, nextCursor }` |
|
|
26
|
+
*
|
|
27
|
+
* Rows take `pageSize` (≤ 100), `cursor`, `sort=col:asc,col2:desc` and filters
|
|
28
|
+
* as `filter[column][operator]=value` — `in`/`nin` take a comma-separated
|
|
29
|
+
* list, `is_null`/`is_not_null` take no value.
|
|
30
|
+
*
|
|
31
|
+
* It exposes every row of every browsable table to whoever can reach it. Mount
|
|
32
|
+
* it only where that is the developer: `gkm dev` serves it at `/__gkm/db` and
|
|
33
|
+
* nothing in a production build does.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* const handler = createIntrospectionHandler({ db, basePath: '/__gkm/db' });
|
|
38
|
+
* app.all('/__gkm/db/*', (c) => handler(c.req.raw));
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export declare function createIntrospectionHandler<DB>(options: IntrospectionHandlerOptions<DB>): IntrospectionHandler;
|
|
42
|
+
//#endregion
|
|
43
|
+
//# sourceMappingURL=handler.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.d.mts","names":[],"sources":["../../src/introspect/handler.ts"],"mappings":";;iBAciB,4BAA4B,sBACpC,mBAAmB;;;;;;EAM3B;;;YAIW,wBAAwB,SAAS,YAAY,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBAgCjD,2BAA2B,IAC1C,SAAS,4BAA4B,MACnC"}
|