@geekmidas/db 10.0.0-alpha.9 → 10.0.0-alpha.90

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.
Files changed (138) hide show
  1. package/README.md +17 -0
  2. package/dist/errors.cjs +72 -5
  3. package/dist/errors.cjs.map +1 -0
  4. package/dist/errors.d.cts +4 -6
  5. package/dist/errors.d.cts.map +1 -1
  6. package/dist/errors.d.mts +4 -6
  7. package/dist/errors.d.mts.map +1 -1
  8. package/dist/errors.mjs +68 -2
  9. package/dist/errors.mjs.map +1 -0
  10. package/dist/introspect/DataBrowser.cjs +117 -0
  11. package/dist/introspect/DataBrowser.cjs.map +1 -0
  12. package/dist/introspect/DataBrowser.d.cts +51 -0
  13. package/dist/introspect/DataBrowser.d.cts.map +1 -0
  14. package/dist/introspect/DataBrowser.d.mts +51 -0
  15. package/dist/introspect/DataBrowser.d.mts.map +1 -0
  16. package/dist/introspect/DataBrowser.mjs +116 -0
  17. package/dist/introspect/DataBrowser.mjs.map +1 -0
  18. package/dist/introspect/errors.cjs +48 -0
  19. package/dist/introspect/errors.cjs.map +1 -0
  20. package/dist/introspect/errors.d.cts +27 -0
  21. package/dist/introspect/errors.d.cts.map +1 -0
  22. package/dist/introspect/errors.d.mts +27 -0
  23. package/dist/introspect/errors.d.mts.map +1 -0
  24. package/dist/introspect/errors.mjs +45 -0
  25. package/dist/introspect/errors.mjs.map +1 -0
  26. package/dist/introspect/filtering.cjs +101 -0
  27. package/dist/introspect/filtering.cjs.map +1 -0
  28. package/dist/introspect/filtering.d.cts +29 -0
  29. package/dist/introspect/filtering.d.cts.map +1 -0
  30. package/dist/introspect/filtering.d.mts +29 -0
  31. package/dist/introspect/filtering.d.mts.map +1 -0
  32. package/dist/introspect/filtering.mjs +98 -0
  33. package/dist/introspect/filtering.mjs.map +1 -0
  34. package/dist/introspect/handler.cjs +159 -0
  35. package/dist/introspect/handler.cjs.map +1 -0
  36. package/dist/introspect/handler.d.cts +43 -0
  37. package/dist/introspect/handler.d.cts.map +1 -0
  38. package/dist/introspect/handler.d.mts +43 -0
  39. package/dist/introspect/handler.d.mts.map +1 -0
  40. package/dist/introspect/handler.mjs +158 -0
  41. package/dist/introspect/handler.mjs.map +1 -0
  42. package/dist/introspect/index.cjs +22 -0
  43. package/dist/introspect/index.d.cts +8 -0
  44. package/dist/introspect/index.d.mts +8 -0
  45. package/dist/introspect/index.mjs +8 -0
  46. package/dist/introspect/introspection.cjs +149 -0
  47. package/dist/introspect/introspection.cjs.map +1 -0
  48. package/dist/introspect/introspection.d.cts +25 -0
  49. package/dist/introspect/introspection.d.cts.map +1 -0
  50. package/dist/introspect/introspection.d.mts +25 -0
  51. package/dist/introspect/introspection.d.mts.map +1 -0
  52. package/dist/introspect/introspection.mjs +146 -0
  53. package/dist/introspect/introspection.mjs.map +1 -0
  54. package/dist/introspect/types.cjs +24 -0
  55. package/dist/introspect/types.cjs.map +1 -0
  56. package/dist/introspect/types.d.cts +113 -0
  57. package/dist/introspect/types.d.cts.map +1 -0
  58. package/dist/introspect/types.d.mts +113 -0
  59. package/dist/introspect/types.d.mts.map +1 -0
  60. package/dist/introspect/types.mjs +23 -0
  61. package/dist/introspect/types.mjs.map +1 -0
  62. package/dist/kysely/pagination.cjs +8 -9
  63. package/dist/kysely/pagination.cjs.map +1 -1
  64. package/dist/kysely/pagination.d.cts +4 -14
  65. package/dist/kysely/pagination.d.cts.map +1 -1
  66. package/dist/kysely/pagination.d.mts +4 -14
  67. package/dist/kysely/pagination.d.mts.map +1 -1
  68. package/dist/kysely/pagination.mjs +7 -10
  69. package/dist/kysely/pagination.mjs.map +1 -1
  70. package/dist/kysely.cjs +11 -2
  71. package/dist/kysely.cjs.map +1 -0
  72. package/dist/kysely.d.cts +9 -2
  73. package/dist/kysely.d.cts.map +1 -0
  74. package/dist/kysely.d.mts +9 -2
  75. package/dist/kysely.d.mts.map +1 -0
  76. package/dist/kysely.mjs +10 -2
  77. package/dist/kysely.mjs.map +1 -0
  78. package/dist/objection/pagination.cjs +7 -7
  79. package/dist/objection/pagination.cjs.map +1 -1
  80. package/dist/objection/pagination.d.cts +4 -14
  81. package/dist/objection/pagination.d.cts.map +1 -1
  82. package/dist/objection/pagination.d.mts +4 -14
  83. package/dist/objection/pagination.d.mts.map +1 -1
  84. package/dist/objection/pagination.mjs +6 -8
  85. package/dist/objection/pagination.mjs.map +1 -1
  86. package/dist/{pagination-kv-KWS-7.d.mts → pagination-CrU89125.d.cts} +14 -4
  87. package/dist/pagination-CrU89125.d.cts.map +1 -0
  88. package/dist/{pagination-BAFX7C1I.d.cts → pagination-CrU89125.d.mts} +14 -4
  89. package/dist/pagination-CrU89125.d.mts.map +1 -0
  90. package/dist/pagination.cjs +55 -4
  91. package/dist/pagination.cjs.map +1 -0
  92. package/dist/pagination.d.cts +2 -2
  93. package/dist/pagination.d.mts +2 -2
  94. package/dist/pagination.mjs +51 -2
  95. package/dist/pagination.mjs.map +1 -0
  96. package/dist/pg/roles.cjs +26 -3
  97. package/dist/pg/roles.cjs.map +1 -1
  98. package/dist/pg/roles.d.cts +16 -8
  99. package/dist/pg/roles.d.cts.map +1 -1
  100. package/dist/pg/roles.d.mts +16 -8
  101. package/dist/pg/roles.d.mts.map +1 -1
  102. package/dist/pg/roles.mjs +25 -2
  103. package/dist/pg/roles.mjs.map +1 -1
  104. package/dist/pg/url.cjs +18 -3
  105. package/dist/pg/url.cjs.map +1 -1
  106. package/dist/pg/url.d.cts +2 -4
  107. package/dist/pg/url.d.cts.map +1 -1
  108. package/dist/pg/url.d.mts +2 -4
  109. package/dist/pg/url.d.mts.map +1 -1
  110. package/dist/pg/url.mjs +17 -3
  111. package/dist/pg/url.mjs.map +1 -1
  112. package/dist/rls.cjs +5 -29
  113. package/dist/rls.cjs.map +1 -1
  114. package/dist/rls.d.cts +6 -10
  115. package/dist/rls.d.cts.map +1 -1
  116. package/dist/rls.d.mts +6 -10
  117. package/dist/rls.d.mts.map +1 -1
  118. package/dist/rls.mjs +3 -5
  119. package/dist/rls.mjs.map +1 -1
  120. package/package.json +14 -3
  121. package/dist/errors-CfFCan-X.mjs +0 -69
  122. package/dist/errors-CfFCan-X.mjs.map +0 -1
  123. package/dist/errors-Cs5jlAv-.cjs +0 -93
  124. package/dist/errors-Cs5jlAv-.cjs.map +0 -1
  125. package/dist/kysely-BAedYsvn.d.mts +0 -13
  126. package/dist/kysely-BAedYsvn.d.mts.map +0 -1
  127. package/dist/kysely-Bo_Zg-D3.mjs +0 -11
  128. package/dist/kysely-Bo_Zg-D3.mjs.map +0 -1
  129. package/dist/kysely-D3eeuId4.d.cts +0 -13
  130. package/dist/kysely-D3eeuId4.d.cts.map +0 -1
  131. package/dist/kysely-LWvS2ekY.cjs +0 -17
  132. package/dist/kysely-LWvS2ekY.cjs.map +0 -1
  133. package/dist/pagination-BAFX7C1I.d.cts.map +0 -1
  134. package/dist/pagination-Bu1dqOy0.cjs +0 -55
  135. package/dist/pagination-Bu1dqOy0.cjs.map +0 -1
  136. package/dist/pagination-CQ8QHXaL.mjs +0 -37
  137. package/dist/pagination-CQ8QHXaL.mjs.map +0 -1
  138. package/dist/pagination-kv-KWS-7.d.mts.map +0 -1
@@ -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"}
@@ -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