@geekmidas/db 10.0.0-alpha.3 → 10.0.0-alpha.30

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 (85) hide show
  1. package/dist/errors.cjs +72 -5
  2. package/dist/errors.cjs.map +1 -0
  3. package/dist/errors.d.cts +4 -6
  4. package/dist/errors.d.cts.map +1 -1
  5. package/dist/errors.d.mts +4 -6
  6. package/dist/errors.d.mts.map +1 -1
  7. package/dist/errors.mjs +68 -2
  8. package/dist/errors.mjs.map +1 -0
  9. package/dist/kysely/pagination.cjs +7 -9
  10. package/dist/kysely/pagination.cjs.map +1 -1
  11. package/dist/kysely/pagination.d.cts +4 -14
  12. package/dist/kysely/pagination.d.cts.map +1 -1
  13. package/dist/kysely/pagination.d.mts +4 -14
  14. package/dist/kysely/pagination.d.mts.map +1 -1
  15. package/dist/kysely/pagination.mjs +6 -9
  16. package/dist/kysely/pagination.mjs.map +1 -1
  17. package/dist/kysely.cjs +11 -2
  18. package/dist/kysely.cjs.map +1 -0
  19. package/dist/kysely.d.cts +9 -2
  20. package/dist/kysely.d.cts.map +1 -0
  21. package/dist/kysely.d.mts +9 -2
  22. package/dist/kysely.d.mts.map +1 -0
  23. package/dist/kysely.mjs +10 -2
  24. package/dist/kysely.mjs.map +1 -0
  25. package/dist/objection/pagination.cjs +6 -7
  26. package/dist/objection/pagination.cjs.map +1 -1
  27. package/dist/objection/pagination.d.cts +4 -14
  28. package/dist/objection/pagination.d.cts.map +1 -1
  29. package/dist/objection/pagination.d.mts +4 -14
  30. package/dist/objection/pagination.d.mts.map +1 -1
  31. package/dist/objection/pagination.mjs +5 -7
  32. package/dist/objection/pagination.mjs.map +1 -1
  33. package/dist/pagination.cjs +39 -4
  34. package/dist/pagination.cjs.map +1 -0
  35. package/dist/pagination.d.cts +30 -2
  36. package/dist/pagination.d.cts.map +1 -0
  37. package/dist/pagination.d.mts +30 -2
  38. package/dist/pagination.d.mts.map +1 -0
  39. package/dist/pagination.mjs +36 -2
  40. package/dist/pagination.mjs.map +1 -0
  41. package/dist/pg/roles.cjs +3 -2
  42. package/dist/pg/roles.cjs.map +1 -1
  43. package/dist/pg/roles.d.cts +6 -8
  44. package/dist/pg/roles.d.cts.map +1 -1
  45. package/dist/pg/roles.d.mts +6 -8
  46. package/dist/pg/roles.d.mts.map +1 -1
  47. package/dist/pg/roles.mjs +2 -1
  48. package/dist/pg/roles.mjs.map +1 -1
  49. package/dist/pg/url.cjs +18 -3
  50. package/dist/pg/url.cjs.map +1 -1
  51. package/dist/pg/url.d.cts +2 -4
  52. package/dist/pg/url.d.cts.map +1 -1
  53. package/dist/pg/url.d.mts +2 -4
  54. package/dist/pg/url.d.mts.map +1 -1
  55. package/dist/pg/url.mjs +17 -3
  56. package/dist/pg/url.mjs.map +1 -1
  57. package/dist/rls.cjs +5 -29
  58. package/dist/rls.cjs.map +1 -1
  59. package/dist/rls.d.cts +6 -10
  60. package/dist/rls.d.cts.map +1 -1
  61. package/dist/rls.d.mts +6 -10
  62. package/dist/rls.d.mts.map +1 -1
  63. package/dist/rls.mjs +3 -5
  64. package/dist/rls.mjs.map +1 -1
  65. package/package.json +8 -8
  66. package/dist/errors-CfFCan-X.mjs +0 -69
  67. package/dist/errors-CfFCan-X.mjs.map +0 -1
  68. package/dist/errors-Cs5jlAv-.cjs +0 -93
  69. package/dist/errors-Cs5jlAv-.cjs.map +0 -1
  70. package/dist/kysely-BAedYsvn.d.mts +0 -13
  71. package/dist/kysely-BAedYsvn.d.mts.map +0 -1
  72. package/dist/kysely-Bo_Zg-D3.mjs +0 -11
  73. package/dist/kysely-Bo_Zg-D3.mjs.map +0 -1
  74. package/dist/kysely-D3eeuId4.d.cts +0 -13
  75. package/dist/kysely-D3eeuId4.d.cts.map +0 -1
  76. package/dist/kysely-LWvS2ekY.cjs +0 -17
  77. package/dist/kysely-LWvS2ekY.cjs.map +0 -1
  78. package/dist/pagination-BAFX7C1I.d.cts +0 -32
  79. package/dist/pagination-BAFX7C1I.d.cts.map +0 -1
  80. package/dist/pagination-Bu1dqOy0.cjs +0 -55
  81. package/dist/pagination-Bu1dqOy0.cjs.map +0 -1
  82. package/dist/pagination-CQ8QHXaL.mjs +0 -37
  83. package/dist/pagination-CQ8QHXaL.mjs.map +0 -1
  84. package/dist/pagination-kv-KWS-7.d.mts +0 -32
  85. package/dist/pagination-kv-KWS-7.d.mts.map +0 -1
@@ -1,12 +1,10 @@
1
- import { Direction, PaginationResult, decodeCursor, encodeCursor } from "../pagination-BAFX7C1I.cjs";
1
+ import { Direction, PaginationResult, decodeCursor, encodeCursor } from "../pagination.cjs";
2
2
  import { Model, QueryBuilder } from "objection";
3
-
4
3
  //#region src/objection/pagination.d.ts
5
-
6
4
  /**
7
5
  * Options for paginated search with Objection.js models.
8
6
  */
9
- interface ObjectionPaginatedSearchOptions<TModel extends Model, TMapRow extends (row: TModel) => unknown = (row: TModel) => TModel> {
7
+ export interface ObjectionPaginatedSearchOptions<TModel extends Model, TMapRow extends (row: TModel) => unknown = (row: TModel) => TModel> {
10
8
  /** The Objection QueryBuilder to paginate (e.g. User.query(trx).where(...)) */
11
9
  query: QueryBuilder<TModel>;
12
10
  /** Cursor value for pagination (value of cursorField from previous page) */
@@ -37,15 +35,7 @@ interface ObjectionPaginatedSearchOptions<TModel extends Model, TMapRow extends
37
35
  * });
38
36
  * ```
39
37
  */
40
- declare function paginatedSearch<TModel extends Model, TMapRow extends (row: TModel) => unknown = (row: TModel) => TModel>({
41
- query,
42
- cursor,
43
- limit,
44
- mapRow,
45
- cursorField,
46
- cursorDirection
47
- }: ObjectionPaginatedSearchOptions<TModel, TMapRow>): Promise<PaginationResult<Awaited<ReturnType<TMapRow>>>>;
48
- //# sourceMappingURL=pagination.d.ts.map
38
+ export declare function paginatedSearch<TModel extends Model, TMapRow extends (row: TModel) => unknown = (row: TModel) => TModel>({ query, cursor, limit, mapRow, cursorField, cursorDirection }: ObjectionPaginatedSearchOptions<TModel, TMapRow>): Promise<PaginationResult<Awaited<ReturnType<TMapRow>>>>;
49
39
  //#endregion
50
- export { Direction, ObjectionPaginatedSearchOptions, PaginationResult, decodeCursor, encodeCursor, paginatedSearch };
40
+ export { Direction, type PaginationResult, decodeCursor, encodeCursor };
51
41
  //# sourceMappingURL=pagination.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"pagination.d.cts","names":[],"sources":["../../src/objection/pagination.ts"],"sourcesContent":[],"mappings":";;;;;;AAaA;;AACgB,UADC,+BACD,CAAA,eAAA,KAAA,EAAA,gBAAA,CAAA,GAAA,EACO,MADP,EAAA,GAAA,OAAA,GAAA,CAAA,GAAA,EACkC,MADlC,EAAA,GAC6C,MAD7C,CAAA,CAAA;EAAK;EACQ,KAAqB,EAG1C,YAH0C,CAG7B,MAH6B,CAAA;EAAM;EAAW,MAG9C,CAAA,EAAA,MAAA;EAAM;EAAP,KAMV,CAAA,EAAA,MAAA;EAAO;EAIW,MAAA,CAAA,EAJlB,OAIkB;EAoBN;EAAe,WAAA,CAAA,EAAA,MAAA;EAAA;EAChB,eACE,CAAA,EAtBJ,SAsBI;;;;;;;;;;;;;;;;;AAQsC;;iBAVvC,+BACN,6BACO,2BAA2B,WAAW;;;;;;;GAQ1D,gCAAgC,QAAQ,WAAW,QACrD,iBAAiB,QAAQ,WAAW"}
1
+ {"version":3,"file":"pagination.d.cts","names":[],"sources":["../../src/objection/pagination.ts"],"mappings":";;;;;;iBAaiB,gCAChB,eAAe,OACf,iBAAiB,KAAK,sBAAsB,KAAK,WAAW;;EAG5D,OAAO,aAAa;;EAEpB;;EAEA;;EAEA,SAAS;;EAET;;EAEA,kBAAkB;;;;;;;;;;;;;;;;;;;wBAoBG,gBACrB,eAAe,OACf,iBAAiB,KAAK,sBAAsB,KAAK,WAAW,UAE5D,OACA,QACA,OACA,QACA,aACA,mBACE,gCAAgC,QAAQ,WAAW,QACrD,iBAAiB,QAAQ,WAAW"}
@@ -1,12 +1,10 @@
1
- import { Direction, PaginationResult, decodeCursor, encodeCursor } from "../pagination-kv-KWS-7.mjs";
1
+ import { Direction, PaginationResult, decodeCursor, encodeCursor } from "../pagination.mjs";
2
2
  import { Model, QueryBuilder } from "objection";
3
-
4
3
  //#region src/objection/pagination.d.ts
5
-
6
4
  /**
7
5
  * Options for paginated search with Objection.js models.
8
6
  */
9
- interface ObjectionPaginatedSearchOptions<TModel extends Model, TMapRow extends (row: TModel) => unknown = (row: TModel) => TModel> {
7
+ export interface ObjectionPaginatedSearchOptions<TModel extends Model, TMapRow extends (row: TModel) => unknown = (row: TModel) => TModel> {
10
8
  /** The Objection QueryBuilder to paginate (e.g. User.query(trx).where(...)) */
11
9
  query: QueryBuilder<TModel>;
12
10
  /** Cursor value for pagination (value of cursorField from previous page) */
@@ -37,15 +35,7 @@ interface ObjectionPaginatedSearchOptions<TModel extends Model, TMapRow extends
37
35
  * });
38
36
  * ```
39
37
  */
40
- declare function paginatedSearch<TModel extends Model, TMapRow extends (row: TModel) => unknown = (row: TModel) => TModel>({
41
- query,
42
- cursor,
43
- limit,
44
- mapRow,
45
- cursorField,
46
- cursorDirection
47
- }: ObjectionPaginatedSearchOptions<TModel, TMapRow>): Promise<PaginationResult<Awaited<ReturnType<TMapRow>>>>;
48
- //# sourceMappingURL=pagination.d.ts.map
38
+ export declare function paginatedSearch<TModel extends Model, TMapRow extends (row: TModel) => unknown = (row: TModel) => TModel>({ query, cursor, limit, mapRow, cursorField, cursorDirection }: ObjectionPaginatedSearchOptions<TModel, TMapRow>): Promise<PaginationResult<Awaited<ReturnType<TMapRow>>>>;
49
39
  //#endregion
