@geekmidas/db 10.0.0-alpha.54 → 10.0.0-alpha.56
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/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/package.json +11 -1
- package/dist/pagination.d.cts.map +0 -1
- package/dist/pagination.d.mts.map +0 -1
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@ Database utilities for Kysely with flexible transaction management. Provides hel
|
|
|
9
9
|
- ✅ **Type-Safe**: Full TypeScript support with generic database schemas
|
|
10
10
|
- ✅ **Connection Abstraction**: Single helper for all database connection types
|
|
11
11
|
- ✅ **Zero Dependencies**: Only peer dependency on Kysely
|
|
12
|
+
- ✅ **Introspection** (`@geekmidas/db/introspect`): schemas, tables and rows as functions and a read-only JSON API
|
|
12
13
|
|
|
13
14
|
## Installation
|
|
14
15
|
|
|
@@ -70,6 +71,22 @@ async function createUserWithPost(
|
|
|
70
71
|
}
|
|
71
72
|
```
|
|
72
73
|
|
|
74
|
+
## Introspection
|
|
75
|
+
|
|
76
|
+
`@geekmidas/db/introspect` reads a Postgres database's structure and pages
|
|
77
|
+
through its rows, read-only — as functions, and as a fetch-style JSON handler
|
|
78
|
+
with no HTTP framework behind it. `gkm dev` serves it at `/__gkm/db`.
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
import { createIntrospectionHandler } from '@geekmidas/db/introspect';
|
|
82
|
+
|
|
83
|
+
const handler = createIntrospectionHandler({ db, basePath: '/__db' });
|
|
84
|
+
app.all('/__db/*', (c) => handler(c.req.raw));
|
|
85
|
+
// GET /__db/schemas, /__db/tables, /__db/tables/users/rows?pageSize=20
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
It returns every row to whoever can reach it, so mount it only in development.
|
|
89
|
+
|
|
73
90
|
## API Reference
|
|
74
91
|
|
|
75
92
|
### `withTransaction`
|
|
@@ -0,0 +1,117 @@
|
|
|
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_filtering = require("./filtering.cjs");
|
|
5
|
+
const require_introspect_introspection = require("./introspection.cjs");
|
|
6
|
+
//#region src/introspect/DataBrowser.ts
|
|
7
|
+
/** How long an introspected schema is reused before it is read again. */
|
|
8
|
+
const SCHEMA_TTL_MS = 6e4;
|
|
9
|
+
const MAX_PAGE_SIZE = 100;
|
|
10
|
+
/**
|
|
11
|
+
* Reads a Postgres database's structure and pages through its rows, read-only.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* const browser = new DataBrowser({ db });
|
|
16
|
+
*
|
|
17
|
+
* const { tables } = await browser.getSchema();
|
|
18
|
+
* const page = await browser.query({ table: 'users', pageSize: 20 });
|
|
19
|
+
* const next = await browser.query({ table: 'users', cursor: page.nextCursor });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
var DataBrowser = class {
|
|
23
|
+
db;
|
|
24
|
+
schemas;
|
|
25
|
+
cursor;
|
|
26
|
+
tableCursors;
|
|
27
|
+
excludeTables;
|
|
28
|
+
defaultPageSize;
|
|
29
|
+
schemaCache;
|
|
30
|
+
constructor(options) {
|
|
31
|
+
this.db = options.db;
|
|
32
|
+
this.schemas = options.schemas ?? ["public"];
|
|
33
|
+
this.cursor = options.cursor;
|
|
34
|
+
this.tableCursors = options.tableCursors ?? {};
|
|
35
|
+
this.excludeTables = options.excludeTables ?? [];
|
|
36
|
+
this.defaultPageSize = options.defaultPageSize ?? 50;
|
|
37
|
+
}
|
|
38
|
+
/** Every schema the database has, browsable or not. */
|
|
39
|
+
listSchemas() {
|
|
40
|
+
return require_introspect_introspection.listSchemas(this.db);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The browsable tables. Cached for a minute, since a migration is rare and
|
|
44
|
+
* introspection is several catalog queries per table.
|
|
45
|
+
*/
|
|
46
|
+
async getSchema(forceRefresh = false) {
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
if (!forceRefresh && this.schemaCache && now < this.schemaCache.expires) return this.schemaCache.info;
|
|
49
|
+
const info = await require_introspect_introspection.introspectSchema(this.db, {
|
|
50
|
+
schemas: this.schemas,
|
|
51
|
+
excludeTables: this.excludeTables
|
|
52
|
+
});
|
|
53
|
+
this.schemaCache = {
|
|
54
|
+
info,
|
|
55
|
+
expires: now + SCHEMA_TTL_MS
|
|
56
|
+
};
|
|
57
|
+
return info;
|
|
58
|
+
}
|
|
59
|
+
/** One table, or null when it is not browsable. */
|
|
60
|
+
async getTableInfo(tableName, schema = this.schemas[0] ?? "public") {
|
|
61
|
+
const { tables } = await this.getSchema();
|
|
62
|
+
return tables.find((t) => t.name === tableName && t.schema === schema) ?? null;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* A page of a table's rows, filtered and sorted.
|
|
66
|
+
*
|
|
67
|
+
* @throws {TableNotFound} the table is not browsable
|
|
68
|
+
* @throws {ColumnNotFound} a filter or sort names a column it does not have
|
|
69
|
+
* @throws {UnsupportedFilterOperator} a filter's operator does not apply
|
|
70
|
+
* @throws {InvalidCursor} the cursor did not come from a previous page
|
|
71
|
+
*/
|
|
72
|
+
async query(options) {
|
|
73
|
+
const schema = options.schema ?? this.schemas[0] ?? "public";
|
|
74
|
+
const table = await this.getTableInfo(options.table, schema);
|
|
75
|
+
if (!table) throw new require_introspect_errors.TableNotFound(options.table, schema);
|
|
76
|
+
const cursor = this.getCursorConfig(table);
|
|
77
|
+
const pageSize = Math.min(options.pageSize ?? this.defaultPageSize, MAX_PAGE_SIZE);
|
|
78
|
+
let query = this.db.withSchema(schema).selectFrom(table.name).selectAll();
|
|
79
|
+
if (options.filters?.length) query = require_introspect_filtering.applyFilters(query, options.filters, table);
|
|
80
|
+
if (options.sort?.length) query = require_introspect_filtering.applySorting(query, options.sort, table);
|
|
81
|
+
else if (cursor) query = query.orderBy(cursor.field, cursor.direction);
|
|
82
|
+
if (options.cursor && cursor) {
|
|
83
|
+
const after = require_pagination.decodeCursor(options.cursor);
|
|
84
|
+
const operator = cursor.direction === "asc" ? ">" : "<";
|
|
85
|
+
query = query.where(cursor.field, operator, after);
|
|
86
|
+
}
|
|
87
|
+
const rows = await query.limit(pageSize + 1).execute();
|
|
88
|
+
const hasMore = rows.length > pageSize;
|
|
89
|
+
const page = hasMore ? rows.slice(0, pageSize) : rows;
|
|
90
|
+
const last = page[page.length - 1];
|
|
91
|
+
const first = page[0];
|
|
92
|
+
return {
|
|
93
|
+
rows: page,
|
|
94
|
+
hasMore,
|
|
95
|
+
nextCursor: hasMore && cursor && last ? require_pagination.encodeCursor(last[cursor.field]) : null,
|
|
96
|
+
prevCursor: options.cursor && cursor && first ? require_pagination.encodeCursor(first[cursor.field]) : null
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The cursor a table pages by: its own, the default, or its primary key
|
|
101
|
+
* when that is one column. Undefined means the table has nothing unique to
|
|
102
|
+
* page by, so it is read a page at a time with no next cursor.
|
|
103
|
+
*/
|
|
104
|
+
getCursorConfig(table) {
|
|
105
|
+
const configured = this.tableCursors[table.name] ?? this.cursor;
|
|
106
|
+
if (configured) return configured;
|
|
107
|
+
const [key, ...rest] = table.primaryKey;
|
|
108
|
+
if (key && rest.length === 0) return {
|
|
109
|
+
field: key,
|
|
110
|
+
direction: "asc"
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
//#endregion
|
|
115
|
+
exports.DataBrowser = DataBrowser;
|
|
116
|
+
|
|
117
|
+
//# sourceMappingURL=DataBrowser.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DataBrowser.cjs","names":["listSchemas","introspectSchema","TableNotFound","applyFilters","applySorting","decodeCursor","encodeCursor"],"sources":["../../src/introspect/DataBrowser.ts"],"sourcesContent":["import type { Kysely, SelectQueryBuilder } from 'kysely';\nimport { Direction, decodeCursor, encodeCursor } from '../pagination';\nimport { TableNotFound } from './errors';\nimport { applyFilters, applySorting } from './filtering';\nimport { introspectSchema, listSchemas } from './introspection';\nimport type {\n\tCursorConfig,\n\tDataBrowserOptions,\n\tQueryOptions,\n\tQueryResult,\n\tSchemaInfo,\n\tTableInfo,\n} from './types';\n\n/** How long an introspected schema is reused before it is read again. */\nconst SCHEMA_TTL_MS = 60 * 1000;\nconst MAX_PAGE_SIZE = 100;\n\n/**\n * Reads a Postgres database's structure and pages through its rows, read-only.\n *\n * @example\n * ```typescript\n * const browser = new DataBrowser({ db });\n *\n * const { tables } = await browser.getSchema();\n * const page = await browser.query({ table: 'users', pageSize: 20 });\n * const next = await browser.query({ table: 'users', cursor: page.nextCursor });\n * ```\n */\nexport class DataBrowser<DB = unknown> {\n\treadonly db: Kysely<DB>;\n\treadonly schemas: string[];\n\tprivate readonly cursor?: CursorConfig;\n\tprivate readonly tableCursors: Record<string, CursorConfig>;\n\tprivate readonly excludeTables: string[];\n\tprivate readonly defaultPageSize: number;\n\tprivate schemaCache?: { info: SchemaInfo; expires: number };\n\n\tconstructor(options: DataBrowserOptions<DB>) {\n\t\tthis.db = options.db;\n\t\tthis.schemas = options.schemas ?? ['public'];\n\t\tthis.cursor = options.cursor;\n\t\tthis.tableCursors = options.tableCursors ?? {};\n\t\tthis.excludeTables = options.excludeTables ?? [];\n\t\tthis.defaultPageSize = options.defaultPageSize ?? 50;\n\t}\n\n\t/** Every schema the database has, browsable or not. */\n\tlistSchemas(): Promise<string[]> {\n\t\treturn listSchemas(this.db);\n\t}\n\n\t/**\n\t * The browsable tables. Cached for a minute, since a migration is rare and\n\t * introspection is several catalog queries per table.\n\t */\n\tasync getSchema(forceRefresh = false): Promise<SchemaInfo> {\n\t\tconst now = Date.now();\n\t\tif (!forceRefresh && this.schemaCache && now < this.schemaCache.expires) {\n\t\t\treturn this.schemaCache.info;\n\t\t}\n\n\t\tconst info = await introspectSchema(this.db, {\n\t\t\tschemas: this.schemas,\n\t\t\texcludeTables: this.excludeTables,\n\t\t});\n\t\tthis.schemaCache = { info, expires: now + SCHEMA_TTL_MS };\n\t\treturn info;\n\t}\n\n\t/** One table, or null when it is not browsable. */\n\tasync getTableInfo(\n\t\ttableName: string,\n\t\tschema = this.schemas[0] ?? 'public',\n\t): Promise<TableInfo | null> {\n\t\tconst { tables } = await this.getSchema();\n\t\treturn (\n\t\t\ttables.find((t) => t.name === tableName && t.schema === schema) ?? null\n\t\t);\n\t}\n\n\t/**\n\t * A page of a table's rows, filtered and sorted.\n\t *\n\t * @throws {TableNotFound} the table is not browsable\n\t * @throws {ColumnNotFound} a filter or sort names a column it does not have\n\t * @throws {UnsupportedFilterOperator} a filter's operator does not apply\n\t * @throws {InvalidCursor} the cursor did not come from a previous page\n\t */\n\tasync query(options: QueryOptions): Promise<QueryResult> {\n\t\tconst schema = options.schema ?? this.schemas[0] ?? 'public';\n\t\tconst table = await this.getTableInfo(options.table, schema);\n\t\tif (!table) {\n\t\t\tthrow new TableNotFound(options.table, schema);\n\t\t}\n\n\t\tconst cursor = this.getCursorConfig(table);\n\t\tconst pageSize = Math.min(\n\t\t\toptions.pageSize ?? this.defaultPageSize,\n\t\t\tMAX_PAGE_SIZE,\n\t\t);\n\n\t\t// The table was found by introspection; the query's static types only\n\t\t// know the tables the application declared.\n\t\tlet query = (this.db as Kysely<any>)\n\t\t\t.withSchema(schema)\n\t\t\t.selectFrom(table.name)\n\t\t\t.selectAll() as SelectQueryBuilder<any, any, Record<string, unknown>>;\n\n\t\tif (options.filters?.length) {\n\t\t\tquery = applyFilters(query, options.filters, table);\n\t\t}\n\n\t\tif (options.sort?.length) {\n\t\t\tquery = applySorting(query, options.sort, table);\n\t\t} else if (cursor) {\n\t\t\tquery = query.orderBy(cursor.field, cursor.direction);\n\t\t}\n\n\t\tif (options.cursor && cursor) {\n\t\t\tconst after = decodeCursor(options.cursor);\n\t\t\tconst operator = cursor.direction === Direction.Asc ? '>' : '<';\n\t\t\tquery = query.where(cursor.field, operator, after);\n\t\t}\n\n\t\t// One more row than the page, so whether there is a next page is known\n\t\t// without counting.\n\t\tconst rows = await query.limit(pageSize + 1).execute();\n\t\tconst hasMore = rows.length > pageSize;\n\t\tconst page = hasMore ? rows.slice(0, pageSize) : rows;\n\n\t\tconst last = page[page.length - 1];\n\t\tconst first = page[0];\n\n\t\treturn {\n\t\t\trows: page,\n\t\t\thasMore,\n\t\t\tnextCursor:\n\t\t\t\thasMore && cursor && last ? encodeCursor(last[cursor.field]) : null,\n\t\t\t// Only known to exist when this page was reached through a cursor.\n\t\t\tprevCursor:\n\t\t\t\toptions.cursor && cursor && first\n\t\t\t\t\t? encodeCursor(first[cursor.field])\n\t\t\t\t\t: null,\n\t\t};\n\t}\n\n\t/**\n\t * The cursor a table pages by: its own, the default, or its primary key\n\t * when that is one column. Undefined means the table has nothing unique to\n\t * page by, so it is read a page at a time with no next cursor.\n\t */\n\tgetCursorConfig(table: TableInfo): CursorConfig | undefined {\n\t\tconst configured = this.tableCursors[table.name] ?? this.cursor;\n\t\tif (configured) return configured;\n\n\t\tconst [key, ...rest] = table.primaryKey;\n\t\tif (key && rest.length === 0) {\n\t\t\treturn { field: key, direction: Direction.Asc };\n\t\t}\n\n\t\treturn undefined;\n\t}\n}\n"],"mappings":";;;;;;;AAeA,MAAM,gBAAgB;AACtB,MAAM,gBAAgB;;;;;;;;;;;;;AActB,IAAa,cAAb,MAAuC;CACtC;CACA;CACA;CACA;CACA;CACA;CACA;CAEA,YAAY,SAAiC;EAC5C,KAAK,KAAK,QAAQ;EAClB,KAAK,UAAU,QAAQ,WAAW,CAAC,QAAQ;EAC3C,KAAK,SAAS,QAAQ;EACtB,KAAK,eAAe,QAAQ,gBAAgB,CAAC;EAC7C,KAAK,gBAAgB,QAAQ,iBAAiB,CAAC;EAC/C,KAAK,kBAAkB,QAAQ,mBAAmB;CACnD;;CAGA,cAAiC;EAChC,OAAOA,iCAAAA,YAAY,KAAK,EAAE;CAC3B;;;;;CAMA,MAAM,UAAU,eAAe,OAA4B;EAC1D,MAAM,MAAM,KAAK,IAAI;EACrB,IAAI,CAAC,gBAAgB,KAAK,eAAe,MAAM,KAAK,YAAY,SAC/D,OAAO,KAAK,YAAY;EAGzB,MAAM,OAAO,MAAMC,iCAAAA,iBAAiB,KAAK,IAAI;GAC5C,SAAS,KAAK;GACd,eAAe,KAAK;EACrB,CAAC;EACD,KAAK,cAAc;GAAE;GAAM,SAAS,MAAM;EAAc;EACxD,OAAO;CACR;;CAGA,MAAM,aACL,WACA,SAAS,KAAK,QAAQ,MAAM,UACA;EAC5B,MAAM,EAAE,WAAW,MAAM,KAAK,UAAU;EACxC,OACC,OAAO,MAAM,MAAM,EAAE,SAAS,aAAa,EAAE,WAAW,MAAM,KAAK;CAErE;;;;;;;;;CAUA,MAAM,MAAM,SAA6C;EACxD,MAAM,SAAS,QAAQ,UAAU,KAAK,QAAQ,MAAM;EACpD,MAAM,QAAQ,MAAM,KAAK,aAAa,QAAQ,OAAO,MAAM;EAC3D,IAAI,CAAC,OACJ,MAAM,IAAIC,0BAAAA,cAAc,QAAQ,OAAO,MAAM;EAG9C,MAAM,SAAS,KAAK,gBAAgB,KAAK;EACzC,MAAM,WAAW,KAAK,IACrB,QAAQ,YAAY,KAAK,iBACzB,aACD;EAIA,IAAI,QAAS,KAAK,GAChB,WAAW,MAAM,CAAC,CAClB,WAAW,MAAM,IAAI,CAAC,CACtB,UAAU;EAEZ,IAAI,QAAQ,SAAS,QACpB,QAAQC,6BAAAA,aAAa,OAAO,QAAQ,SAAS,KAAK;EAGnD,IAAI,QAAQ,MAAM,QACjB,QAAQC,6BAAAA,aAAa,OAAO,QAAQ,MAAM,KAAK;OACzC,IAAI,QACV,QAAQ,MAAM,QAAQ,OAAO,OAAO,OAAO,SAAS;EAGrD,IAAI,QAAQ,UAAU,QAAQ;GAC7B,MAAM,QAAQC,mBAAAA,aAAa,QAAQ,MAAM;GACzC,MAAM,WAAW,OAAO,cAAA,QAA8B,MAAM;GAC5D,QAAQ,MAAM,MAAM,OAAO,OAAO,UAAU,KAAK;EAClD;EAIA,MAAM,OAAO,MAAM,MAAM,MAAM,WAAW,CAAC,CAAC,CAAC,QAAQ;EACrD,MAAM,UAAU,KAAK,SAAS;EAC9B,MAAM,OAAO,UAAU,KAAK,MAAM,GAAG,QAAQ,IAAI;EAEjD,MAAM,OAAO,KAAK,KAAK,SAAS;EAChC,MAAM,QAAQ,KAAK;EAEnB,OAAO;GACN,MAAM;GACN;GACA,YACC,WAAW,UAAU,OAAOC,mBAAAA,aAAa,KAAK,OAAO,MAAM,IAAI;GAEhE,YACC,QAAQ,UAAU,UAAU,QACzBA,mBAAAA,aAAa,MAAM,OAAO,MAAM,IAChC;EACL;CACD;;;;;;CAOA,gBAAgB,OAA4C;EAC3D,MAAM,aAAa,KAAK,aAAa,MAAM,SAAS,KAAK;EACzD,IAAI,YAAY,OAAO;EAEvB,MAAM,CAAC,KAAK,GAAG,QAAQ,MAAM;EAC7B,IAAI,OAAO,KAAK,WAAW,GAC1B,OAAO;GAAE,OAAO;GAAK,WAAA;EAAyB;CAIhD;AACD"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { CursorConfig, DataBrowserOptions, QueryOptions, QueryResult, SchemaInfo, TableInfo } from "./types.cjs";
|
|
2
|
+
import { Kysely } from "kysely";
|
|
3
|
+
//#region src/introspect/DataBrowser.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Reads a Postgres database's structure and pages through its rows, read-only.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* const browser = new DataBrowser({ db });
|
|
10
|
+
*
|
|
11
|
+
* const { tables } = await browser.getSchema();
|
|
12
|
+
* const page = await browser.query({ table: 'users', pageSize: 20 });
|
|
13
|
+
* const next = await browser.query({ table: 'users', cursor: page.nextCursor });
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export declare class DataBrowser<DB = unknown> {
|
|
17
|
+
readonly db: Kysely<DB>;
|
|
18
|
+
readonly schemas: string[];
|
|
19
|
+
private readonly cursor?;
|
|
20
|
+
private readonly tableCursors;
|
|
21
|
+
private readonly excludeTables;
|
|
22
|
+
private readonly defaultPageSize;
|
|
23
|
+
private schemaCache?;
|
|
24
|
+
constructor(options: DataBrowserOptions<DB>);
|
|
25
|
+
/** Every schema the database has, browsable or not. */
|
|
26
|
+
listSchemas(): Promise<string[]>;
|
|
27
|
+
/**
|
|
28
|
+
* The browsable tables. Cached for a minute, since a migration is rare and
|
|
29
|
+
* introspection is several catalog queries per table.
|
|
30
|
+
*/
|
|
31
|
+
getSchema(forceRefresh?: boolean): Promise<SchemaInfo>;
|
|
32
|
+
/** One table, or null when it is not browsable. */
|
|
33
|
+
getTableInfo(tableName: string, schema?: string): Promise<TableInfo | null>;
|
|
34
|
+
/**
|
|
35
|
+
* A page of a table's rows, filtered and sorted.
|
|
36
|
+
*
|
|
37
|
+
* @throws {TableNotFound} the table is not browsable
|
|
38
|
+
* @throws {ColumnNotFound} a filter or sort names a column it does not have
|
|
39
|
+
* @throws {UnsupportedFilterOperator} a filter's operator does not apply
|
|
40
|
+
* @throws {InvalidCursor} the cursor did not come from a previous page
|
|
41
|
+
*/
|
|
42
|
+
query(options: QueryOptions): Promise<QueryResult>;
|
|
43
|
+
/**
|
|
44
|
+
* The cursor a table pages by: its own, the default, or its primary key
|
|
45
|
+
* when that is one column. Undefined means the table has nothing unique to
|
|
46
|
+
* page by, so it is read a page at a time with no next cursor.
|
|
47
|
+
*/
|
|
48
|
+
getCursorConfig(table: TableInfo): CursorConfig | undefined;
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
//# sourceMappingURL=DataBrowser.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DataBrowser.d.cts","names":[],"sources":["../../src/introspect/DataBrowser.ts"],"mappings":";;;;;;;;;;;;;;;qBA8Ba,YAAY;WACf,IAAI,OAAO;WACX;mBACQ;mBACA;mBACA;mBACA;UACT;EAER,YAAY,SAAS,mBAAmB;;EAUxC,eAAe;;;;;EAQT,UAAU,yBAAuB,QAAQ;;EAezC,aACL,mBACA,kBACE,QAAQ;;;;;;;;;EAeL,MAAM,SAAS,eAAe,QAAQ;;;;;;EA+D5C,gBAAgB,OAAO,YAAY"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { CursorConfig, DataBrowserOptions, QueryOptions, QueryResult, SchemaInfo, TableInfo } from "./types.mjs";
|
|
2
|
+
import { Kysely } from "kysely";
|
|
3
|
+
//#region src/introspect/DataBrowser.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Reads a Postgres database's structure and pages through its rows, read-only.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* const browser = new DataBrowser({ db });
|
|
10
|
+
*
|
|
11
|
+
* const { tables } = await browser.getSchema();
|
|
12
|
+
* const page = await browser.query({ table: 'users', pageSize: 20 });
|
|
13
|
+
* const next = await browser.query({ table: 'users', cursor: page.nextCursor });
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export declare class DataBrowser<DB = unknown> {
|
|
17
|
+
readonly db: Kysely<DB>;
|
|
18
|
+
readonly schemas: string[];
|
|
19
|
+
private readonly cursor?;
|
|
20
|
+
private readonly tableCursors;
|
|
21
|
+
private readonly excludeTables;
|
|
22
|
+
private readonly defaultPageSize;
|
|
23
|
+
private schemaCache?;
|
|
24
|
+
constructor(options: DataBrowserOptions<DB>);
|
|
25
|
+
/** Every schema the database has, browsable or not. */
|
|
26
|
+
listSchemas(): Promise<string[]>;
|
|
27
|
+
/**
|
|
28
|
+
* The browsable tables. Cached for a minute, since a migration is rare and
|
|
29
|
+
* introspection is several catalog queries per table.
|
|
30
|
+
*/
|
|
31
|
+
getSchema(forceRefresh?: boolean): Promise<SchemaInfo>;
|
|
32
|
+
/** One table, or null when it is not browsable. */
|
|
33
|
+
getTableInfo(tableName: string, schema?: string): Promise<TableInfo | null>;
|
|
34
|
+
/**
|
|
35
|
+
* A page of a table's rows, filtered and sorted.
|
|
36
|
+
*
|
|
37
|
+
* @throws {TableNotFound} the table is not browsable
|
|
38
|
+
* @throws {ColumnNotFound} a filter or sort names a column it does not have
|
|
39
|
+
* @throws {UnsupportedFilterOperator} a filter's operator does not apply
|
|
40
|
+
* @throws {InvalidCursor} the cursor did not come from a previous page
|
|
41
|
+
*/
|
|
42
|
+
query(options: QueryOptions): Promise<QueryResult>;
|
|
43
|
+
/**
|
|
44
|
+
* The cursor a table pages by: its own, the default, or its primary key
|
|
45
|
+
* when that is one column. Undefined means the table has nothing unique to
|
|
46
|
+
* page by, so it is read a page at a time with no next cursor.
|
|
47
|
+
*/
|
|
48
|
+
getCursorConfig(table: TableInfo): CursorConfig | undefined;
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
//# sourceMappingURL=DataBrowser.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DataBrowser.d.mts","names":[],"sources":["../../src/introspect/DataBrowser.ts"],"mappings":";;;;;;;;;;;;;;;qBA8Ba,YAAY;WACf,IAAI,OAAO;WACX;mBACQ;mBACA;mBACA;mBACA;UACT;EAER,YAAY,SAAS,mBAAmB;;EAUxC,eAAe;;;;;EAQT,UAAU,yBAAuB,QAAQ;;EAezC,aACL,mBACA,kBACE,QAAQ;;;;;;;;;EAeL,MAAM,SAAS,eAAe,QAAQ;;;;;;EA+D5C,gBAAgB,OAAO,YAAY"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { decodeCursor, encodeCursor } from "../pagination.mjs";
|
|
2
|
+
import { TableNotFound } from "./errors.mjs";
|
|
3
|
+
import { applyFilters, applySorting } from "./filtering.mjs";
|
|
4
|
+
import { introspectSchema, listSchemas } from "./introspection.mjs";
|
|
5
|
+
//#region src/introspect/DataBrowser.ts
|
|
6
|
+
/** How long an introspected schema is reused before it is read again. */
|
|
7
|
+
const SCHEMA_TTL_MS = 6e4;
|
|
8
|
+
const MAX_PAGE_SIZE = 100;
|
|
9
|
+
/**
|
|
10
|
+
* Reads a Postgres database's structure and pages through its rows, read-only.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const browser = new DataBrowser({ db });
|
|
15
|
+
*
|
|
16
|
+
* const { tables } = await browser.getSchema();
|
|
17
|
+
* const page = await browser.query({ table: 'users', pageSize: 20 });
|
|
18
|
+
* const next = await browser.query({ table: 'users', cursor: page.nextCursor });
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
var DataBrowser = class {
|
|
22
|
+
db;
|
|
23
|
+
schemas;
|
|
24
|
+
cursor;
|
|
25
|
+
tableCursors;
|
|
26
|
+
excludeTables;
|
|
27
|
+
defaultPageSize;
|
|
28
|
+
schemaCache;
|
|
29
|
+
constructor(options) {
|
|
30
|
+
this.db = options.db;
|
|
31
|
+
this.schemas = options.schemas ?? ["public"];
|
|
32
|
+
this.cursor = options.cursor;
|
|
33
|
+
this.tableCursors = options.tableCursors ?? {};
|
|
34
|
+
this.excludeTables = options.excludeTables ?? [];
|
|
35
|
+
this.defaultPageSize = options.defaultPageSize ?? 50;
|
|
36
|
+
}
|
|
37
|
+
/** Every schema the database has, browsable or not. */
|
|
38
|
+
listSchemas() {
|
|
39
|
+
return listSchemas(this.db);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The browsable tables. Cached for a minute, since a migration is rare and
|
|
43
|
+
* introspection is several catalog queries per table.
|
|
44
|
+
*/
|
|
45
|
+
async getSchema(forceRefresh = false) {
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
if (!forceRefresh && this.schemaCache && now < this.schemaCache.expires) return this.schemaCache.info;
|
|
48
|
+
const info = await introspectSchema(this.db, {
|
|
49
|
+
schemas: this.schemas,
|
|
50
|
+
excludeTables: this.excludeTables
|
|
51
|
+
});
|
|
52
|
+
this.schemaCache = {
|
|
53
|
+
info,
|
|
54
|
+
expires: now + SCHEMA_TTL_MS
|
|
55
|
+
};
|
|
56
|
+
return info;
|
|
57
|
+
}
|
|
58
|
+
/** One table, or null when it is not browsable. */
|
|
59
|
+
async getTableInfo(tableName, schema = this.schemas[0] ?? "public") {
|
|
60
|
+
const { tables } = await this.getSchema();
|
|
61
|
+
return tables.find((t) => t.name === tableName && t.schema === schema) ?? null;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* A page of a table's rows, filtered and sorted.
|
|
65
|
+
*
|
|
66
|
+
* @throws {TableNotFound} the table is not browsable
|
|
67
|
+
* @throws {ColumnNotFound} a filter or sort names a column it does not have
|
|
68
|
+
* @throws {UnsupportedFilterOperator} a filter's operator does not apply
|
|
69
|
+
* @throws {InvalidCursor} the cursor did not come from a previous page
|
|
70
|
+
*/
|
|
71
|
+
async query(options) {
|
|
72
|
+
const schema = options.schema ?? this.schemas[0] ?? "public";
|
|
73
|
+
const table = await this.getTableInfo(options.table, schema);
|
|
74
|
+
if (!table) throw new TableNotFound(options.table, schema);
|
|
75
|
+
const cursor = this.getCursorConfig(table);
|
|
76
|
+
const pageSize = Math.min(options.pageSize ?? this.defaultPageSize, MAX_PAGE_SIZE);
|
|
77
|
+
let query = this.db.withSchema(schema).selectFrom(table.name).selectAll();
|
|
78
|
+
if (options.filters?.length) query = applyFilters(query, options.filters, table);
|
|
79
|
+
if (options.sort?.length) query = applySorting(query, options.sort, table);
|
|
80
|
+
else if (cursor) query = query.orderBy(cursor.field, cursor.direction);
|
|
81
|
+
if (options.cursor && cursor) {
|
|
82
|
+
const after = decodeCursor(options.cursor);
|
|
83
|
+
const operator = cursor.direction === "asc" ? ">" : "<";
|
|
84
|
+
query = query.where(cursor.field, operator, after);
|
|
85
|
+
}
|
|
86
|
+
const rows = await query.limit(pageSize + 1).execute();
|
|
87
|
+
const hasMore = rows.length > pageSize;
|
|
88
|
+
const page = hasMore ? rows.slice(0, pageSize) : rows;
|
|
89
|
+
const last = page[page.length - 1];
|
|
90
|
+
const first = page[0];
|
|
91
|
+
return {
|
|
92
|
+
rows: page,
|
|
93
|
+
hasMore,
|
|
94
|
+
nextCursor: hasMore && cursor && last ? encodeCursor(last[cursor.field]) : null,
|
|
95
|
+
prevCursor: options.cursor && cursor && first ? encodeCursor(first[cursor.field]) : null
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The cursor a table pages by: its own, the default, or its primary key
|
|
100
|
+
* when that is one column. Undefined means the table has nothing unique to
|
|
101
|
+
* page by, so it is read a page at a time with no next cursor.
|
|
102
|
+
*/
|
|
103
|
+
getCursorConfig(table) {
|
|
104
|
+
const configured = this.tableCursors[table.name] ?? this.cursor;
|
|
105
|
+
if (configured) return configured;
|
|
106
|
+
const [key, ...rest] = table.primaryKey;
|
|
107
|
+
if (key && rest.length === 0) return {
|
|
108
|
+
field: key,
|
|
109
|
+
direction: "asc"
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
//#endregion
|
|
114
|
+
export { DataBrowser };
|
|
115
|
+
|
|
116
|
+
//# sourceMappingURL=DataBrowser.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DataBrowser.mjs","names":[],"sources":["../../src/introspect/DataBrowser.ts"],"sourcesContent":["import type { Kysely, SelectQueryBuilder } from 'kysely';\nimport { Direction, decodeCursor, encodeCursor } from '../pagination';\nimport { TableNotFound } from './errors';\nimport { applyFilters, applySorting } from './filtering';\nimport { introspectSchema, listSchemas } from './introspection';\nimport type {\n\tCursorConfig,\n\tDataBrowserOptions,\n\tQueryOptions,\n\tQueryResult,\n\tSchemaInfo,\n\tTableInfo,\n} from './types';\n\n/** How long an introspected schema is reused before it is read again. */\nconst SCHEMA_TTL_MS = 60 * 1000;\nconst MAX_PAGE_SIZE = 100;\n\n/**\n * Reads a Postgres database's structure and pages through its rows, read-only.\n *\n * @example\n * ```typescript\n * const browser = new DataBrowser({ db });\n *\n * const { tables } = await browser.getSchema();\n * const page = await browser.query({ table: 'users', pageSize: 20 });\n * const next = await browser.query({ table: 'users', cursor: page.nextCursor });\n * ```\n */\nexport class DataBrowser<DB = unknown> {\n\treadonly db: Kysely<DB>;\n\treadonly schemas: string[];\n\tprivate readonly cursor?: CursorConfig;\n\tprivate readonly tableCursors: Record<string, CursorConfig>;\n\tprivate readonly excludeTables: string[];\n\tprivate readonly defaultPageSize: number;\n\tprivate schemaCache?: { info: SchemaInfo; expires: number };\n\n\tconstructor(options: DataBrowserOptions<DB>) {\n\t\tthis.db = options.db;\n\t\tthis.schemas = options.schemas ?? ['public'];\n\t\tthis.cursor = options.cursor;\n\t\tthis.tableCursors = options.tableCursors ?? {};\n\t\tthis.excludeTables = options.excludeTables ?? [];\n\t\tthis.defaultPageSize = options.defaultPageSize ?? 50;\n\t}\n\n\t/** Every schema the database has, browsable or not. */\n\tlistSchemas(): Promise<string[]> {\n\t\treturn listSchemas(this.db);\n\t}\n\n\t/**\n\t * The browsable tables. Cached for a minute, since a migration is rare and\n\t * introspection is several catalog queries per table.\n\t */\n\tasync getSchema(forceRefresh = false): Promise<SchemaInfo> {\n\t\tconst now = Date.now();\n\t\tif (!forceRefresh && this.schemaCache && now < this.schemaCache.expires) {\n\t\t\treturn this.schemaCache.info;\n\t\t}\n\n\t\tconst info = await introspectSchema(this.db, {\n\t\t\tschemas: this.schemas,\n\t\t\texcludeTables: this.excludeTables,\n\t\t});\n\t\tthis.schemaCache = { info, expires: now + SCHEMA_TTL_MS };\n\t\treturn info;\n\t}\n\n\t/** One table, or null when it is not browsable. */\n\tasync getTableInfo(\n\t\ttableName: string,\n\t\tschema = this.schemas[0] ?? 'public',\n\t): Promise<TableInfo | null> {\n\t\tconst { tables } = await this.getSchema();\n\t\treturn (\n\t\t\ttables.find((t) => t.name === tableName && t.schema === schema) ?? null\n\t\t);\n\t}\n\n\t/**\n\t * A page of a table's rows, filtered and sorted.\n\t *\n\t * @throws {TableNotFound} the table is not browsable\n\t * @throws {ColumnNotFound} a filter or sort names a column it does not have\n\t * @throws {UnsupportedFilterOperator} a filter's operator does not apply\n\t * @throws {InvalidCursor} the cursor did not come from a previous page\n\t */\n\tasync query(options: QueryOptions): Promise<QueryResult> {\n\t\tconst schema = options.schema ?? this.schemas[0] ?? 'public';\n\t\tconst table = await this.getTableInfo(options.table, schema);\n\t\tif (!table) {\n\t\t\tthrow new TableNotFound(options.table, schema);\n\t\t}\n\n\t\tconst cursor = this.getCursorConfig(table);\n\t\tconst pageSize = Math.min(\n\t\t\toptions.pageSize ?? this.defaultPageSize,\n\t\t\tMAX_PAGE_SIZE,\n\t\t);\n\n\t\t// The table was found by introspection; the query's static types only\n\t\t// know the tables the application declared.\n\t\tlet query = (this.db as Kysely<any>)\n\t\t\t.withSchema(schema)\n\t\t\t.selectFrom(table.name)\n\t\t\t.selectAll() as SelectQueryBuilder<any, any, Record<string, unknown>>;\n\n\t\tif (options.filters?.length) {\n\t\t\tquery = applyFilters(query, options.filters, table);\n\t\t}\n\n\t\tif (options.sort?.length) {\n\t\t\tquery = applySorting(query, options.sort, table);\n\t\t} else if (cursor) {\n\t\t\tquery = query.orderBy(cursor.field, cursor.direction);\n\t\t}\n\n\t\tif (options.cursor && cursor) {\n\t\t\tconst after = decodeCursor(options.cursor);\n\t\t\tconst operator = cursor.direction === Direction.Asc ? '>' : '<';\n\t\t\tquery = query.where(cursor.field, operator, after);\n\t\t}\n\n\t\t// One more row than the page, so whether there is a next page is known\n\t\t// without counting.\n\t\tconst rows = await query.limit(pageSize + 1).execute();\n\t\tconst hasMore = rows.length > pageSize;\n\t\tconst page = hasMore ? rows.slice(0, pageSize) : rows;\n\n\t\tconst last = page[page.length - 1];\n\t\tconst first = page[0];\n\n\t\treturn {\n\t\t\trows: page,\n\t\t\thasMore,\n\t\t\tnextCursor:\n\t\t\t\thasMore && cursor && last ? encodeCursor(last[cursor.field]) : null,\n\t\t\t// Only known to exist when this page was reached through a cursor.\n\t\t\tprevCursor:\n\t\t\t\toptions.cursor && cursor && first\n\t\t\t\t\t? encodeCursor(first[cursor.field])\n\t\t\t\t\t: null,\n\t\t};\n\t}\n\n\t/**\n\t * The cursor a table pages by: its own, the default, or its primary key\n\t * when that is one column. Undefined means the table has nothing unique to\n\t * page by, so it is read a page at a time with no next cursor.\n\t */\n\tgetCursorConfig(table: TableInfo): CursorConfig | undefined {\n\t\tconst configured = this.tableCursors[table.name] ?? this.cursor;\n\t\tif (configured) return configured;\n\n\t\tconst [key, ...rest] = table.primaryKey;\n\t\tif (key && rest.length === 0) {\n\t\t\treturn { field: key, direction: Direction.Asc };\n\t\t}\n\n\t\treturn undefined;\n\t}\n}\n"],"mappings":";;;;;;AAeA,MAAM,gBAAgB;AACtB,MAAM,gBAAgB;;;;;;;;;;;;;AActB,IAAa,cAAb,MAAuC;CACtC;CACA;CACA;CACA;CACA;CACA;CACA;CAEA,YAAY,SAAiC;EAC5C,KAAK,KAAK,QAAQ;EAClB,KAAK,UAAU,QAAQ,WAAW,CAAC,QAAQ;EAC3C,KAAK,SAAS,QAAQ;EACtB,KAAK,eAAe,QAAQ,gBAAgB,CAAC;EAC7C,KAAK,gBAAgB,QAAQ,iBAAiB,CAAC;EAC/C,KAAK,kBAAkB,QAAQ,mBAAmB;CACnD;;CAGA,cAAiC;EAChC,OAAO,YAAY,KAAK,EAAE;CAC3B;;;;;CAMA,MAAM,UAAU,eAAe,OAA4B;EAC1D,MAAM,MAAM,KAAK,IAAI;EACrB,IAAI,CAAC,gBAAgB,KAAK,eAAe,MAAM,KAAK,YAAY,SAC/D,OAAO,KAAK,YAAY;EAGzB,MAAM,OAAO,MAAM,iBAAiB,KAAK,IAAI;GAC5C,SAAS,KAAK;GACd,eAAe,KAAK;EACrB,CAAC;EACD,KAAK,cAAc;GAAE;GAAM,SAAS,MAAM;EAAc;EACxD,OAAO;CACR;;CAGA,MAAM,aACL,WACA,SAAS,KAAK,QAAQ,MAAM,UACA;EAC5B,MAAM,EAAE,WAAW,MAAM,KAAK,UAAU;EACxC,OACC,OAAO,MAAM,MAAM,EAAE,SAAS,aAAa,EAAE,WAAW,MAAM,KAAK;CAErE;;;;;;;;;CAUA,MAAM,MAAM,SAA6C;EACxD,MAAM,SAAS,QAAQ,UAAU,KAAK,QAAQ,MAAM;EACpD,MAAM,QAAQ,MAAM,KAAK,aAAa,QAAQ,OAAO,MAAM;EAC3D,IAAI,CAAC,OACJ,MAAM,IAAI,cAAc,QAAQ,OAAO,MAAM;EAG9C,MAAM,SAAS,KAAK,gBAAgB,KAAK;EACzC,MAAM,WAAW,KAAK,IACrB,QAAQ,YAAY,KAAK,iBACzB,aACD;EAIA,IAAI,QAAS,KAAK,GAChB,WAAW,MAAM,CAAC,CAClB,WAAW,MAAM,IAAI,CAAC,CACtB,UAAU;EAEZ,IAAI,QAAQ,SAAS,QACpB,QAAQ,aAAa,OAAO,QAAQ,SAAS,KAAK;EAGnD,IAAI,QAAQ,MAAM,QACjB,QAAQ,aAAa,OAAO,QAAQ,MAAM,KAAK;OACzC,IAAI,QACV,QAAQ,MAAM,QAAQ,OAAO,OAAO,OAAO,SAAS;EAGrD,IAAI,QAAQ,UAAU,QAAQ;GAC7B,MAAM,QAAQ,aAAa,QAAQ,MAAM;GACzC,MAAM,WAAW,OAAO,cAAA,QAA8B,MAAM;GAC5D,QAAQ,MAAM,MAAM,OAAO,OAAO,UAAU,KAAK;EAClD;EAIA,MAAM,OAAO,MAAM,MAAM,MAAM,WAAW,CAAC,CAAC,CAAC,QAAQ;EACrD,MAAM,UAAU,KAAK,SAAS;EAC9B,MAAM,OAAO,UAAU,KAAK,MAAM,GAAG,QAAQ,IAAI;EAEjD,MAAM,OAAO,KAAK,KAAK,SAAS;EAChC,MAAM,QAAQ,KAAK;EAEnB,OAAO;GACN,MAAM;GACN;GACA,YACC,WAAW,UAAU,OAAO,aAAa,KAAK,OAAO,MAAM,IAAI;GAEhE,YACC,QAAQ,UAAU,UAAU,QACzB,aAAa,MAAM,OAAO,MAAM,IAChC;EACL;CACD;;;;;;CAOA,gBAAgB,OAA4C;EAC3D,MAAM,aAAa,KAAK,aAAa,MAAM,SAAS,KAAK;EACzD,IAAI,YAAY,OAAO;EAEvB,MAAM,CAAC,KAAK,GAAG,QAAQ,MAAM;EAC7B,IAAI,OAAO,KAAK,WAAW,GAC1B,OAAO;GAAE,OAAO;GAAK,WAAA;EAAyB;CAIhD;AACD"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/introspect/errors.ts
|
|
3
|
+
/**
|
|
4
|
+
* What a row query can get wrong. Each is the caller's mistake rather than the
|
|
5
|
+
* database's, so the JSON handler answers them as 4xx with the class name as
|
|
6
|
+
* the error code — a client matches on that, not on the message.
|
|
7
|
+
*/
|
|
8
|
+
/** A table that is not in the browsable schemas, or was excluded. */
|
|
9
|
+
var TableNotFound = class extends Error {
|
|
10
|
+
table;
|
|
11
|
+
schema;
|
|
12
|
+
constructor(table, schema) {
|
|
13
|
+
super(`There is no table '${table}' in schema '${schema}'. List the tables to see what can be browsed; an excluded table is left out on purpose.`);
|
|
14
|
+
this.table = table;
|
|
15
|
+
this.schema = schema;
|
|
16
|
+
this.name = "TableNotFound";
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
/** A filter or sort naming a column the table does not have. */
|
|
20
|
+
var ColumnNotFound = class extends Error {
|
|
21
|
+
column;
|
|
22
|
+
table;
|
|
23
|
+
constructor(column, table) {
|
|
24
|
+
super(`Table '${table}' has no column '${column}'. Use a column name exactly as the table description lists it.`);
|
|
25
|
+
this.column = column;
|
|
26
|
+
this.table = table;
|
|
27
|
+
this.name = "ColumnNotFound";
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
/** A filter operator that does not apply to the column's type. */
|
|
31
|
+
var UnsupportedFilterOperator = class extends Error {
|
|
32
|
+
operator;
|
|
33
|
+
column;
|
|
34
|
+
columnType;
|
|
35
|
+
constructor(operator, column, columnType) {
|
|
36
|
+
super(`'${operator}' cannot filter '${column}', a ${columnType} column. Use an operator that applies to ${columnType} values.`);
|
|
37
|
+
this.operator = operator;
|
|
38
|
+
this.column = column;
|
|
39
|
+
this.columnType = columnType;
|
|
40
|
+
this.name = "UnsupportedFilterOperator";
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
//#endregion
|
|
44
|
+
exports.ColumnNotFound = ColumnNotFound;
|
|
45
|
+
exports.TableNotFound = TableNotFound;
|
|
46
|
+
exports.UnsupportedFilterOperator = UnsupportedFilterOperator;
|
|
47
|
+
|
|
48
|
+
//# sourceMappingURL=errors.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.cjs","names":[],"sources":["../../src/introspect/errors.ts"],"sourcesContent":["/**\n * What a row query can get wrong. Each is the caller's mistake rather than the\n * database's, so the JSON handler answers them as 4xx with the class name as\n * the error code — a client matches on that, not on the message.\n */\n\n/** A table that is not in the browsable schemas, or was excluded. */\nexport class TableNotFound extends Error {\n\tconstructor(\n\t\treadonly table: string,\n\t\treadonly schema: string,\n\t) {\n\t\tsuper(\n\t\t\t`There is no table '${table}' in schema '${schema}'. List the tables to see what can be browsed; an excluded table is left out on purpose.`,\n\t\t);\n\t\tthis.name = 'TableNotFound';\n\t}\n}\n\n/** A filter or sort naming a column the table does not have. */\nexport class ColumnNotFound extends Error {\n\tconstructor(\n\t\treadonly column: string,\n\t\treadonly table: string,\n\t) {\n\t\tsuper(\n\t\t\t`Table '${table}' has no column '${column}'. Use a column name exactly as the table description lists it.`,\n\t\t);\n\t\tthis.name = 'ColumnNotFound';\n\t}\n}\n\n/** A filter operator that does not apply to the column's type. */\nexport class UnsupportedFilterOperator extends Error {\n\tconstructor(\n\t\treadonly operator: string,\n\t\treadonly column: string,\n\t\treadonly columnType: string,\n\t) {\n\t\tsuper(\n\t\t\t`'${operator}' cannot filter '${column}', a ${columnType} column. Use an operator that applies to ${columnType} values.`,\n\t\t);\n\t\tthis.name = 'UnsupportedFilterOperator';\n\t}\n}\n"],"mappings":";;;;;;;;AAOA,IAAa,gBAAb,cAAmC,MAAM;CAE9B;CACA;CAFV,YACC,OACA,QACC;EACD,MACC,sBAAsB,MAAM,eAAe,OAAO,yFACnD;EALS,KAAA,QAAA;EACA,KAAA,SAAA;EAKT,KAAK,OAAO;CACb;AACD;;AAGA,IAAa,iBAAb,cAAoC,MAAM;CAE/B;CACA;CAFV,YACC,QACA,OACC;EACD,MACC,UAAU,MAAM,mBAAmB,OAAO,gEAC3C;EALS,KAAA,SAAA;EACA,KAAA,QAAA;EAKT,KAAK,OAAO;CACb;AACD;;AAGA,IAAa,4BAAb,cAA+C,MAAM;CAE1C;CACA;CACA;CAHV,YACC,UACA,QACA,YACC;EACD,MACC,IAAI,SAAS,mBAAmB,OAAO,OAAO,WAAW,2CAA2C,WAAW,SAChH;EANS,KAAA,WAAA;EACA,KAAA,SAAA;EACA,KAAA,aAAA;EAKT,KAAK,OAAO;CACb;AACD"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region src/introspect/errors.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* What a row query can get wrong. Each is the caller's mistake rather than the
|
|
4
|
+
* database's, so the JSON handler answers them as 4xx with the class name as
|
|
5
|
+
* the error code — a client matches on that, not on the message.
|
|
6
|
+
*/
|
|
7
|
+
/** A table that is not in the browsable schemas, or was excluded. */
|
|
8
|
+
export declare class TableNotFound extends Error {
|
|
9
|
+
readonly table: string;
|
|
10
|
+
readonly schema: string;
|
|
11
|
+
constructor(table: string, schema: string);
|
|
12
|
+
}
|
|
13
|
+
/** A filter or sort naming a column the table does not have. */
|
|
14
|
+
export declare class ColumnNotFound extends Error {
|
|
15
|
+
readonly column: string;
|
|
16
|
+
readonly table: string;
|
|
17
|
+
constructor(column: string, table: string);
|
|
18
|
+
}
|
|
19
|
+
/** A filter operator that does not apply to the column's type. */
|
|
20
|
+
export declare class UnsupportedFilterOperator extends Error {
|
|
21
|
+
readonly operator: string;
|
|
22
|
+
readonly column: string;
|
|
23
|
+
readonly columnType: string;
|
|
24
|
+
constructor(operator: string, column: string, columnType: string);
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
//# sourceMappingURL=errors.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.cts","names":[],"sources":["../../src/introspect/errors.ts"],"mappings":";;;;;;;qBAOa,sBAAsB;WAExB;WACA;EAFV,YACU,eACA;;;qBAUE,uBAAuB;WAEzB;WACA;EAFV,YACU,gBACA;;;qBAUE,kCAAkC;WAEpC;WACA;WACA;EAHV,YACU,kBACA,gBACA"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region src/introspect/errors.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* What a row query can get wrong. Each is the caller's mistake rather than the
|
|
4
|
+
* database's, so the JSON handler answers them as 4xx with the class name as
|
|
5
|
+
* the error code — a client matches on that, not on the message.
|
|
6
|
+
*/
|
|
7
|
+
/** A table that is not in the browsable schemas, or was excluded. */
|
|
8
|
+
export declare class TableNotFound extends Error {
|
|
9
|
+
readonly table: string;
|
|
10
|
+
readonly schema: string;
|
|
11
|
+
constructor(table: string, schema: string);
|
|
12
|
+
}
|
|
13
|
+
/** A filter or sort naming a column the table does not have. */
|
|
14
|
+
export declare class ColumnNotFound extends Error {
|
|
15
|
+
readonly column: string;
|
|
16
|
+
readonly table: string;
|
|
17
|
+
constructor(column: string, table: string);
|
|
18
|
+
}
|
|
19
|
+
/** A filter operator that does not apply to the column's type. */
|
|
20
|
+
export declare class UnsupportedFilterOperator extends Error {
|
|
21
|
+
readonly operator: string;
|
|
22
|
+
readonly column: string;
|
|
23
|
+
readonly columnType: string;
|
|
24
|
+
constructor(operator: string, column: string, columnType: string);
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
//# sourceMappingURL=errors.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.mts","names":[],"sources":["../../src/introspect/errors.ts"],"mappings":";;;;;;;qBAOa,sBAAsB;WAExB;WACA;EAFV,YACU,eACA;;;qBAUE,uBAAuB;WAEzB;WACA;EAFV,YACU,gBACA;;;qBAUE,kCAAkC;WAEpC;WACA;WACA;EAHV,YACU,kBACA,gBACA"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//#region src/introspect/errors.ts
|
|
2
|
+
/**
|
|
3
|
+
* What a row query can get wrong. Each is the caller's mistake rather than the
|
|
4
|
+
* database's, so the JSON handler answers them as 4xx with the class name as
|
|
5
|
+
* the error code — a client matches on that, not on the message.
|
|
6
|
+
*/
|
|
7
|
+
/** A table that is not in the browsable schemas, or was excluded. */
|
|
8
|
+
var TableNotFound = class extends Error {
|
|
9
|
+
table;
|
|
10
|
+
schema;
|
|
11
|
+
constructor(table, schema) {
|
|
12
|
+
super(`There is no table '${table}' in schema '${schema}'. List the tables to see what can be browsed; an excluded table is left out on purpose.`);
|
|
13
|
+
this.table = table;
|
|
14
|
+
this.schema = schema;
|
|
15
|
+
this.name = "TableNotFound";
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
/** A filter or sort naming a column the table does not have. */
|
|
19
|
+
var ColumnNotFound = class extends Error {
|
|
20
|
+
column;
|
|
21
|
+
table;
|
|
22
|
+
constructor(column, table) {
|
|
23
|
+
super(`Table '${table}' has no column '${column}'. Use a column name exactly as the table description lists it.`);
|
|
24
|
+
this.column = column;
|
|
25
|
+
this.table = table;
|
|
26
|
+
this.name = "ColumnNotFound";
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
/** A filter operator that does not apply to the column's type. */
|
|
30
|
+
var UnsupportedFilterOperator = class extends Error {
|
|
31
|
+
operator;
|
|
32
|
+
column;
|
|
33
|
+
columnType;
|
|
34
|
+
constructor(operator, column, columnType) {
|
|
35
|
+
super(`'${operator}' cannot filter '${column}', a ${columnType} column. Use an operator that applies to ${columnType} values.`);
|
|
36
|
+
this.operator = operator;
|
|
37
|
+
this.column = column;
|
|
38
|
+
this.columnType = columnType;
|
|
39
|
+
this.name = "UnsupportedFilterOperator";
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
//#endregion
|
|
43
|
+
export { ColumnNotFound, TableNotFound, UnsupportedFilterOperator };
|
|
44
|
+
|
|
45
|
+
//# sourceMappingURL=errors.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.mjs","names":[],"sources":["../../src/introspect/errors.ts"],"sourcesContent":["/**\n * What a row query can get wrong. Each is the caller's mistake rather than the\n * database's, so the JSON handler answers them as 4xx with the class name as\n * the error code — a client matches on that, not on the message.\n */\n\n/** A table that is not in the browsable schemas, or was excluded. */\nexport class TableNotFound extends Error {\n\tconstructor(\n\t\treadonly table: string,\n\t\treadonly schema: string,\n\t) {\n\t\tsuper(\n\t\t\t`There is no table '${table}' in schema '${schema}'. List the tables to see what can be browsed; an excluded table is left out on purpose.`,\n\t\t);\n\t\tthis.name = 'TableNotFound';\n\t}\n}\n\n/** A filter or sort naming a column the table does not have. */\nexport class ColumnNotFound extends Error {\n\tconstructor(\n\t\treadonly column: string,\n\t\treadonly table: string,\n\t) {\n\t\tsuper(\n\t\t\t`Table '${table}' has no column '${column}'. Use a column name exactly as the table description lists it.`,\n\t\t);\n\t\tthis.name = 'ColumnNotFound';\n\t}\n}\n\n/** A filter operator that does not apply to the column's type. */\nexport class UnsupportedFilterOperator extends Error {\n\tconstructor(\n\t\treadonly operator: string,\n\t\treadonly column: string,\n\t\treadonly columnType: string,\n\t) {\n\t\tsuper(\n\t\t\t`'${operator}' cannot filter '${column}', a ${columnType} column. Use an operator that applies to ${columnType} values.`,\n\t\t);\n\t\tthis.name = 'UnsupportedFilterOperator';\n\t}\n}\n"],"mappings":";;;;;;;AAOA,IAAa,gBAAb,cAAmC,MAAM;CAE9B;CACA;CAFV,YACC,OACA,QACC;EACD,MACC,sBAAsB,MAAM,eAAe,OAAO,yFACnD;EALS,KAAA,QAAA;EACA,KAAA,SAAA;EAKT,KAAK,OAAO;CACb;AACD;;AAGA,IAAa,iBAAb,cAAoC,MAAM;CAE/B;CACA;CAFV,YACC,QACA,OACC;EACD,MACC,UAAU,MAAM,mBAAmB,OAAO,gEAC3C;EALS,KAAA,SAAA;EACA,KAAA,QAAA;EAKT,KAAK,OAAO;CACb;AACD;;AAGA,IAAa,4BAAb,cAA+C,MAAM;CAE1C;CACA;CACA;CAHV,YACC,UACA,QACA,YACC;EACD,MACC,IAAI,SAAS,mBAAmB,OAAO,OAAO,WAAW,2CAA2C,WAAW,SAChH;EANS,KAAA,WAAA;EACA,KAAA,SAAA;EACA,KAAA,aAAA;EAKT,KAAK,OAAO;CACb;AACD"}
|