@geekmidas/db 10.0.0-alpha.11 → 10.0.0-alpha.110

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
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`
package/dist/errors.cjs CHANGED
@@ -1,6 +1,73 @@
1
- const require_errors = require('./errors-Cs5jlAv-.cjs');
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/errors.ts
3
+ /**
4
+ * Database URL errors and redaction — engine-neutral.
5
+ *
6
+ * Nothing here is Postgres-specific: every engine addresses itself as a URL
7
+ * carrying a password, and "must name a host" holds for all of them. The parts
8
+ * that differ — libpq's `options=-c search_path=…`, which no other engine
9
+ * accepts — live under the driver they belong to.
10
+ *
11
+ * Messages state the rule, which is constant; the offending value is a field.
12
+ * An interpolated message cannot be matched on and carries user input into
13
+ * every log line that touches it.
14
+ */
15
+ /**
16
+ * Replace the password in a connection string with `***`.
17
+ *
18
+ * A connection string is a secret, and the places it is most likely to be
19
+ * written down — a log line, an error, a crash report shipped to a third party
20
+ * — are the places nobody is reading carefully. This exists so that redacting
21
+ * is the easy thing to do at each of them.
22
+ *
23
+ * Safe on input that is not a URL: it returns a constant rather than throwing,
24
+ * because this runs on paths that must not fail. Anything unparseable is
25
+ * treated as potentially secret and withheld entirely rather than echoed.
26
+ */
27
+ function redactDatabaseUrl(url) {
28
+ try {
29
+ const parsed = new URL(url);
30
+ if (!parsed.password) return url;
31
+ parsed.password = "***";
32
+ return parsed.toString();
33
+ } catch {
34
+ return "<unparseable url>";
35
+ }
36
+ }
37
+ /**
38
+ * Base for an address that cannot become a connection string.
39
+ *
40
+ * Deliberately carries no URL field: these are thrown while *composing* one, so
41
+ * there is no URL yet — only the parts, which include the password.
42
+ */
43
+ var DatabaseAddressError = class extends Error {
44
+ constructor(message) {
45
+ super(message);
46
+ this.name = new.target.name;
47
+ }
48
+ };
49
+ /** An address with no host to connect to. */
50
+ var MissingDatabaseHost = class extends DatabaseAddressError {
51
+ constructor() {
52
+ super("A database address must name a host");
53
+ }
54
+ };
55
+ /**
56
+ * An address with no database name.
57
+ *
58
+ * Postgres silently falls back to the connecting role's name, so the app
59
+ * connects successfully to the wrong database and fails later with confusing
60
+ * "relation does not exist" errors. Rejecting it here is much cheaper.
61
+ */
62
+ var MissingDatabaseName = class extends DatabaseAddressError {
63
+ constructor() {
64
+ super("A database address must name a database");
65
+ }
66
+ };
67
+ //#endregion
68
+ exports.DatabaseAddressError = DatabaseAddressError;
69
+ exports.MissingDatabaseHost = MissingDatabaseHost;
70
+ exports.MissingDatabaseName = MissingDatabaseName;
71
+ exports.redactDatabaseUrl = redactDatabaseUrl;
2
72
 
3
- exports.DatabaseAddressError = require_errors.DatabaseAddressError;
4
- exports.MissingDatabaseHost = require_errors.MissingDatabaseHost;
5
- exports.MissingDatabaseName = require_errors.MissingDatabaseName;
6
- exports.redactDatabaseUrl = require_errors.redactDatabaseUrl;
73
+ //# sourceMappingURL=errors.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.cjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/**\n * Database URL errors and redaction — engine-neutral.\n *\n * Nothing here is Postgres-specific: every engine addresses itself as a URL\n * carrying a password, and \"must name a host\" holds for all of them. The parts\n * that differ — libpq's `options=-c search_path=…`, which no other engine\n * accepts — live under the driver they belong to.\n *\n * Messages state the rule, which is constant; the offending value is a field.\n * An interpolated message cannot be matched on and carries user input into\n * every log line that touches it.\n */\n\n/**\n * Replace the password in a connection string with `***`.\n *\n * A connection string is a secret, and the places it is most likely to be\n * written down — a log line, an error, a crash report shipped to a third party\n * — are the places nobody is reading carefully. This exists so that redacting\n * is the easy thing to do at each of them.\n *\n * Safe on input that is not a URL: it returns a constant rather than throwing,\n * because this runs on paths that must not fail. Anything unparseable is\n * treated as potentially secret and withheld entirely rather than echoed.\n */\nexport function redactDatabaseUrl(url: string): string {\n\ttry {\n\t\tconst parsed = new URL(url);\n\t\tif (!parsed.password) return url;\n\t\tparsed.password = '***';\n\t\treturn parsed.toString();\n\t} catch {\n\t\treturn '<unparseable url>';\n\t}\n}\n\n/**\n * Base for an address that cannot become a connection string.\n *\n * Deliberately carries no URL field: these are thrown while *composing* one, so\n * there is no URL yet — only the parts, which include the password.\n */\nexport abstract class DatabaseAddressError extends Error {\n\tconstructor(message: string) {\n\t\tsuper(message);\n\t\tthis.name = new.target.name;\n\t}\n}\n\n/** An address with no host to connect to. */\nexport class MissingDatabaseHost extends DatabaseAddressError {\n\tconstructor() {\n\t\tsuper('A database address must name a host');\n\t}\n}\n\n/**\n * An address with no database name.\n *\n * Postgres silently falls back to the connecting role's name, so the app\n * connects successfully to the wrong database and fails later with confusing\n * \"relation does not exist\" errors. Rejecting it here is much cheaper.\n */\nexport class MissingDatabaseName extends DatabaseAddressError {\n\tconstructor() {\n\t\tsuper('A database address must name a database');\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,SAAgB,kBAAkB,KAAqB;CACtD,IAAI;EACH,MAAM,SAAS,IAAI,IAAI,GAAG;EAC1B,IAAI,CAAC,OAAO,UAAU,OAAO;EAC7B,OAAO,WAAW;EAClB,OAAO,OAAO,SAAS;CACxB,QAAQ;EACP,OAAO;CACR;AACD;;;;;;;AAQA,IAAsB,uBAAtB,cAAmD,MAAM;CACxD,YAAY,SAAiB;EAC5B,MAAM,OAAO;EACb,KAAK,OAAO,WAAW;CACxB;AACD;;AAGA,IAAa,sBAAb,cAAyC,qBAAqB;CAC7D,cAAc;EACb,MAAM,qCAAqC;CAC5C;AACD;;;;;;;;AASA,IAAa,sBAAb,cAAyC,qBAAqB;CAC7D,cAAc;EACb,MAAM,yCAAyC;CAChD;AACD"}
package/dist/errors.d.cts CHANGED
@@ -23,18 +23,18 @@
23
23
  * because this runs on paths that must not fail. Anything unparseable is
24
24
  * treated as potentially secret and withheld entirely rather than echoed.
25
25
  */
26
- declare function redactDatabaseUrl(url: string): string;
26
+ export declare function redactDatabaseUrl(url: string): string;
27
27
  /**
28
28
  * Base for an address that cannot become a connection string.
29
29
  *
30
30
  * Deliberately carries no URL field: these are thrown while *composing* one, so
31
31
  * there is no URL yet — only the parts, which include the password.
32
32
  */
33
- declare abstract class DatabaseAddressError extends Error {
33
+ export declare abstract class DatabaseAddressError extends Error {
34
34
  constructor(message: string);
35
35
  }
36
36
  /** An address with no host to connect to. */
37
- declare class MissingDatabaseHost extends DatabaseAddressError {
37
+ export declare class MissingDatabaseHost extends DatabaseAddressError {
38
38
  constructor();
39
39
  }
40
40
  /**
@@ -44,10 +44,8 @@ declare class MissingDatabaseHost extends DatabaseAddressError {
44
44
  * connects successfully to the wrong database and fails later with confusing
45
45
  * "relation does not exist" errors. Rejecting it here is much cheaper.
46
46
  */
47
- declare class MissingDatabaseName extends DatabaseAddressError {
47
+ export declare class MissingDatabaseName extends DatabaseAddressError {
48
48
  constructor();
49
49
  }
50
- //# sourceMappingURL=errors.d.ts.map
51
50
  //#endregion
52
- export { DatabaseAddressError, MissingDatabaseHost, MissingDatabaseName, redactDatabaseUrl };
53
51
  //# sourceMappingURL=errors.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.cts","names":[],"sources":["../src/errors.ts"],"sourcesContent":[],"mappings":";;AAyBA;AAiBA;AAQA;AAaA;;;;;;;;;;;;;;;;;;;;iBAtCgB,iBAAA;;;;;;;uBAiBM,oBAAA,SAA6B,KAAK;;;;cAQ3C,mBAAA,SAA4B,oBAAoB;;;;;;;;;;cAahD,mBAAA,SAA4B,oBAAoB"}
1
+ {"version":3,"file":"errors.d.cts","names":[],"sources":["../src/errors.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;wBAyBgB,kBAAkB;;;;;;;8BAiBZ,6BAA6B;EAClD,YAAY;;;qBAOA,4BAA4B;EACxC;;;;;;;;;qBAYY,4BAA4B;EACxC"}
package/dist/errors.d.mts CHANGED
@@ -23,18 +23,18 @@
23
23
  * because this runs on paths that must not fail. Anything unparseable is
24
24
  * treated as potentially secret and withheld entirely rather than echoed.
25
25
  */
26
- declare function redactDatabaseUrl(url: string): string;
26
+ export declare function redactDatabaseUrl(url: string): string;
27
27
  /**
28
28
  * Base for an address that cannot become a connection string.
29
29
  *
30
30
  * Deliberately carries no URL field: these are thrown while *composing* one, so
31
31
  * there is no URL yet — only the parts, which include the password.
32
32
  */
33
- declare abstract class DatabaseAddressError extends Error {
33
+ export declare abstract class DatabaseAddressError extends Error {
34
34
  constructor(message: string);
35
35
  }
36
36
  /** An address with no host to connect to. */
37
- declare class MissingDatabaseHost extends DatabaseAddressError {
37
+ export declare class MissingDatabaseHost extends DatabaseAddressError {
38
38
  constructor();
39
39
  }
40
40
  /**
@@ -44,10 +44,8 @@ declare class MissingDatabaseHost extends DatabaseAddressError {
44
44
  * connects successfully to the wrong database and fails later with confusing
45
45
  * "relation does not exist" errors. Rejecting it here is much cheaper.
46
46
  */
47
- declare class MissingDatabaseName extends DatabaseAddressError {
47
+ export declare class MissingDatabaseName extends DatabaseAddressError {
48
48
  constructor();
49
49
  }
50
- //# sourceMappingURL=errors.d.ts.map
51
50
  //#endregion
52
- export { DatabaseAddressError, MissingDatabaseHost, MissingDatabaseName, redactDatabaseUrl };
53
51
  //# sourceMappingURL=errors.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.mts","names":[],"sources":["../src/errors.ts"],"sourcesContent":[],"mappings":";;AAyBA;AAiBA;AAQA;AAaA;;;;;;;;;;;;;;;;;;;;iBAtCgB,iBAAA;;;;;;;uBAiBM,oBAAA,SAA6B,KAAK;;;;cAQ3C,mBAAA,SAA4B,oBAAoB;;;;;;;;;;cAahD,mBAAA,SAA4B,oBAAoB"}
1
+ {"version":3,"file":"errors.d.mts","names":[],"sources":["../src/errors.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;wBAyBgB,kBAAkB;;;;;;;8BAiBZ,6BAA6B;EAClD,YAAY;;;qBAOA,4BAA4B;EACxC;;;;;;;;;qBAYY,4BAA4B;EACxC"}
package/dist/errors.mjs CHANGED
@@ -1,3 +1,69 @@
1
- import { DatabaseAddressError, MissingDatabaseHost, MissingDatabaseName, redactDatabaseUrl } from "./errors-CfFCan-X.mjs";
1
+ //#region src/errors.ts
2
+ /**
3
+ * Database URL errors and redaction — engine-neutral.
4
+ *
5
+ * Nothing here is Postgres-specific: every engine addresses itself as a URL
6
+ * carrying a password, and "must name a host" holds for all of them. The parts
7
+ * that differ — libpq's `options=-c search_path=…`, which no other engine
8
+ * accepts — live under the driver they belong to.
9
+ *
10
+ * Messages state the rule, which is constant; the offending value is a field.
11
+ * An interpolated message cannot be matched on and carries user input into
12
+ * every log line that touches it.
13
+ */
14
+ /**
15
+ * Replace the password in a connection string with `***`.
16
+ *
17
+ * A connection string is a secret, and the places it is most likely to be
18
+ * written down — a log line, an error, a crash report shipped to a third party
19
+ * — are the places nobody is reading carefully. This exists so that redacting
20
+ * is the easy thing to do at each of them.
21
+ *
22
+ * Safe on input that is not a URL: it returns a constant rather than throwing,
23
+ * because this runs on paths that must not fail. Anything unparseable is
24
+ * treated as potentially secret and withheld entirely rather than echoed.
25
+ */
26
+ function redactDatabaseUrl(url) {
27
+ try {
28
+ const parsed = new URL(url);
29
+ if (!parsed.password) return url;
30
+ parsed.password = "***";
31
+ return parsed.toString();
32
+ } catch {
33
+ return "<unparseable url>";
34
+ }
35
+ }
36
+ /**
37
+ * Base for an address that cannot become a connection string.
38
+ *
39
+ * Deliberately carries no URL field: these are thrown while *composing* one, so
40
+ * there is no URL yet — only the parts, which include the password.
41
+ */
42
+ var DatabaseAddressError = class extends Error {
43
+ constructor(message) {
44
+ super(message);
45
+ this.name = new.target.name;
46
+ }
47
+ };
48
+ /** An address with no host to connect to. */
49
+ var MissingDatabaseHost = class extends DatabaseAddressError {
50
+ constructor() {
51
+ super("A database address must name a host");
52
+ }
53
+ };
54
+ /**
55
+ * An address with no database name.
56
+ *
57
+ * Postgres silently falls back to the connecting role's name, so the app
58
+ * connects successfully to the wrong database and fails later with confusing
59
+ * "relation does not exist" errors. Rejecting it here is much cheaper.
60
+ */
61
+ var MissingDatabaseName = class extends DatabaseAddressError {
62
+ constructor() {
63
+ super("A database address must name a database");
64
+ }
65
+ };
66
+ //#endregion
67
+ export { DatabaseAddressError, MissingDatabaseHost, MissingDatabaseName, redactDatabaseUrl };
2
68
 
3
- export { DatabaseAddressError, MissingDatabaseHost, MissingDatabaseName, redactDatabaseUrl };
69
+ //# sourceMappingURL=errors.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.mjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/**\n * Database URL errors and redaction — engine-neutral.\n *\n * Nothing here is Postgres-specific: every engine addresses itself as a URL\n * carrying a password, and \"must name a host\" holds for all of them. The parts\n * that differ — libpq's `options=-c search_path=…`, which no other engine\n * accepts — live under the driver they belong to.\n *\n * Messages state the rule, which is constant; the offending value is a field.\n * An interpolated message cannot be matched on and carries user input into\n * every log line that touches it.\n */\n\n/**\n * Replace the password in a connection string with `***`.\n *\n * A connection string is a secret, and the places it is most likely to be\n * written down — a log line, an error, a crash report shipped to a third party\n * — are the places nobody is reading carefully. This exists so that redacting\n * is the easy thing to do at each of them.\n *\n * Safe on input that is not a URL: it returns a constant rather than throwing,\n * because this runs on paths that must not fail. Anything unparseable is\n * treated as potentially secret and withheld entirely rather than echoed.\n */\nexport function redactDatabaseUrl(url: string): string {\n\ttry {\n\t\tconst parsed = new URL(url);\n\t\tif (!parsed.password) return url;\n\t\tparsed.password = '***';\n\t\treturn parsed.toString();\n\t} catch {\n\t\treturn '<unparseable url>';\n\t}\n}\n\n/**\n * Base for an address that cannot become a connection string.\n *\n * Deliberately carries no URL field: these are thrown while *composing* one, so\n * there is no URL yet — only the parts, which include the password.\n */\nexport abstract class DatabaseAddressError extends Error {\n\tconstructor(message: string) {\n\t\tsuper(message);\n\t\tthis.name = new.target.name;\n\t}\n}\n\n/** An address with no host to connect to. */\nexport class MissingDatabaseHost extends DatabaseAddressError {\n\tconstructor() {\n\t\tsuper('A database address must name a host');\n\t}\n}\n\n/**\n * An address with no database name.\n *\n * Postgres silently falls back to the connecting role's name, so the app\n * connects successfully to the wrong database and fails later with confusing\n * \"relation does not exist\" errors. Rejecting it here is much cheaper.\n */\nexport class MissingDatabaseName extends DatabaseAddressError {\n\tconstructor() {\n\t\tsuper('A database address must name a database');\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,SAAgB,kBAAkB,KAAqB;CACtD,IAAI;EACH,MAAM,SAAS,IAAI,IAAI,GAAG;EAC1B,IAAI,CAAC,OAAO,UAAU,OAAO;EAC7B,OAAO,WAAW;EAClB,OAAO,OAAO,SAAS;CACxB,QAAQ;EACP,OAAO;CACR;AACD;;;;;;;AAQA,IAAsB,uBAAtB,cAAmD,MAAM;CACxD,YAAY,SAAiB;EAC5B,MAAM,OAAO;EACb,KAAK,OAAO,WAAW;CACxB;AACD;;AAGA,IAAa,sBAAb,cAAyC,qBAAqB;CAC7D,cAAc;EACb,MAAM,qCAAqC;CAC5C;AACD;;;;;;;;AASA,IAAa,sBAAb,cAAyC,qBAAqB;CAC7D,cAAc;EACb,MAAM,yCAAyC;CAChD;AACD"}
@@ -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