50
- export { Direction, ObjectionPaginatedSearchOptions, PaginationResult, decodeCursor, encodeCursor, paginatedSearch };
40
+ export { Direction, type PaginationResult, decodeCursor, encodeCursor };
51
41
  //# sourceMappingURL=pagination.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"pagination.d.mts","names":[],"sources":["../../src/objection/pagination.ts"],"sourcesContent":[],"mappings":";;;;;;AAaA;;AACgB,UADC,+BACD,CAAA,eAAA,KAAA,EAAA,gBAAA,CAAA,GAAA,EACO,MADP,EAAA,GAAA,OAAA,GAAA,CAAA,GAAA,EACkC,MADlC,EAAA,GAC6C,MAD7C,CAAA,CAAA;EAAK;EACQ,KAAqB,EAG1C,YAH0C,CAG7B,MAH6B,CAAA;EAAM;EAAW,MAG9C,CAAA,EAAA,MAAA;EAAM;EAAP,KAMV,CAAA,EAAA,MAAA;EAAO;EAIW,MAAA,CAAA,EAJlB,OAIkB;EAoBN;EAAe,WAAA,CAAA,EAAA,MAAA;EAAA;EAChB,eACE,CAAA,EAtBJ,SAsBI;;;;;;;;;;;;;;;;;AAQsC;;iBAVvC,+BACN,6BACO,2BAA2B,WAAW;;;;;;;GAQ1D,gCAAgC,QAAQ,WAAW,QACrD,iBAAiB,QAAQ,WAAW"}
1
+ {"version":3,"file":"pagination.d.mts","names":[],"sources":["../../src/objection/pagination.ts"],"mappings":";;;;;;iBAaiB,gCAChB,eAAe,OACf,iBAAiB,KAAK,sBAAsB,KAAK,WAAW;;EAG5D,OAAO,aAAa;;EAEpB;;EAEA;;EAEA,SAAS;;EAET;;EAEA,kBAAkB;;;;;;;;;;;;;;;;;;;wBAoBG,gBACrB,eAAe,OACf,iBAAiB,KAAK,sBAAsB,KAAK,WAAW,UAE5D,OACA,QACA,OACA,QACA,aACA,mBACE,gCAAgC,QAAQ,WAAW,QACrD,iBAAiB,QAAQ,WAAW"}
@@ -1,5 +1,4 @@
1
- import { Direction, decodeCursor, encodeCursor } from "../pagination-CQ8QHXaL.mjs";
2
-
1
+ import { Direction, decodeCursor, encodeCursor } from "../pagination.mjs";
3
2
  //#region src/objection/pagination.ts
4
3
  /**
5
4
  * Cursor-based paginated search for Objection.js models.
@@ -18,11 +17,11 @@ import { Direction, decodeCursor, encodeCursor } from "../pagination-CQ8QHXaL.mj
18
17
  * });
19
18
  * ```
20
19
  */
21
- async function paginatedSearch({ query, cursor, limit = 20, mapRow, cursorField = "id", cursorDirection = Direction.Asc }) {
20
+ async function paginatedSearch({ query, cursor, limit = 20, mapRow, cursorField = "id", cursorDirection = "asc" }) {
22
21
  const total = await query.resultSize();
23
22
  let paginatedQuery = query.clone();
24
23
  if (cursor) {
25
- const operator = cursorDirection === Direction.Asc ? ">" : "<";
24
+ const operator = cursorDirection === "asc" ? ">" : "<";
26
25
  paginatedQuery = paginatedQuery.where(cursorField, operator, cursor);
27
26
  }
28
27
  const data = await paginatedQuery.orderBy(cursorField, cursorDirection).limit(limit + 1);
@@ -31,9 +30,8 @@ async function paginatedSearch({ query, cursor, limit = 20, mapRow, cursorField
31
30
  const lastRow = rows[rows.length - 1];
32
31
  const nextCursor = hasMore && lastRow ? String(lastRow[cursorField]) : void 0;
33
32
  const mapper = mapRow ?? ((row) => row);
34
- const items = await Promise.all(rows.map(mapper));
35
33
  return {
36
- items,
34
+ items: await Promise.all(rows.map(mapper)),
37
35
  pagination: {
38
36
  total,
39
37
  hasMore,
@@ -41,7 +39,7 @@ async function paginatedSearch({ query, cursor, limit = 20, mapRow, cursorField
41
39
  }
42
40
  };
43
41
  }
44
-
45
42
  //#endregion
46
43
  export { Direction, decodeCursor, encodeCursor, paginatedSearch };
44
+
47
45
  //# sourceMappingURL=pagination.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"pagination.mjs","names":["row: TModel"],"sources":["../../src/objection/pagination.ts"],"sourcesContent":["import type { Model, QueryBuilder } from 'objection';\nimport {\n\tDirection,\n\tdecodeCursor,\n\tencodeCursor,\n\ttype PaginationResult,\n} from '../pagination';\n\nexport { Direction, decodeCursor, encodeCursor, type PaginationResult };\n\n/**\n * Options for paginated search with Objection.js models.\n */\nexport interface ObjectionPaginatedSearchOptions<\n\tTModel extends Model,\n\tTMapRow extends (row: TModel) => unknown = (row: TModel) => TModel,\n> {\n\t/** The Objection QueryBuilder to paginate (e.g. User.query(trx).where(...)) */\n\tquery: QueryBuilder<TModel>;\n\t/** Cursor value for pagination (value of cursorField from previous page) */\n\tcursor?: string;\n\t/** Maximum number of items per page (default: 20) */\n\tlimit?: number;\n\t/** Function to transform each row to the output format (default: identity) */\n\tmapRow?: TMapRow;\n\t/** Field to use for cursor pagination (default: 'id') */\n\tcursorField?: string;\n\t/** Sort direction for cursor field (default: Direction.Asc) */\n\tcursorDirection?: Direction;\n}\n\n/**\n * Cursor-based paginated search for Objection.js models.\n *\n * Accepts a pre-built QueryBuilder so callers can apply filters,\n * eager loading, and scopes before pagination is layered on top.\n *\n * @example\n * ```typescript\n * const result = await paginatedSearch({\n * query: User.query(trx).where('orgId', orgId).withGraphFetched('roles'),\n * cursor: previousCursor,\n * limit: 20,\n * cursorField: 'createdAt',\n * cursorDirection: Direction.Desc,\n * });\n * ```\n */\nexport async function paginatedSearch<\n\tTModel extends Model,\n\tTMapRow extends (row: TModel) => unknown = (row: TModel) => TModel,\n>({\n\tquery,\n\tcursor,\n\tlimit = 20,\n\tmapRow,\n\tcursorField = 'id',\n\tcursorDirection = Direction.Asc,\n}: ObjectionPaginatedSearchOptions<TModel, TMapRow>): Promise<\n\tPaginationResult<Awaited<ReturnType<TMapRow>>>\n> {\n\t// Get total count (without cursor filtering)\n\tconst total = await query.resultSize();\n\n\t// Apply cursor if provided\n\tlet paginatedQuery = query.clone();\n\tif (cursor) {\n\t\tconst operator = cursorDirection === Direction.Asc ? '>' : '<';\n\t\tpaginatedQuery = paginatedQuery.where(cursorField, operator, cursor);\n\t}\n\n\t// Fetch one extra to determine if there are more results\n\tconst data = await paginatedQuery\n\t\t.orderBy(cursorField, cursorDirection)\n\t\t.limit(limit + 1);\n\n\tconst hasMore = data.length > limit;\n\tconst rows = hasMore ? data.slice(0, limit) : data;\n\tconst lastRow = rows[rows.length - 1];\n\tconst nextCursor =\n\t\thasMore && lastRow\n\t\t\t? String((lastRow as Record<string, unknown>)[cursorField])\n\t\t\t: undefined;\n\n\tconst mapper = mapRow ?? ((row: TModel) => row);\n\tconst items = (await Promise.all(rows.map(mapper))) as Awaited<\n\t\tReturnType<TMapRow>\n\t>[];\n\n\treturn {\n\t\titems,\n\t\tpagination: {\n\t\t\ttotal,\n\t\t\thasMore,\n\t\t\tcursor: nextCursor,\n\t\t},\n\t};\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAgDA,eAAsB,gBAGpB,EACD,OACA,QACA,QAAQ,IACR,QACA,cAAc,MACd,kBAAkB,UAAU,KACsB,EAEjD;CAED,MAAM,QAAQ,MAAM,MAAM,YAAY;CAGtC,IAAI,iBAAiB,MAAM,OAAO;AAClC,KAAI,QAAQ;EACX,MAAM,WAAW,oBAAoB,UAAU,MAAM,MAAM;AAC3D,mBAAiB,eAAe,MAAM,aAAa,UAAU,OAAO;CACpE;CAGD,MAAM,OAAO,MAAM,eACjB,QAAQ,aAAa,gBAAgB,CACrC,MAAM,QAAQ,EAAE;CAElB,MAAM,UAAU,KAAK,SAAS;CAC9B,MAAM,OAAO,UAAU,KAAK,MAAM,GAAG,MAAM,GAAG;CAC9C,MAAM,UAAU,KAAK,KAAK,SAAS;CACnC,MAAM,aACL,WAAW,UACR,OAAQ,QAAoC,aAAa;CAG7D,MAAM,SAAS,WAAW,CAACA,QAAgB;CAC3C,MAAM,QAAS,MAAM,QAAQ,IAAI,KAAK,IAAI,OAAO,CAAC;AAIlD,QAAO;EACN;EACA,YAAY;GACX;GACA;GACA,QAAQ;EACR;CACD;AACD"}
1
+ {"version":3,"file":"pagination.mjs","names":[],"sources":["../../src/objection/pagination.ts"],"sourcesContent":["import type { Model, QueryBuilder } from 'objection';\nimport {\n\tDirection,\n\tdecodeCursor,\n\tencodeCursor,\n\ttype PaginationResult,\n} from '../pagination';\n\nexport { Direction, decodeCursor, encodeCursor, type PaginationResult };\n\n/**\n * Options for paginated search with Objection.js models.\n */\nexport interface ObjectionPaginatedSearchOptions<\n\tTModel extends Model,\n\tTMapRow extends (row: TModel) => unknown = (row: TModel) => TModel,\n> {\n\t/** The Objection QueryBuilder to paginate (e.g. User.query(trx).where(...)) */\n\tquery: QueryBuilder<TModel>;\n\t/** Cursor value for pagination (value of cursorField from previous page) */\n\tcursor?: string;\n\t/** Maximum number of items per page (default: 20) */\n\tlimit?: number;\n\t/** Function to transform each row to the output format (default: identity) */\n\tmapRow?: TMapRow;\n\t/** Field to use for cursor pagination (default: 'id') */\n\tcursorField?: string;\n\t/** Sort direction for cursor field (default: Direction.Asc) */\n\tcursorDirection?: Direction;\n}\n\n/**\n * Cursor-based paginated search for Objection.js models.\n *\n * Accepts a pre-built QueryBuilder so callers can apply filters,\n * eager loading, and scopes before pagination is layered on top.\n *\n * @example\n * ```typescript\n * const result = await paginatedSearch({\n * query: User.query(trx).where('orgId', orgId).withGraphFetched('roles'),\n * cursor: previousCursor,\n * limit: 20,\n * cursorField: 'createdAt',\n * cursorDirection: Direction.Desc,\n * });\n * ```\n */\nexport async function paginatedSearch<\n\tTModel extends Model,\n\tTMapRow extends (row: TModel) => unknown = (row: TModel) => TModel,\n>({\n\tquery,\n\tcursor,\n\tlimit = 20,\n\tmapRow,\n\tcursorField = 'id',\n\tcursorDirection = Direction.Asc,\n}: ObjectionPaginatedSearchOptions<TModel, TMapRow>): Promise<\n\tPaginationResult<Awaited<ReturnType<TMapRow>>>\n> {\n\t// Get total count (without cursor filtering)\n\tconst total = await query.resultSize();\n\n\t// Apply cursor if provided\n\tlet paginatedQuery = query.clone();\n\tif (cursor) {\n\t\tconst operator = cursorDirection === Direction.Asc ? '>' : '<';\n\t\tpaginatedQuery = paginatedQuery.where(cursorField, operator, cursor);\n\t}\n\n\t// Fetch one extra to determine if there are more results\n\tconst data = await paginatedQuery\n\t\t.orderBy(cursorField, cursorDirection)\n\t\t.limit(limit + 1);\n\n\tconst hasMore = data.length > limit;\n\tconst rows = hasMore ? data.slice(0, limit) : data;\n\tconst lastRow = rows[rows.length - 1];\n\tconst nextCursor =\n\t\thasMore && lastRow\n\t\t\t? String((lastRow as Record<string, unknown>)[cursorField])\n\t\t\t: undefined;\n\n\tconst mapper = mapRow ?? ((row: TModel) => row);\n\tconst items = (await Promise.all(rows.map(mapper))) as Awaited<\n\t\tReturnType<TMapRow>\n\t>[];\n\n\treturn {\n\t\titems,\n\t\tpagination: {\n\t\t\ttotal,\n\t\t\thasMore,\n\t\t\tcursor: nextCursor,\n\t\t},\n\t};\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAgDA,eAAsB,gBAGpB,EACD,OACA,QACA,QAAQ,IACR,QACA,cAAc,MACd,kBAAA,SAGC;CAED,MAAM,QAAQ,MAAM,MAAM,WAAW;CAGrC,IAAI,iBAAiB,MAAM,MAAM;CACjC,IAAI,QAAQ;EACX,MAAM,WAAW,oBAAA,QAAoC,MAAM;EAC3D,iBAAiB,eAAe,MAAM,aAAa,UAAU,MAAM;CACpE;CAGA,MAAM,OAAO,MAAM,eACjB,QAAQ,aAAa,eAAe,CAAC,CACrC,MAAM,QAAQ,CAAC;CAEjB,MAAM,UAAU,KAAK,SAAS;CAC9B,MAAM,OAAO,UAAU,KAAK,MAAM,GAAG,KAAK,IAAI;CAC9C,MAAM,UAAU,KAAK,KAAK,SAAS;CACnC,MAAM,aACL,WAAW,UACR,OAAQ,QAAoC,YAAY,IACxD,KAAA;CAEJ,MAAM,SAAS,YAAY,QAAgB;CAK3C,OAAO;EACN,OAAA,MALoB,QAAQ,IAAI,KAAK,IAAI,MAAM,CAAC;EAMhD,YAAY;GACX;GACA;GACA,QAAQ;EACT;CACD;AACD"}
@@ -1,5 +1,40 @@
1
- const require_pagination = require('./pagination-Bu1dqOy0.cjs');
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/pagination.ts
3
+ /**
4
+ * Sort direction for cursor-based pagination.
5
+ */
6
+ let Direction = /* @__PURE__ */ function(Direction) {
7
+ Direction["Asc"] = "asc";
8
+ Direction["Desc"] = "desc";
9
+ return Direction;
10
+ }({});
11
+ /**
12
+ * Encode a cursor value for safe URL transmission.
13
+ * Supports various types: string, number, Date, etc.
14
+ */
15
+ function encodeCursor(value) {
16
+ const payload = {
17
+ v: value instanceof Date ? value.toISOString() : value,
18
+ t: value instanceof Date ? "date" : typeof value
19
+ };
20
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
21
+ }
22
+ /**
23
+ * Decode a cursor string back to its original value.
24
+ */
25
+ function decodeCursor(cursor) {
26
+ try {
27
+ const json = Buffer.from(cursor, "base64url").toString("utf-8");
28
+ const payload = JSON.parse(json);
29
+ if (payload.t === "date") return new Date(payload.v);
30
+ return payload.v;
31
+ } catch {
32
+ throw new Error("Invalid cursor format");
33
+ }
34
+ }
35
+ //#endregion
36
+ exports.Direction = Direction;
37
+ exports.decodeCursor = decodeCursor;
38
+ exports.encodeCursor = encodeCursor;
2
39
 
3
- exports.Direction = require_pagination.Direction;
4
- exports.decodeCursor = require_pagination.decodeCursor;
5
- exports.encodeCursor = require_pagination.encodeCursor;
40
+ //# sourceMappingURL=pagination.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pagination.cjs","names":[],"sources":["../src/pagination.ts"],"sourcesContent":["/**\n * Sort direction for cursor-based pagination.\n */\nexport enum Direction {\n\tAsc = 'asc',\n\tDesc = 'desc',\n}\n\n/**\n * Result of a paginated query.\n */\nexport interface PaginationResult<TItem> {\n\titems: TItem[];\n\tpagination: {\n\t\ttotal: number;\n\t\thasMore: boolean;\n\t\tcursor?: string;\n\t};\n}\n\n/**\n * Encode a cursor value for safe URL transmission.\n * Supports various types: string, number, Date, etc.\n */\nexport function encodeCursor(value: unknown): string {\n\tconst payload = {\n\t\tv: value instanceof Date ? value.toISOString() : value,\n\t\tt: value instanceof Date ? 'date' : typeof value,\n\t};\n\treturn Buffer.from(JSON.stringify(payload)).toString('base64url');\n}\n\n/**\n * Decode a cursor string back to its original value.\n */\nexport function decodeCursor(cursor: string): unknown {\n\ttry {\n\t\tconst json = Buffer.from(cursor, 'base64url').toString('utf-8');\n\t\tconst payload = JSON.parse(json);\n\n\t\tif (payload.t === 'date') {\n\t\t\treturn new Date(payload.v);\n\t\t}\n\n\t\treturn payload.v;\n\t} catch {\n\t\tthrow new Error('Invalid cursor format');\n\t}\n}\n"],"mappings":";;;;;AAGA,IAAY,YAAL,yBAAA,WAAA;CACN,UAAA,SAAA;CACA,UAAA,UAAA;;AACD,EAAA,CAAA,CAAA;;;;;AAkBA,SAAgB,aAAa,OAAwB;CACpD,MAAM,UAAU;EACf,GAAG,iBAAiB,OAAO,MAAM,YAAY,IAAI;EACjD,GAAG,iBAAiB,OAAO,SAAS,OAAO;CAC5C;CACA,OAAO,OAAO,KAAK,KAAK,UAAU,OAAO,CAAC,CAAC,CAAC,SAAS,WAAW;AACjE;;;;AAKA,SAAgB,aAAa,QAAyB;CACrD,IAAI;EACH,MAAM,OAAO,OAAO,KAAK,QAAQ,WAAW,CAAC,CAAC,SAAS,OAAO;EAC9D,MAAM,UAAU,KAAK,MAAM,IAAI;EAE/B,IAAI,QAAQ,MAAM,QACjB,OAAO,IAAI,KAAK,QAAQ,CAAC;EAG1B,OAAO,QAAQ;CAChB,QAAQ;EACP,MAAM,IAAI,MAAM,uBAAuB;CACxC;AACD"}
@@ -1,2 +1,30 @@
1
- import { Direction, PaginationResult, decodeCursor, encodeCursor } from "./pagination-BAFX7C1I.cjs";
2
- export { Direction, PaginationResult, decodeCursor, encodeCursor };
1
+ //#region src/pagination.d.ts
2
+ /**
3
+ * Sort direction for cursor-based pagination.
4
+ */
5
+ export declare enum Direction {
6
+ Asc = "asc",
7
+ Desc = "desc"
8
+ }
9
+ /**
10
+ * Result of a paginated query.
11
+ */
12
+ export interface PaginationResult<TItem> {
13
+ items: TItem[];
14
+ pagination: {
15
+ total: number;
16
+ hasMore: boolean;
17
+ cursor?: string;
18
+ };
19
+ }
20
+ /**
21
+ * Encode a cursor value for safe URL transmission.
22
+ * Supports various types: string, number, Date, etc.
23
+ */
24
+ export declare function encodeCursor(value: unknown): string;
25
+ /**
26
+ * Decode a cursor string back to its original value.
27
+ */
28
+ export declare function decodeCursor(cursor: string): unknown;
29
+ //#endregion
30
+ //# sourceMappingURL=pagination.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pagination.d.cts","names":[],"sources":["../src/pagination.ts"],"mappings":";;;;oBAGY;EACX;EACA;;;;;iBAMgB,iBAAiB;EACjC,OAAO;EACP;IACC;IACA;IACA;;;;;;;wBAQc,aAAa;;;;wBAWb,aAAa"}
@@ -1,2 +1,30 @@
1
- import { Direction, PaginationResult, decodeCursor, encodeCursor } from "./pagination-kv-KWS-7.mjs";
2
- export { Direction, PaginationResult, decodeCursor, encodeCursor };
1
+ //#region src/pagination.d.ts
2
+ /**
3
+ * Sort direction for cursor-based pagination.
4
+ */
5
+ export declare enum Direction {
6
+ Asc = "asc",
7
+ Desc = "desc"
8
+ }
9
+ /**
10
+ * Result of a paginated query.
11
+ */
12
+ export interface PaginationResult<TItem> {
13
+ items: TItem[];
14
+ pagination: {
15
+ total: number;
16
+ hasMore: boolean;
17
+ cursor?: string;
18
+ };
19
+ }
20
+ /**
21
+ * Encode a cursor value for safe URL transmission.
22
+ * Supports various types: string, number, Date, etc.
23
+ */
24
+ export declare function encodeCursor(value: unknown): string;
25
+ /**
26
+ * Decode a cursor string back to its original value.
27
+ */
28
+ export declare function decodeCursor(cursor: string): unknown;
29
+ //#endregion
30
+ //# sourceMappingURL=pagination.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pagination.d.mts","names":[],"sources":["../src/pagination.ts"],"mappings":";;;;oBAGY;EACX;EACA;;;;;iBAMgB,iBAAiB;EACjC,OAAO;EACP;IACC;IACA;IACA;;;;;;;wBAQc,aAAa;;;;wBAWb,aAAa"}
@@ -1,3 +1,37 @@
1
- import { Direction, decodeCursor, encodeCursor } from "./pagination-CQ8QHXaL.mjs";
1
+ //#region src/pagination.ts
2
+ /**
3
+ * Sort direction for cursor-based pagination.
4
+ */
5
+ let Direction = /* @__PURE__ */ function(Direction) {
6
+ Direction["Asc"] = "asc";
7
+ Direction["Desc"] = "desc";
8
+ return Direction;
9
+ }({});
10
+ /**
11
+ * Encode a cursor value for safe URL transmission.
12
+ * Supports various types: string, number, Date, etc.
13
+ */
14
+ function encodeCursor(value) {
15
+ const payload = {
16
+ v: value instanceof Date ? value.toISOString() : value,
17
+ t: value instanceof Date ? "date" : typeof value
18
+ };
19
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
20
+ }
21
+ /**
22
+ * Decode a cursor string back to its original value.
23
+ */
24
+ function decodeCursor(cursor) {
25
+ try {
26
+ const json = Buffer.from(cursor, "base64url").toString("utf-8");
27
+ const payload = JSON.parse(json);
28
+ if (payload.t === "date") return new Date(payload.v);
29
+ return payload.v;
30
+ } catch {
31
+ throw new Error("Invalid cursor format");
32
+ }
33
+ }
34
+ //#endregion
35
+ export { Direction, decodeCursor, encodeCursor };
2
36
 
3
- export { Direction, decodeCursor, encodeCursor };
37
+ //# sourceMappingURL=pagination.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pagination.mjs","names":[],"sources":["../src/pagination.ts"],"sourcesContent":["/**\n * Sort direction for cursor-based pagination.\n */\nexport enum Direction {\n\tAsc = 'asc',\n\tDesc = 'desc',\n}\n\n/**\n * Result of a paginated query.\n */\nexport interface PaginationResult<TItem> {\n\titems: TItem[];\n\tpagination: {\n\t\ttotal: number;\n\t\thasMore: boolean;\n\t\tcursor?: string;\n\t};\n}\n\n/**\n * Encode a cursor value for safe URL transmission.\n * Supports various types: string, number, Date, etc.\n */\nexport function encodeCursor(value: unknown): string {\n\tconst payload = {\n\t\tv: value instanceof Date ? value.toISOString() : value,\n\t\tt: value instanceof Date ? 'date' : typeof value,\n\t};\n\treturn Buffer.from(JSON.stringify(payload)).toString('base64url');\n}\n\n/**\n * Decode a cursor string back to its original value.\n */\nexport function decodeCursor(cursor: string): unknown {\n\ttry {\n\t\tconst json = Buffer.from(cursor, 'base64url').toString('utf-8');\n\t\tconst payload = JSON.parse(json);\n\n\t\tif (payload.t === 'date') {\n\t\t\treturn new Date(payload.v);\n\t\t}\n\n\t\treturn payload.v;\n\t} catch {\n\t\tthrow new Error('Invalid cursor format');\n\t}\n}\n"],"mappings":";;;;AAGA,IAAY,YAAL,yBAAA,WAAA;CACN,UAAA,SAAA;CACA,UAAA,UAAA;;AACD,EAAA,CAAA,CAAA;;;;;AAkBA,SAAgB,aAAa,OAAwB;CACpD,MAAM,UAAU;EACf,GAAG,iBAAiB,OAAO,MAAM,YAAY,IAAI;EACjD,GAAG,iBAAiB,OAAO,SAAS,OAAO;CAC5C;CACA,OAAO,OAAO,KAAK,KAAK,UAAU,OAAO,CAAC,CAAC,CAAC,SAAS,WAAW;AACjE;;;;AAKA,SAAgB,aAAa,QAAyB;CACrD,IAAI;EACH,MAAM,OAAO,OAAO,KAAK,QAAQ,WAAW,CAAC,CAAC,SAAS,OAAO;EAC9D,MAAM,UAAU,KAAK,MAAM,IAAI;EAE/B,IAAI,QAAQ,MAAM,QACjB,OAAO,IAAI,KAAK,QAAQ,CAAC;EAG1B,OAAO,QAAQ;CAChB,QAAQ;EACP,MAAM,IAAI,MAAM,uBAAuB;CACxC;AACD"}
package/dist/pg/roles.cjs CHANGED
@@ -1,4 +1,4 @@
1
-
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  //#region src/pg/roles.ts
3
3
  /**
4
4
  * Everything one tenant's roles need, in dependency order.
@@ -105,6 +105,7 @@ function readerStatements(spec, reader) {
105
105
  }
106
106
  /** A reader role was named and no password was supplied for it. */
107
107
  var ReaderNeedsPassword = class extends Error {
108
+ role;
108
109
  constructor(role) {
109
110
  super(`'${role}' is a reader role and no password was supplied for it. This module composes DDL and invents no secrets: pass passwords.reader alongside the role name.`);
110
111
  this.role = role;
@@ -145,10 +146,10 @@ function ident(name) {
145
146
  function literal(value) {
146
147
  return `'${value.replace(/'/g, "''")}'`;
147
148
  }
148
-
149
149
  //#endregion
150
150
  exports.ReaderNeedsPassword = ReaderNeedsPassword;
151
151
  exports.ownerRole = ownerRole;
152
152
  exports.readerRole = readerRole;
153
153
  exports.roleStatements = roleStatements;
154
+
154
155
  //# sourceMappingURL=roles.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"roles.cjs","names":["spec: RoleSpec","reader: string","role: string","runtime: string","name: string","value: string"],"sources":["../../src/pg/roles.ts"],"sourcesContent":["/**\n * The role DDL — an owner, a runtime role, and the grants between them.\n *\n * Generation only. The same statements have to run in-process from `gkm dev`\n * and from a provisioner inside a VPC at deploy, and one implementation is what\n * stops \"works locally, fails deployed\" — so this composes SQL and applies\n * none of it.\n *\n * **What the split is for.** A handler connects as the runtime role, which can\n * read and write rows and cannot create, alter, or drop anything. A migrator\n * connects as the owner, which can. That is a security property rather than a\n * convention — a compromised handler cannot `DROP TABLE` because its role holds\n * no such grant — and it is only a property at all once these statements have\n * actually run. Until then a schema is a namespace, not a boundary.\n *\n * **Passwords are an input.** This module invents no secrets: a caller derives\n * or fetches them and passes them in, because where a password comes from is\n * the one part that legitimately differs between a laptop and a deploy.\n */\n\n/** The roles and schema one tenant needs. */\nexport interface RoleSpec {\n\t/** The role a handler connects as. Reads and writes rows; owns nothing. */\n\truntime: string;\n\t/** The role a migrator connects as. Owns the schema and everything in it. */\n\towner: string;\n\t/**\n\t * A third role that may only read, for a reader endpoint.\n\t *\n\t * Optional because it is only worth creating where something points at it.\n\t * Read-only is enforced *here*, by the grants, rather than by which endpoint\n\t * a URL happens to name — which is what makes falling back to the writer's\n\t * endpoint safe when a cluster has no replica.\n\t */\n\treader?: string;\n\t/** The schema they operate in, pinned on every role's `search_path`. */\n\tschema: string;\n\t/** Supplied, never generated here. */\n\tpasswords: { runtime: string; owner: string; reader?: string };\n}\n\n/** One statement, and how to tell whether it still needs running. */\nexport interface RoleStatement {\n\t/** What it does, for the line a reconciler prints. */\n\tdescribe: string;\n\t/**\n\t * Answers \"does this already exist\", parameterised.\n\t *\n\t * Absent means the statement is idempotent in itself and is run every time —\n\t * a `GRANT` re-granting what is already granted is a no-op, and checking\n\t * would cost a round trip to learn nothing.\n\t */\n\texists?: { sql: string; values: unknown[] };\n\tsql: string;\n}\n\n/**\n * Everything one tenant's roles need, in dependency order.\n *\n * The owner before the schema it owns, the schema before the grants on it. A\n * caller applies them in sequence and may stop at the first failure without\n * leaving a half-granted role that looks complete.\n */\nexport function roleStatements(spec: RoleSpec): RoleStatement[] {\n\tconst { runtime, owner, schema, passwords } = spec;\n\n\treturn [\n\t\t{\n\t\t\tdescribe: `role ${owner}`,\n\t\t\texists: roleExists(owner),\n\t\t\tsql: `CREATE ROLE ${ident(owner)} LOGIN PASSWORD ${literal(passwords.owner)}`,\n\t\t},\n\t\t{\n\t\t\tdescribe: `role ${runtime}`,\n\t\t\texists: roleExists(runtime),\n\t\t\tsql: `CREATE ROLE ${ident(runtime)} LOGIN PASSWORD ${literal(passwords.runtime)}`,\n\t\t},\n\t\t{\n\t\t\t// Postgres will not let you create an object owned by a role you are\n\t\t\t// not a member of, and an RDS master is not a superuser — so\n\t\t\t// `CREATE SCHEMA … AUTHORIZATION` fails with \"must be able to SET\n\t\t\t// ROLE\" unless the connected role is granted membership first.\n\t\t\t//\n\t\t\t// Granting is idempotent and needs no check. It also leaves the master\n\t\t\t// able to administer the schema, which is what you want from the one\n\t\t\t// credential that exists before any role does.\n\t\t\tdescribe: `${owner} granted to the connecting role`,\n\t\t\tsql: `GRANT ${ident(owner)} TO CURRENT_USER`,\n\t\t},\n\t\t{\n\t\t\tdescribe: `schema ${schema}`,\n\t\t\texists: {\n\t\t\t\tsql: 'SELECT 1 FROM information_schema.schemata WHERE schema_name = $1',\n\t\t\t\tvalues: [schema],\n\t\t\t},\n\t\t\t// Owned by the owner from the moment it exists. Creating it as the\n\t\t\t// cluster master and granting afterwards leaves a window where the\n\t\t\t// master owns objects the owner is supposed to.\n\t\t\tsql: `CREATE SCHEMA ${ident(schema)} AUTHORIZATION ${ident(owner)}`,\n\t\t},\n\t\t{\n\t\t\t// And the schema that was already there, which the guard above skips.\n\t\t\t// A schema created before its roles existed is owned by whoever\n\t\t\t// created it — the cluster master — and nothing below fixes that, so\n\t\t\t// the owner cannot create in the schema it is supposed to own. The\n\t\t\t// failure is `no schema has been selected to create in`, which names\n\t\t\t// neither the schema nor the owner and reads like a missing\n\t\t\t// `search_path`.\n\t\t\t//\n\t\t\t// Idempotent when the owner is already right, so it costs a statement\n\t\t\t// rather than a check.\n\t\t\tdescribe: `${schema} is owned by ${owner}`,\n\t\t\tsql: `ALTER SCHEMA ${ident(schema)} OWNER TO ${ident(owner)}`,\n\t\t},\n\t\t{\n\t\t\tdescribe: `${runtime} may use ${schema}`,\n\t\t\tsql: `GRANT USAGE ON SCHEMA ${ident(schema)} TO ${ident(runtime)}`,\n\t\t},\n\t\t{\n\t\t\t// The important half: *future* tables. A grant on what exists today\n\t\t\t// covers nothing the next migration creates, which is how a runtime\n\t\t\t// role silently loses access one deploy later.\n\t\t\tdescribe: `${runtime} may read and write future tables in ${schema}`,\n\t\t\tsql:\n\t\t\t\t`ALTER DEFAULT PRIVILEGES FOR ROLE ${ident(owner)} IN SCHEMA ${ident(schema)} ` +\n\t\t\t\t`GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO ${ident(runtime)}`,\n\t\t},\n\t\t{\n\t\t\tdescribe: `${runtime} may use future sequences in ${schema}`,\n\t\t\tsql:\n\t\t\t\t`ALTER DEFAULT PRIVILEGES FOR ROLE ${ident(owner)} IN SCHEMA ${ident(schema)} ` +\n\t\t\t\t`GRANT USAGE, SELECT ON SEQUENCES TO ${ident(runtime)}`,\n\t\t},\n\t\t{\n\t\t\t// And the tables that are already there, for a schema that existed\n\t\t\t// before its roles did.\n\t\t\tdescribe: `${runtime} may read and write existing tables in ${schema}`,\n\t\t\tsql:\n\t\t\t\t`GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA ${ident(schema)} ` +\n\t\t\t\t`TO ${ident(runtime)}`,\n\t\t},\n\t\t{\n\t\t\tdescribe: `${runtime} may use existing sequences in ${schema}`,\n\t\t\tsql: `GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA ${ident(schema)} TO ${ident(runtime)}`,\n\t\t},\n\t\t{\n\t\t\t// Pinned on the role rather than carried in every URL. A connection\n\t\t\t// string that has to remember `search_path` is one that eventually\n\t\t\t// forgets, and the forgetting looks like an empty database.\n\t\t\tdescribe: `${runtime} resolves names in ${schema}`,\n\t\t\tsql: `ALTER ROLE ${ident(runtime)} SET search_path TO ${ident(schema)}`,\n\t\t},\n\t\t{\n\t\t\tdescribe: `${owner} resolves names in ${schema}`,\n\t\t\tsql: `ALTER ROLE ${ident(owner)} SET search_path TO ${ident(schema)}`,\n\t\t},\n\t\t...(spec.reader ? readerStatements(spec, spec.reader) : []),\n\t];\n}\n\n/**\n * A role that may read and nothing else.\n *\n * `SELECT` and no more, on what exists and on what the owner creates later. No\n * `INSERT`, no `UPDATE`, no `DELETE`, and no sequence `USAGE` — a reader that\n * could take a sequence value could not write the row it was for, so granting\n * it would be surface with no use.\n */\nfunction readerStatements(spec: RoleSpec, reader: string): RoleStatement[] {\n\tconst { owner, schema, passwords } = spec;\n\tconst password = passwords.reader;\n\n\tif (!password) throw new ReaderNeedsPassword(reader);\n\n\treturn [\n\t\t{\n\t\t\tdescribe: `role ${reader}`,\n\t\t\texists: roleExists(reader),\n\t\t\tsql: `CREATE ROLE ${ident(reader)} LOGIN PASSWORD ${literal(password)}`,\n\t\t},\n\t\t{\n\t\t\tdescribe: `${reader} may use ${schema}`,\n\t\t\tsql: `GRANT USAGE ON SCHEMA ${ident(schema)} TO ${ident(reader)}`,\n\t\t},\n\t\t{\n\t\t\tdescribe: `${reader} may read future tables in ${schema}`,\n\t\t\tsql:\n\t\t\t\t`ALTER DEFAULT PRIVILEGES FOR ROLE ${ident(owner)} IN SCHEMA ${ident(schema)} ` +\n\t\t\t\t`GRANT SELECT ON TABLES TO ${ident(reader)}`,\n\t\t},\n\t\t{\n\t\t\tdescribe: `${reader} may read existing tables in ${schema}`,\n\t\t\tsql: `GRANT SELECT ON ALL TABLES IN SCHEMA ${ident(schema)} TO ${ident(reader)}`,\n\t\t},\n\t\t{\n\t\t\tdescribe: `${reader} resolves names in ${schema}`,\n\t\t\tsql: `ALTER ROLE ${ident(reader)} SET search_path TO ${ident(schema)}`,\n\t\t},\n\t];\n}\n\n/** A reader role was named and no password was supplied for it. */\nexport class ReaderNeedsPassword extends Error {\n\tconstructor(readonly role: string) {\n\t\tsuper(\n\t\t\t`'${role}' is a reader role and no password was supplied for it. ` +\n\t\t\t\t`This module composes DDL and invents no secrets: pass ` +\n\t\t\t\t`passwords.reader alongside the role name.`,\n\t\t);\n\t\tthis.name = 'ReaderNeedsPassword';\n\t}\n}\n\n/** The read-only role's name for a given runtime role. */\nexport function readerRole(runtime: string): string {\n\treturn `${runtime}_reader`;\n}\n\n/** The owner role's name for a given runtime role. */\nexport function ownerRole(runtime: string): string {\n\treturn `${runtime}_owner`;\n}\n\nfunction roleExists(role: string): { sql: string; values: unknown[] } {\n\treturn { sql: 'SELECT 1 FROM pg_roles WHERE rolname = $1', values: [role] };\n}\n\n/**\n * Quote an identifier for use in DDL.\n *\n * Names reaching here are already canonical, so this is defence rather than\n * sanitisation — but DDL cannot be parameterised, and an unquoted identifier is\n * the one place a name could ever be more than a name.\n */\nfunction ident(name: string): string {\n\treturn `\"${name.replace(/\"/g, '\"\"')}\"`;\n}\n\n/**\n * Quote a string literal.\n *\n * A password cannot be a bind parameter in `CREATE ROLE`, which is the one\n * place in this module where a value rather than a name is interpolated — so it\n * is escaped rather than trusted, whatever the caller believes it derived.\n */\nfunction literal(value: string): string {\n\treturn `'${value.replace(/'/g, \"''\")}'`;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"roles.cjs","names":[],"sources":["../../src/pg/roles.ts"],"sourcesContent":["/**\n * The role DDL — an owner, a runtime role, and the grants between them.\n *\n * Generation only. The same statements have to run in-process from `gkm dev`\n * and from a provisioner inside a VPC at deploy, and one implementation is what\n * stops \"works locally, fails deployed\" — so this composes SQL and applies\n * none of it.\n *\n * **What the split is for.** A handler connects as the runtime role, which can\n * read and write rows and cannot create, alter, or drop anything. A migrator\n * connects as the owner, which can. That is a security property rather than a\n * convention — a compromised handler cannot `DROP TABLE` because its role holds\n * no such grant — and it is only a property at all once these statements have\n * actually run. Until then a schema is a namespace, not a boundary.\n *\n * **Passwords are an input.** This module invents no secrets: a caller derives\n * or fetches them and passes them in, because where a password comes from is\n * the one part that legitimately differs between a laptop and a deploy.\n */\n\n/** The roles and schema one tenant needs. */\nexport interface RoleSpec {\n\t/** The role a handler connects as. Reads and writes rows; owns nothing. */\n\truntime: string;\n\t/** The role a migrator connects as. Owns the schema and everything in it. */\n\towner: string;\n\t/**\n\t * A third role that may only read, for a reader endpoint.\n\t *\n\t * Optional because it is only worth creating where something points at it.\n\t * Read-only is enforced *here*, by the grants, rather than by which endpoint\n\t * a URL happens to name — which is what makes falling back to the writer's\n\t * endpoint safe when a cluster has no replica.\n\t */\n\treader?: string;\n\t/** The schema they operate in, pinned on every role's `search_path`. */\n\tschema: string;\n\t/** Supplied, never generated here. */\n\tpasswords: { runtime: string; owner: string; reader?: string };\n}\n\n/** One statement, and how to tell whether it still needs running. */\nexport interface RoleStatement {\n\t/** What it does, for the line a reconciler prints. */\n\tdescribe: string;\n\t/**\n\t * Answers \"does this already exist\", parameterised.\n\t *\n\t * Absent means the statement is idempotent in itself and is run every time —\n\t * a `GRANT` re-granting what is already granted is a no-op, and checking\n\t * would cost a round trip to learn nothing.\n\t */\n\texists?: { sql: string; values: unknown[] };\n\tsql: string;\n}\n\n/**\n * Everything one tenant's roles need, in dependency order.\n *\n * The owner before the schema it owns, the schema before the grants on it. A\n * caller applies them in sequence and may stop at the first failure without\n * leaving a half-granted role that looks complete.\n */\nexport function roleStatements(spec: RoleSpec): RoleStatement[] {\n\tconst { runtime, owner, schema, passwords } = spec;\n\n\treturn [\n\t\t{\n\t\t\tdescribe: `role ${owner}`,\n\t\t\texists: roleExists(owner),\n\t\t\tsql: `CREATE ROLE ${ident(owner)} LOGIN PASSWORD ${literal(passwords.owner)}`,\n\t\t},\n\t\t{\n\t\t\tdescribe: `role ${runtime}`,\n\t\t\texists: roleExists(runtime),\n\t\t\tsql: `CREATE ROLE ${ident(runtime)} LOGIN PASSWORD ${literal(passwords.runtime)}`,\n\t\t},\n\t\t{\n\t\t\t// Postgres will not let you create an object owned by a role you are\n\t\t\t// not a member of, and an RDS master is not a superuser — so\n\t\t\t// `CREATE SCHEMA … AUTHORIZATION` fails with \"must be able to SET\n\t\t\t// ROLE\" unless the connected role is granted membership first.\n\t\t\t//\n\t\t\t// Granting is idempotent and needs no check. It also leaves the master\n\t\t\t// able to administer the schema, which is what you want from the one\n\t\t\t// credential that exists before any role does.\n\t\t\tdescribe: `${owner} granted to the connecting role`,\n\t\t\tsql: `GRANT ${ident(owner)} TO CURRENT_USER`,\n\t\t},\n\t\t{\n\t\t\tdescribe: `schema ${schema}`,\n\t\t\texists: {\n\t\t\t\tsql: 'SELECT 1 FROM information_schema.schemata WHERE schema_name = $1',\n\t\t\t\tvalues: [schema],\n\t\t\t},\n\t\t\t// Owned by the owner from the moment it exists. Creating it as the\n\t\t\t// cluster master and granting afterwards leaves a window where the\n\t\t\t// master owns objects the owner is supposed to.\n\t\t\tsql: `CREATE SCHEMA ${ident(schema)} AUTHORIZATION ${ident(owner)}`,\n\t\t},\n\t\t{\n\t\t\t// And the schema that was already there, which the guard above skips.\n\t\t\t// A schema created before its roles existed is owned by whoever\n\t\t\t// created it — the cluster master — and nothing below fixes that, so\n\t\t\t// the owner cannot create in the schema it is supposed to own. The\n\t\t\t// failure is `no schema has been selected to create in`, which names\n\t\t\t// neither the schema nor the owner and reads like a missing\n\t\t\t// `search_path`.\n\t\t\t//\n\t\t\t// Idempotent when the owner is already right, so it costs a statement\n\t\t\t// rather than a check.\n\t\t\tdescribe: `${schema} is owned by ${owner}`,\n\t\t\tsql: `ALTER SCHEMA ${ident(schema)} OWNER TO ${ident(owner)}`,\n\t\t},\n\t\t{\n\t\t\tdescribe: `${runtime} may use ${schema}`,\n\t\t\tsql: `GRANT USAGE ON SCHEMA ${ident(schema)} TO ${ident(runtime)}`,\n\t\t},\n\t\t{\n\t\t\t// The important half: *future* tables. A grant on what exists today\n\t\t\t// covers nothing the next migration creates, which is how a runtime\n\t\t\t// role silently loses access one deploy later.\n\t\t\tdescribe: `${runtime} may read and write future tables in ${schema}`,\n\t\t\tsql:\n\t\t\t\t`ALTER DEFAULT PRIVILEGES FOR ROLE ${ident(owner)} IN SCHEMA ${ident(schema)} ` +\n\t\t\t\t`GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO ${ident(runtime)}`,\n\t\t},\n\t\t{\n\t\t\tdescribe: `${runtime} may use future sequences in ${schema}`,\n\t\t\tsql:\n\t\t\t\t`ALTER DEFAULT PRIVILEGES FOR ROLE ${ident(owner)} IN SCHEMA ${ident(schema)} ` +\n\t\t\t\t`GRANT USAGE, SELECT ON SEQUENCES TO ${ident(runtime)}`,\n\t\t},\n\t\t{\n\t\t\t// And the tables that are already there, for a schema that existed\n\t\t\t// before its roles did.\n\t\t\tdescribe: `${runtime} may read and write existing tables in ${schema}`,\n\t\t\tsql:\n\t\t\t\t`GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA ${ident(schema)} ` +\n\t\t\t\t`TO ${ident(runtime)}`,\n\t\t},\n\t\t{\n\t\t\tdescribe: `${runtime} may use existing sequences in ${schema}`,\n\t\t\tsql: `GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA ${ident(schema)} TO ${ident(runtime)}`,\n\t\t},\n\t\t{\n\t\t\t// Pinned on the role rather than carried in every URL. A connection\n\t\t\t// string that has to remember `search_path` is one that eventually\n\t\t\t// forgets, and the forgetting looks like an empty database.\n\t\t\tdescribe: `${runtime} resolves names in ${schema}`,\n\t\t\tsql: `ALTER ROLE ${ident(runtime)} SET search_path TO ${ident(schema)}`,\n\t\t},\n\t\t{\n\t\t\tdescribe: `${owner} resolves names in ${schema}`,\n\t\t\tsql: `ALTER ROLE ${ident(owner)} SET search_path TO ${ident(schema)}`,\n\t\t},\n\t\t...(spec.reader ? readerStatements(spec, spec.reader) : []),\n\t];\n}\n\n/**\n * A role that may read and nothing else.\n *\n * `SELECT` and no more, on what exists and on what the owner creates later. No\n * `INSERT`, no `UPDATE`, no `DELETE`, and no sequence `USAGE` — a reader that\n * could take a sequence value could not write the row it was for, so granting\n * it would be surface with no use.\n */\nfunction readerStatements(spec: RoleSpec, reader: string): RoleStatement[] {\n\tconst { owner, schema, passwords } = spec;\n\tconst password = passwords.reader;\n\n\tif (!password) throw new ReaderNeedsPassword(reader);\n\n\treturn [\n\t\t{\n\t\t\tdescribe: `role ${reader}`,\n\t\t\texists: roleExists(reader),\n\t\t\tsql: `CREATE ROLE ${ident(reader)} LOGIN PASSWORD ${literal(password)}`,\n\t\t},\n\t\t{\n\t\t\tdescribe: `${reader} may use ${schema}`,\n\t\t\tsql: `GRANT USAGE ON SCHEMA ${ident(schema)} TO ${ident(reader)}`,\n\t\t},\n\t\t{\n\t\t\tdescribe: `${reader} may read future tables in ${schema}`,\n\t\t\tsql:\n\t\t\t\t`ALTER DEFAULT PRIVILEGES FOR ROLE ${ident(owner)} IN SCHEMA ${ident(schema)} ` +\n\t\t\t\t`GRANT SELECT ON TABLES TO ${ident(reader)}`,\n\t\t},\n\t\t{\n\t\t\tdescribe: `${reader} may read existing tables in ${schema}`,\n\t\t\tsql: `GRANT SELECT ON ALL TABLES IN SCHEMA ${ident(schema)} TO ${ident(reader)}`,\n\t\t},\n\t\t{\n\t\t\tdescribe: `${reader} resolves names in ${schema}`,\n\t\t\tsql: `ALTER ROLE ${ident(reader)} SET search_path TO ${ident(schema)}`,\n\t\t},\n\t];\n}\n\n/** A reader role was named and no password was supplied for it. */\nexport class ReaderNeedsPassword extends Error {\n\tconstructor(readonly role: string) {\n\t\tsuper(\n\t\t\t`'${role}' is a reader role and no password was supplied for it. ` +\n\t\t\t\t`This module composes DDL and invents no secrets: pass ` +\n\t\t\t\t`passwords.reader alongside the role name.`,\n\t\t);\n\t\tthis.name = 'ReaderNeedsPassword';\n\t}\n}\n\n/** The read-only role's name for a given runtime role. */\nexport function readerRole(runtime: string): string {\n\treturn `${runtime}_reader`;\n}\n\n/** The owner role's name for a given runtime role. */\nexport function ownerRole(runtime: string): string {\n\treturn `${runtime}_owner`;\n}\n\nfunction roleExists(role: string): { sql: string; values: unknown[] } {\n\treturn { sql: 'SELECT 1 FROM pg_roles WHERE rolname = $1', values: [role] };\n}\n\n/**\n * Quote an identifier for use in DDL.\n *\n * Names reaching here are already canonical, so this is defence rather than\n * sanitisation — but DDL cannot be parameterised, and an unquoted identifier is\n * the one place a name could ever be more than a name.\n */\nfunction ident(name: string): string {\n\treturn `\"${name.replace(/\"/g, '\"\"')}\"`;\n}\n\n/**\n * Quote a string literal.\n *\n * A password cannot be a bind parameter in `CREATE ROLE`, which is the one\n * place in this module where a value rather than a name is interpolated — so it\n * is escaped rather than trusted, whatever the caller believes it derived.\n */\nfunction literal(value: string): string {\n\treturn `'${value.replace(/'/g, \"''\")}'`;\n}\n"],"mappings":"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"}
@@ -19,7 +19,7 @@
19
19
  * the one part that legitimately differs between a laptop and a deploy.
20
20
  */
21
21
  /** The roles and schema one tenant needs. */
22
- interface RoleSpec {
22
+ export interface RoleSpec {
23
23
  /** The role a handler connects as. Reads and writes rows; owns nothing. */
24
24
  runtime: string;
25
25
  /** The role a migrator connects as. Owns the schema and everything in it. */
@@ -43,7 +43,7 @@ interface RoleSpec {
43
43
  };
44
44
  }
45
45
  /** One statement, and how to tell whether it still needs running. */
46
- interface RoleStatement {
46
+ export interface RoleStatement {
47
47
  /** What it does, for the line a reconciler prints. */
48
48
  describe: string;
49
49
  /**
@@ -66,17 +66,15 @@ interface RoleStatement {
66
66
  * caller applies them in sequence and may stop at the first failure without
67
67
  * leaving a half-granted role that looks complete.
68
68
  */
69
- declare function roleStatements(spec: RoleSpec): RoleStatement[];
69
+ export declare function roleStatements(spec: RoleSpec): RoleStatement[];
70
70
  /** A reader role was named and no password was supplied for it. */
71
- declare class ReaderNeedsPassword extends Error {
71
+ export declare class ReaderNeedsPassword extends Error {
72
72
  readonly role: string;
73
73
  constructor(role: string);
74
74
  }
75
75
  /** The read-only role's name for a given runtime role. */
76
- declare function readerRole(runtime: string): string;
76
+ export declare function readerRole(runtime: string): string;
77
77
  /** The owner role's name for a given runtime role. */
78
- declare function ownerRole(runtime: string): string;
79
- //# sourceMappingURL=roles.d.ts.map
78
+ export declare function ownerRole(runtime: string): string;
80
79
  //#endregion
81
- export { ReaderNeedsPassword, RoleSpec, RoleStatement, ownerRole, readerRole, roleStatements };
82
80
  //# sourceMappingURL=roles.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"roles.d.cts","names":[],"sources":["../../src/pg/roles.ts"],"sourcesContent":[],"mappings":";;AAqBA;AAqBA;AAqBA;;;;AAA6D;AA2I7D;AAYA;AAKA;;;;;;;;;;UAtMiB,QAAA;;;;;;;;;;;;;;;;;;;;;;;;UAqBA,aAAA;;;;;;;;;;;;;;;;;;;;;;;iBAqBD,cAAA,OAAqB,WAAW;;cA2InC,mBAAA,SAA4B,KAAK;;;;;iBAY9B,UAAA;;iBAKA,SAAA"}
1
+ {"version":3,"file":"roles.d.cts","names":[],"sources":["../../src/pg/roles.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;iBAqBiB;;EAEhB;;EAEA;;;;;;;;;EASA;;EAEA;;EAEA;IAAa;IAAiB;IAAe;;;;iBAI7B;;EAEhB;;;;;;;;EAQA;IAAW;IAAa;;EACxB;;;;;;;;;wBAUe,eAAe,MAAM,WAAW;;qBA2InC,4BAA4B;WACnB;EAArB,YAAqB;;;wBAWN,WAAW;;wBAKX,UAAU"}
@@ -19,7 +19,7 @@
19
19
  * the one part that legitimately differs between a laptop and a deploy.
20
20
  */
21
21
  /** The roles and schema one tenant needs. */
22
- interface RoleSpec {
22
+ export interface RoleSpec {
23
23
  /** The role a handler connects as. Reads and writes rows; owns nothing. */
24
24
  runtime: string;
25
25
  /** The role a migrator connects as. Owns the schema and everything in it. */
@@ -43,7 +43,7 @@ interface RoleSpec {
43
43
  };
44
44
  }
45
45
  /** One statement, and how to tell whether it still needs running. */
46
- interface RoleStatement {
46
+ export interface RoleStatement {
47
47
  /** What it does, for the line a reconciler prints. */
48
48
  describe: string;
49
49
  /**
@@ -66,17 +66,15 @@ interface RoleStatement {
66
66
  * caller applies them in sequence and may stop at the first failure without
67
67
  * leaving a half-granted role that looks complete.
68
68
  */
69
- declare function roleStatements(spec: RoleSpec): RoleStatement[];
69
+ export declare function roleStatements(spec: RoleSpec): RoleStatement[];
70
70
  /** A reader role was named and no password was supplied for it. */
71
- declare class ReaderNeedsPassword extends Error {
71
+ export declare class ReaderNeedsPassword extends Error {
72
72
  readonly role: string;
73
73
  constructor(role: string);
74
74
  }
75
75
  /** The read-only role's name for a given runtime role. */
76
- declare function readerRole(runtime: string): string;
76
+ export declare function readerRole(runtime: string): string;
77
77
  /** The owner role's name for a given runtime role. */
78
- declare function ownerRole(runtime: string): string;
79
- //# sourceMappingURL=roles.d.ts.map
78
+ export declare function ownerRole(runtime: string): string;
80
79
  //#endregion
81
- export { ReaderNeedsPassword, RoleSpec, RoleStatement, ownerRole, readerRole, roleStatements };
82
80
  //# sourceMappingURL=roles.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"roles.d.mts","names":[],"sources":["../../src/pg/roles.ts"],"sourcesContent":[],"mappings":";;AAqBA;AAqBA;AAqBA;;;;AAA6D;AA2I7D;AAYA;AAKA;;;;;;;;;;UAtMiB,QAAA;;;;;;;;;;;;;;;;;;;;;;;;UAqBA,aAAA;;;;;;;;;;;;;;;;;;;;;;;iBAqBD,cAAA,OAAqB,WAAW;;cA2InC,mBAAA,SAA4B,KAAK;;;;;iBAY9B,UAAA;;iBAKA,SAAA"}
1
+ {"version":3,"file":"roles.d.mts","names":[],"sources":["../../src/pg/roles.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;iBAqBiB;;EAEhB;;EAEA;;;;;;;;;EASA;;EAEA;;EAEA;IAAa;IAAiB;IAAe;;;;iBAI7B;;EAEhB;;;;;;;;EAQA;IAAW;IAAa;;EACxB;;;;;;;;;wBAUe,eAAe,MAAM,WAAW;;qBA2InC,4BAA4B;WACnB;EAArB,YAAqB;;;wBAWN,WAAW;;wBAKX,UAAU"}
package/dist/pg/roles.mjs CHANGED
@@ -104,6 +104,7 @@ function readerStatements(spec, reader) {
104
104
  }
105
105
  /** A reader role was named and no password was supplied for it. */
106
106
  var ReaderNeedsPassword = class extends Error {
107
+ role;
107
108
  constructor(role) {
108
109
  super(`'${role}' is a reader role and no password was supplied for it. This module composes DDL and invents no secrets: pass passwords.reader alongside the role name.`);
109
110
  this.role = role;
@@ -144,7 +145,7 @@ function ident(name) {
144
145
  function literal(value) {
145
146
  return `'${value.replace(/'/g, "''")}'`;
146
147
  }
147
-
148
148
  //#endregion
149
149
  export { ReaderNeedsPassword, ownerRole, readerRole, roleStatements };
150
+
150
151
  //# sourceMappingURL=roles.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"roles.mjs","names":["spec: RoleSpec","reader: string","role: string","runtime: string","name: string","value: string"],"sources":["../../src/pg/roles.ts"],"sourcesContent":["/**\n * The role DDL — an owner, a runtime role, and the grants between them.\n *\n * Generation only. The same statements have to run in-process from `gkm dev`\n * and from a provisioner inside a VPC at deploy, and one implementation is what\n * stops \"works locally, fails deployed\" — so this composes SQL and applies\n * none of it.\n *\n * **What the split is for.** A handler connects as the runtime role, which can\n * read and write rows and cannot create, alter, or drop anything. A migrator\n * connects as the owner, which can. That is a security property rather than a\n * convention — a compromised handler cannot `DROP TABLE` because its role holds\n * no such grant — and it is only a property at all once these statements have\n * actually run. Until then a schema is a namespace, not a boundary.\n *\n * **Passwords are an input.** This module invents no secrets: a caller derives\n * or fetches them and passes them in, because where a password comes from is\n * the one part that legitimately differs between a laptop and a deploy.\n */\n\n/** The roles and schema one tenant needs. */\nexport interface RoleSpec {\n\t/** The role a handler connects as. Reads and writes rows; owns nothing. */\n\truntime: string;\n\t/** The role a migrator connects as. Owns the schema and everything in it. */\n\towner: string;\n\t/**\n\t * A third role that may only read, for a reader endpoint.\n\t *\n\t * Optional because it is only worth creating where something points at it.\n\t * Read-only is enforced *here*, by the grants, rather than by which endpoint\n\t * a URL happens to name — which is what makes falling back to the writer's\n\t * endpoint safe when a cluster has no replica.\n\t */\n\treader?: string;\n\t/** The schema they operate in, pinned on every role's `search_path`. */\n\tschema: string;\n\t/** Supplied, never generated here. */\n\tpasswords: { runtime: string; owner: string; reader?: string };\n}\n\n/** One statement, and how to tell whether it still needs running. */\nexport interface RoleStatement {\n\t/** What it does, for the line a reconciler prints. */\n\tdescribe: string;\n\t/**\n\t * Answers \"does this already exist\", parameterised.\n\t *\n\t * Absent means the statement is idempotent in itself and is run every time —\n\t * a `GRANT` re-granting what is already granted is a no-op, and checking\n\t * would cost a round trip to learn nothing.\n\t */\n\texists?: { sql: string; values: unknown[] };\n\tsql: string;\n}\n\n/**\n * Everything one tenant's roles need, in dependency order.\n *\n * The owner before the schema it owns, the schema before the grants on it. A\n * caller applies them in sequence and may stop at the first failure without\n * leaving a half-granted role that looks complete.\n */\nexport function roleStatements(spec: RoleSpec): RoleStatement[] {\n\tconst { runtime, owner, schema, passwords } = spec;\n\n\treturn [\n\t\t{\n\t\t\tdescribe: `role ${owner}`,\n\t\t\texists: roleExists(owner),\n\t\t\tsql: `CREATE ROLE ${ident(owner)} LOGIN PASSWORD ${literal(passwords.owner)}`,\n\t\t},\n\t\t{\n\t\t\tdescribe: `role ${runtime}`,\n\t\t\texists: roleExists(runtime),\n\t\t\tsql: `CREATE ROLE ${ident(runtime)} LOGIN PASSWORD ${literal(passwords.runtime)}`,\n\t\t},\n\t\t{\n\t\t\t// Postgres will not let you create an object owned by a role you are\n\t\t\t// not a member of, and an RDS master is not a superuser — so\n\t\t\t// `CREATE SCHEMA … AUTHORIZATION` fails with \"must be able to SET\n\t\t\t// ROLE\" unless the connected role is granted membership first.\n\t\t\t//\n\t\t\t// Granting is idempotent and needs no check. It also leaves the master\n\t\t\t// able to administer the schema, which is what you want from the one\n\t\t\t// credential that exists before any role does.\n\t\t\tdescribe: `${owner} granted to the connecting role`,\n\t\t\tsql: `GRANT ${ident(owner)} TO CURRENT_USER`,\n\t\t},\n\t\t{\n\t\t\tdescribe: `schema ${schema}`,\n\t\t\texists: {\n\t\t\t\tsql: 'SELECT 1 FROM information_schema.schemata WHERE schema_name = $1',\n\t\t\t\tvalues: [schema],\n\t\t\t},\n\t\t\t// Owned by the owner from the moment it exists. Creating it as the\n\t\t\t// cluster master and granting afterwards leaves a window where the\n\t\t\t// master owns objects the owner is supposed to.\n\t\t\tsql: `CREATE SCHEMA ${ident(schema)} AUTHORIZATION ${ident(owner)}`,\n\t\t},\n\t\t{\n\t\t\t// And the schema that was already there, which the guard above skips.\n\t\t\t// A schema created before its roles existed is owned by whoever\n\t\t\t// created it — the cluster master — and nothing below fixes that, so\n\t\t\t// the owner cannot create in the schema it is supposed to own. The\n\t\t\t// failure is `no schema has been selected to create in`, which names\n\t\t\t// neither the schema nor the owner and reads like a missing\n\t\t\t// `search_path`.\n\t\t\t//\n\t\t\t// Idempotent when the owner is already right, so it costs a statement\n\t\t\t// rather than a check.\n\t\t\tdescribe: `${schema} is owned by ${owner}`,\n\t\t\tsql: `ALTER SCHEMA ${ident(schema)} OWNER TO ${ident(owner)}`,\n\t\t},\n\t\t{\n\t\t\tdescribe: `${runtime} may use ${schema}`,\n\t\t\tsql: `GRANT USAGE ON SCHEMA ${ident(schema)} TO ${ident(runtime)}`,\n\t\t},\n\t\t{\n\t\t\t// The important half: *future* tables. A grant on what exists today\n\t\t\t// covers nothing the next migration creates, which is how a runtime\n\t\t\t// role silently loses access one deploy later.\n\t\t\tdescribe: `${runtime} may read and write future tables in ${schema}`,\n\t\t\tsql:\n\t\t\t\t`ALTER DEFAULT PRIVILEGES FOR ROLE ${ident(owner)} IN SCHEMA ${ident(schema)} ` +\n\t\t\t\t`GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO ${ident(runtime)}`,\n\t\t},\n\t\t{\n\t\t\tdescribe: `${runtime} may use future sequences in ${schema}`,\n\t\t\tsql:\n\t\t\t\t`ALTER DEFAULT PRIVILEGES FOR ROLE ${ident(owner)} IN SCHEMA ${ident(schema)} ` +\n\t\t\t\t`GRANT USAGE, SELECT ON SEQUENCES TO ${ident(runtime)}`,\n\t\t},\n\t\t{\n\t\t\t// And the tables that are already there, for a schema that existed\n\t\t\t// before its roles did.\n\t\t\tdescribe: `${runtime} may read and write existing tables in ${schema}`,\n\t\t\tsql:\n\t\t\t\t`GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA ${ident(schema)} ` +\n\t\t\t\t`TO ${ident(runtime)}`,\n\t\t},\n\t\t{\n\t\t\tdescribe: `${runtime} may use existing sequences in ${schema}`,\n\t\t\tsql: `GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA ${ident(schema)} TO ${ident(runtime)}`,\n\t\t},\n\t\t{\n\t\t\t// Pinned on the role rather than carried in every URL. A connection\n\t\t\t// string that has to remember `search_path` is one that eventually\n\t\t\t// forgets, and the forgetting looks like an empty database.\n\t\t\tdescribe: `${runtime} resolves names in ${schema}`,\n\t\t\tsql: `ALTER ROLE ${ident(runtime)} SET search_path TO ${ident(schema)}`,\n\t\t},\n\t\t{\n\t\t\tdescribe: `${owner} resolves names in ${schema}`,\n\t\t\tsql: `ALTER ROLE ${ident(owner)} SET search_path TO ${ident(schema)}`,\n\t\t},\n\t\t...(spec.reader ? readerStatements(spec, spec.reader) : []),\n\t];\n}\n\n/**\n * A role that may read and nothing else.\n *\n * `SELECT` and no more, on what exists and on what the owner creates later. No\n * `INSERT`, no `UPDATE`, no `DELETE`, and no sequence `USAGE` — a reader that\n * could take a sequence value could not write the row it was for, so granting\n * it would be surface with no use.\n */\nfunction readerStatements(spec: RoleSpec, reader: string): RoleStatement[] {\n\tconst { owner, schema, passwords } = spec;\n\tconst password = passwords.reader;\n\n\tif (!password) throw new ReaderNeedsPassword(reader);\n\n\treturn [\n\t\t{\n\t\t\tdescribe: `role ${reader}`,\n\t\t\texists: roleExists(reader),\n\t\t\tsql: `CREATE ROLE ${ident(reader)} LOGIN PASSWORD ${literal(password)}`,\n\t\t},\n\t\t{\n\t\t\tdescribe: `${reader} may use ${schema}`,\n\t\t\tsql: `GRANT USAGE ON SCHEMA ${ident(schema)} TO ${ident(reader)}`,\n\t\t},\n\t\t{\n\t\t\tdescribe: `${reader} may read future tables in ${schema}`,\n\t\t\tsql:\n\t\t\t\t`ALTER DEFAULT PRIVILEGES FOR ROLE ${ident(owner)} IN SCHEMA ${ident(schema)} ` +\n\t\t\t\t`GRANT SELECT ON TABLES TO ${ident(reader)}`,\n\t\t},\n\t\t{\n\t\t\tdescribe: `${reader} may read existing tables in ${schema}`,\n\t\t\tsql: `GRANT SELECT ON ALL TABLES IN SCHEMA ${ident(schema)} TO ${ident(reader)}`,\n\t\t},\n\t\t{\n\t\t\tdescribe: `${reader} resolves names in ${schema}`,\n\t\t\tsql: `ALTER ROLE ${ident(reader)} SET search_path TO ${ident(schema)}`,\n\t\t},\n\t];\n}\n\n/** A reader role was named and no password was supplied for it. */\nexport class ReaderNeedsPassword extends Error {\n\tconstructor(readonly role: string) {\n\t\tsuper(\n\t\t\t`'${role}' is a reader role and no password was supplied for it. ` +\n\t\t\t\t`This module composes DDL and invents no secrets: pass ` +\n\t\t\t\t`passwords.reader alongside the role name.`,\n\t\t);\n\t\tthis.name = 'ReaderNeedsPassword';\n\t}\n}\n\n/** The read-only role's name for a given runtime role. */\nexport function readerRole(runtime: string): string {\n\treturn `${runtime}_reader`;\n}\n\n/** The owner role's name for a given runtime role. */\nexport function ownerRole(runtime: string): string {\n\treturn `${runtime}_owner`;\n}\n\nfunction roleExists(role: string): { sql: string; values: unknown[] } {\n\treturn { sql: 'SELECT 1 FROM pg_roles WHERE rolname = $1', values: [role] };\n}\n\n/**\n * Quote an identifier for use in DDL.\n *\n * Names reaching here are already canonical, so this is defence rather than\n * sanitisation — but DDL cannot be parameterised, and an unquoted identifier is\n * the one place a name could ever be more than a name.\n */\nfunction ident(name: string): string {\n\treturn `\"${name.replace(/\"/g, '\"\"')}\"`;\n}\n\n/**\n * Quote a string literal.\n *\n * A password cannot be a bind parameter in `CREATE ROLE`, which is the one\n * place in this module where a value rather than a name is interpolated — so it\n * is escaped rather than trusted, whatever the caller believes it derived.\n */\nfunction literal(value: string): string {\n\treturn `'${value.replace(/'/g, \"''\")}'`;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"roles.mjs","names":[],"sources":["../../src/pg/roles.ts"],"sourcesContent":["/**\n * The role DDL — an owner, a runtime role, and the grants between them.\n *\n * Generation only. The same statements have to run in-process from `gkm dev`\n * and from a provisioner inside a VPC at deploy, and one implementation is what\n * stops \"works locally, fails deployed\" — so this composes SQL and applies\n * none of it.\n *\n * **What the split is for.** A handler connects as the runtime role, which can\n * read and write rows and cannot create, alter, or drop anything. A migrator\n * connects as the owner, which can. That is a security property rather than a\n * convention — a compromised handler cannot `DROP TABLE` because its role holds\n * no such grant — and it is only a property at all once these statements have\n * actually run. Until then a schema is a namespace, not a boundary.\n *\n * **Passwords are an input.** This module invents no secrets: a caller derives\n * or fetches them and passes them in, because where a password comes from is\n * the one part that legitimately differs between a laptop and a deploy.\n */\n\n/** The roles and schema one tenant needs. */\nexport interface RoleSpec {\n\t/** The role a handler connects as. Reads and writes rows; owns nothing. */\n\truntime: string;\n\t/** The role a migrator connects as. Owns the schema and everything in it. */\n\towner: string;\n\t/**\n\t * A third role that may only read, for a reader endpoint.\n\t *\n\t * Optional because it is only worth creating where something points at it.\n\t * Read-only is enforced *here*, by the grants, rather than by which endpoint\n\t * a URL happens to name — which is what makes falling back to the writer's\n\t * endpoint safe when a cluster has no replica.\n\t */\n\treader?: string;\n\t/** The schema they operate in, pinned on every role's `search_path`. */\n\tschema: string;\n\t/** Supplied, never generated here. */\n\tpasswords: { runtime: string; owner: string; reader?: string };\n}\n\n/** One statement, and how to tell whether it still needs running. */\nexport interface RoleStatement {\n\t/** What it does, for the line a reconciler prints. */\n\tdescribe: string;\n\t/**\n\t * Answers \"does this already exist\", parameterised.\n\t *\n\t * Absent means the statement is idempotent in itself and is run every time —\n\t * a `GRANT` re-granting what is already granted is a no-op, and checking\n\t * would cost a round trip to learn nothing.\n\t */\n\texists?: { sql: string; values: unknown[] };\n\tsql: string;\n}\n\n/**\n * Everything one tenant's roles need, in dependency order.\n *\n * The owner before the schema it owns, the schema before the grants on it. A\n * caller applies them in sequence and may stop at the first failure without\n * leaving a half-granted role that looks complete.\n */\nexport function roleStatements(spec: RoleSpec): RoleStatement[] {\n\tconst { runtime, owner, schema, passwords } = spec;\n\n\treturn [\n\t\t{\n\t\t\tdescribe: `role ${owner}`,\n\t\t\texists: roleExists(owner),\n\t\t\tsql: `CREATE ROLE ${ident(owner)} LOGIN PASSWORD ${literal(passwords.owner)}`,\n\t\t},\n\t\t{\n\t\t\tdescribe: `role ${runtime}`,\n\t\t\texists: roleExists(runtime),\n\t\t\tsql: `CREATE ROLE ${ident(runtime)} LOGIN PASSWORD ${literal(passwords.runtime)}`,\n\t\t},\n\t\t{\n\t\t\t// Postgres will not let you create an object owned by a role you are\n\t\t\t// not a member of, and an RDS master is not a superuser — so\n\t\t\t// `CREATE SCHEMA … AUTHORIZATION` fails with \"must be able to SET\n\t\t\t// ROLE\" unless the connected role is granted membership first.\n\t\t\t//\n\t\t\t// Granting is idempotent and needs no check. It also leaves the master\n\t\t\t// able to administer the schema, which is what you want from the one\n\t\t\t// credential that exists before any role does.\n\t\t\tdescribe: `${owner} granted to the connecting role`,\n\t\t\tsql: `GRANT ${ident(owner)} TO CURRENT_USER`,\n\t\t},\n\t\t{\n\t\t\tdescribe: `schema ${schema}`,\n\t\t\texists: {\n\t\t\t\tsql: 'SELECT 1 FROM information_schema.schemata WHERE schema_name = $1',\n\t\t\t\tvalues: [schema],\n\t\t\t},\n\t\t\t// Owned by the owner from the moment it exists. Creating it as the\n\t\t\t// cluster master and granting afterwards leaves a window where the\n\t\t\t// master owns objects the owner is supposed to.\n\t\t\tsql: `CREATE SCHEMA ${ident(schema)} AUTHORIZATION ${ident(owner)}`,\n\t\t},\n\t\t{\n\t\t\t// And the schema that was already there, which the guard above skips.\n\t\t\t// A schema created before its roles existed is owned by whoever\n\t\t\t// created it — the cluster master — and nothing below fixes that, so\n\t\t\t// the owner cannot create in the schema it is supposed to own. The\n\t\t\t// failure is `no schema has been selected to create in`, which names\n\t\t\t// neither the schema nor the owner and reads like a missing\n\t\t\t// `search_path`.\n\t\t\t//\n\t\t\t// Idempotent when the owner is already right, so it costs a statement\n\t\t\t// rather than a check.\n\t\t\tdescribe: `${schema} is owned by ${owner}`,\n\t\t\tsql: `ALTER SCHEMA ${ident(schema)} OWNER TO ${ident(owner)}`,\n\t\t},\n\t\t{\n\t\t\tdescribe: `${runtime} may use ${schema}`,\n\t\t\tsql: `GRANT USAGE ON SCHEMA ${ident(schema)} TO ${ident(runtime)}`,\n\t\t},\n\t\t{\n\t\t\t// The important half: *future* tables. A grant on what exists today\n\t\t\t// covers nothing the next migration creates, which is how a runtime\n\t\t\t// role silently loses access one deploy later.\n\t\t\tdescribe: `${runtime} may read and write future tables in ${schema}`,\n\t\t\tsql:\n\t\t\t\t`ALTER DEFAULT PRIVILEGES FOR ROLE ${ident(owner)} IN SCHEMA ${ident(schema)} ` +\n\t\t\t\t`GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO ${ident(runtime)}`,\n\t\t},\n\t\t{\n\t\t\tdescribe: `${runtime} may use future sequences in ${schema}`,\n\t\t\tsql:\n\t\t\t\t`ALTER DEFAULT PRIVILEGES FOR ROLE ${ident(owner)} IN SCHEMA ${ident(schema)} ` +\n\t\t\t\t`GRANT USAGE, SELECT ON SEQUENCES TO ${ident(runtime)}`,\n\t\t},\n\t\t{\n\t\t\t// And the tables that are already there, for a schema that existed\n\t\t\t// before its roles did.\n\t\t\tdescribe: `${runtime} may read and write existing tables in ${schema}`,\n\t\t\tsql:\n\t\t\t\t`GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA ${ident(schema)} ` +\n\t\t\t\t`TO ${ident(runtime)}`,\n\t\t},\n\t\t{\n\t\t\tdescribe: `${runtime} may use existing sequences in ${schema}`,\n\t\t\tsql: `GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA ${ident(schema)} TO ${ident(runtime)}`,\n\t\t},\n\t\t{\n\t\t\t// Pinned on the role rather than carried in every URL. A connection\n\t\t\t// string that has to remember `search_path` is one that eventually\n\t\t\t// forgets, and the forgetting looks like an empty database.\n\t\t\tdescribe: `${runtime} resolves names in ${schema}`,\n\t\t\tsql: `ALTER ROLE ${ident(runtime)} SET search_path TO ${ident(schema)}`,\n\t\t},\n\t\t{\n\t\t\tdescribe: `${owner} resolves names in ${schema}`,\n\t\t\tsql: `ALTER ROLE ${ident(owner)} SET search_path TO ${ident(schema)}`,\n\t\t},\n\t\t...(spec.reader ? readerStatements(spec, spec.reader) : []),\n\t];\n}\n\n/**\n * A role that may read and nothing else.\n *\n * `SELECT` and no more, on what exists and on what the owner creates later. No\n * `INSERT`, no `UPDATE`, no `DELETE`, and no sequence `USAGE` — a reader that\n * could take a sequence value could not write the row it was for, so granting\n * it would be surface with no use.\n */\nfunction readerStatements(spec: RoleSpec, reader: string): RoleStatement[] {\n\tconst { owner, schema, passwords } = spec;\n\tconst password = passwords.reader;\n\n\tif (!password) throw new ReaderNeedsPassword(reader);\n\n\treturn [\n\t\t{\n\t\t\tdescribe: `role ${reader}`,\n\t\t\texists: roleExists(reader),\n\t\t\tsql: `CREATE ROLE ${ident(reader)} LOGIN PASSWORD ${literal(password)}`,\n\t\t},\n\t\t{\n\t\t\tdescribe: `${reader} may use ${schema}`,\n\t\t\tsql: `GRANT USAGE ON SCHEMA ${ident(schema)} TO ${ident(reader)}`,\n\t\t},\n\t\t{\n\t\t\tdescribe: `${reader} may read future tables in ${schema}`,\n\t\t\tsql:\n\t\t\t\t`ALTER DEFAULT PRIVILEGES FOR ROLE ${ident(owner)} IN SCHEMA ${ident(schema)} ` +\n\t\t\t\t`GRANT SELECT ON TABLES TO ${ident(reader)}`,\n\t\t},\n\t\t{\n\t\t\tdescribe: `${reader} may read existing tables in ${schema}`,\n\t\t\tsql: `GRANT SELECT ON ALL TABLES IN SCHEMA ${ident(schema)} TO ${ident(reader)}`,\n\t\t},\n\t\t{\n\t\t\tdescribe: `${reader} resolves names in ${schema}`,\n\t\t\tsql: `ALTER ROLE ${ident(reader)} SET search_path TO ${ident(schema)}`,\n\t\t},\n\t];\n}\n\n/** A reader role was named and no password was supplied for it. */\nexport class ReaderNeedsPassword extends Error {\n\tconstructor(readonly role: string) {\n\t\tsuper(\n\t\t\t`'${role}' is a reader role and no password was supplied for it. ` +\n\t\t\t\t`This module composes DDL and invents no secrets: pass ` +\n\t\t\t\t`passwords.reader alongside the role name.`,\n\t\t);\n\t\tthis.name = 'ReaderNeedsPassword';\n\t}\n}\n\n/** The read-only role's name for a given runtime role. */\nexport function readerRole(runtime: string): string {\n\treturn `${runtime}_reader`;\n}\n\n/** The owner role's name for a given runtime role. */\nexport function ownerRole(runtime: string): string {\n\treturn `${runtime}_owner`;\n}\n\nfunction roleExists(role: string): { sql: string; values: unknown[] } {\n\treturn { sql: 'SELECT 1 FROM pg_roles WHERE rolname = $1', values: [role] };\n}\n\n/**\n * Quote an identifier for use in DDL.\n *\n * Names reaching here are already canonical, so this is defence rather than\n * sanitisation — but DDL cannot be parameterised, and an unquoted identifier is\n * the one place a name could ever be more than a name.\n */\nfunction ident(name: string): string {\n\treturn `\"${name.replace(/\"/g, '\"\"')}\"`;\n}\n\n/**\n * Quote a string literal.\n *\n * A password cannot be a bind parameter in `CREATE ROLE`, which is the one\n * place in this module where a value rather than a name is interpolated — so it\n * is escaped rather than trusted, whatever the caller believes it derived.\n */\nfunction literal(value: string): string {\n\treturn `'${value.replace(/'/g, \"''\")}'`;\n}\n"],"mappings":"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"}
package/dist/pg/url.cjs CHANGED
@@ -1,6 +1,21 @@
1
- const require_errors = require('../errors-Cs5jlAv-.cjs');
2
-
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_errors = require("../errors.cjs");
3
3
  //#region src/pg/url.ts
4
+ /**
5
+ * Composing a `postgres://` connection string.
6
+ *
7
+ * One direction only. Parsing exists in `pg` already — every consumer hands the
8
+ * string straight to `new Pool({ connectionString })` — so a parser here would
9
+ * be surface whose only caller is its own round-trip test.
10
+ *
11
+ * What is *not* already solved is composition: a provisioned instance exposes
12
+ * host, port, database, username, and password as five separate values, and the
13
+ * construct reads one URL. Doing that join by hand is where `search_path`
14
+ * quietly goes missing.
15
+ *
16
+ * A URL built here is a secret — it carries the role's password. See
17
+ * {@link redactDatabaseUrl} before writing one anywhere.
18
+ */
4
19
  const SCHEME = "postgres:";
5
20
  const DEFAULT_PORT = 5432;
6
21
  /**
@@ -27,7 +42,7 @@ function build(address) {
27
42
  if (ssl === false) url.searchParams.set("sslmode", "disable");
28
43
  return url.toString();
29
44
  }
30
-
31
45
  //#endregion
32
46
  exports.build = build;
47
+
33
48
  //# sourceMappingURL=url.cjs.map