@fuzdev/fuz_app 0.110.1 → 0.111.0

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 (81) hide show
  1. package/dist/actions/action_rpc.d.ts +2 -2
  2. package/dist/auth/CLAUDE.md +24 -5
  3. package/dist/auth/account_action_specs.d.ts +0 -2
  4. package/dist/auth/account_action_specs.d.ts.map +1 -1
  5. package/dist/auth/account_queries.d.ts +22 -0
  6. package/dist/auth/account_queries.d.ts.map +1 -1
  7. package/dist/auth/account_queries.js +49 -15
  8. package/dist/auth/account_schema.d.ts +7 -9
  9. package/dist/auth/account_schema.d.ts.map +1 -1
  10. package/dist/auth/account_schema.js +1 -2
  11. package/dist/auth/admin_action_specs.d.ts +0 -2
  12. package/dist/auth/admin_action_specs.d.ts.map +1 -1
  13. package/dist/auth/api_token_queries.d.ts +9 -4
  14. package/dist/auth/api_token_queries.d.ts.map +1 -1
  15. package/dist/auth/api_token_queries.js +29 -7
  16. package/dist/auth/app_settings_queries.d.ts +8 -0
  17. package/dist/auth/app_settings_queries.d.ts.map +1 -1
  18. package/dist/auth/app_settings_queries.js +14 -3
  19. package/dist/auth/audit_log_queries.d.ts +15 -3
  20. package/dist/auth/audit_log_queries.d.ts.map +1 -1
  21. package/dist/auth/audit_log_queries.js +32 -7
  22. package/dist/auth/audit_log_schema.d.ts +0 -1
  23. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  24. package/dist/auth/auth_ddl.d.ts +6 -0
  25. package/dist/auth/auth_ddl.d.ts.map +1 -1
  26. package/dist/auth/auth_ddl.js +9 -0
  27. package/dist/auth/invite_queries.d.ts +10 -0
  28. package/dist/auth/invite_queries.d.ts.map +1 -1
  29. package/dist/auth/invite_queries.js +25 -6
  30. package/dist/auth/migrations.d.ts.map +1 -1
  31. package/dist/auth/migrations.js +10 -0
  32. package/dist/auth/role_grant_offer_ddl.d.ts +23 -2
  33. package/dist/auth/role_grant_offer_ddl.d.ts.map +1 -1
  34. package/dist/auth/role_grant_offer_ddl.js +43 -2
  35. package/dist/auth/role_grant_offer_queries.d.ts.map +1 -1
  36. package/dist/auth/role_grant_offer_queries.js +29 -48
  37. package/dist/auth/role_grant_queries.d.ts +18 -0
  38. package/dist/auth/role_grant_queries.d.ts.map +1 -1
  39. package/dist/auth/role_grant_queries.js +43 -17
  40. package/dist/auth/session_queries.d.ts +12 -3
  41. package/dist/auth/session_queries.d.ts.map +1 -1
  42. package/dist/auth/session_queries.js +21 -6
  43. package/dist/db/CLAUDE.md +15 -0
  44. package/dist/db/cell_audit_queries.d.ts.map +1 -1
  45. package/dist/db/cell_audit_queries.js +3 -1
  46. package/dist/db/cell_field_queries.d.ts +11 -2
  47. package/dist/db/cell_field_queries.d.ts.map +1 -1
  48. package/dist/db/cell_field_queries.js +23 -7
  49. package/dist/db/cell_grant_queries.d.ts +9 -1
  50. package/dist/db/cell_grant_queries.d.ts.map +1 -1
  51. package/dist/db/cell_grant_queries.js +25 -9
  52. package/dist/db/cell_item_queries.d.ts +12 -1
  53. package/dist/db/cell_item_queries.d.ts.map +1 -1
  54. package/dist/db/cell_item_queries.js +25 -7
  55. package/dist/db/cell_queries.d.ts +29 -0
  56. package/dist/db/cell_queries.d.ts.map +1 -1
  57. package/dist/db/cell_queries.js +53 -20
  58. package/dist/db/fact_queries.d.ts +7 -0
  59. package/dist/db/fact_queries.d.ts.map +1 -1
  60. package/dist/db/fact_queries.js +19 -2
  61. package/dist/db/schema_ready.d.ts +6 -1
  62. package/dist/db/schema_ready.d.ts.map +1 -1
  63. package/dist/db/schema_ready.js +6 -2
  64. package/dist/db/sql_columns.d.ts +67 -0
  65. package/dist/db/sql_columns.d.ts.map +1 -0
  66. package/dist/db/sql_columns.js +77 -0
  67. package/dist/db/sql_identifier.d.ts +6 -5
  68. package/dist/db/sql_identifier.d.ts.map +1 -1
  69. package/dist/db/sql_identifier.js +6 -5
  70. package/dist/http/CLAUDE.md +1 -1
  71. package/dist/http/route_spec.d.ts +2 -2
  72. package/dist/server/app_server.d.ts.map +1 -1
  73. package/dist/server/app_server.js +1 -2
  74. package/dist/testing/CLAUDE.md +2 -1
  75. package/dist/testing/cross_backend/expected_schema.json +1 -1
  76. package/dist/testing/db.d.ts +18 -0
  77. package/dist/testing/db.d.ts.map +1 -1
  78. package/dist/testing/db.js +24 -1
  79. package/dist/ui/AdminOverview.svelte +5 -10
  80. package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
  81. package/package.json +1 -1
@@ -20,6 +20,19 @@
20
20
  * @module
21
21
  */
22
22
  import { assert_row } from './assert_row.js';
23
+ import { columns_sql, qualify_columns } from './sql_columns.js';
24
+ /**
25
+ * The full `cell_item` column set, named explicitly so a row read fails
26
+ * loud on schema drift (see `CELL_COLUMNS` in `db/cell_queries.ts`). Column
27
+ * order mirrors the Rust twin's `item_columns(alias)` (`fuz_cell`). Keep in
28
+ * sync with `CellItemRow` and the `cell_item` DDL in `db/cell_ddl.ts`.
29
+ */
30
+ export const CELL_ITEM_COLUMNS = [
31
+ 'parent_id',
32
+ 'position',
33
+ 'child_id',
34
+ 'created_at'
35
+ ];
23
36
  /**
24
37
  * Insert one item row at the caller-supplied `position`.
25
38
  *
@@ -34,7 +47,7 @@ import { assert_row } from './assert_row.js';
34
47
  export const query_cell_item_insert = async (deps, input) => {
35
48
  const row = await deps.db.query_one(`INSERT INTO cell_item (parent_id, position, child_id)
36
49
  VALUES ($1, $2, $3)
37
- RETURNING *`, [input.parent_id, input.position, input.child_id]);
50
+ RETURNING ${columns_sql(CELL_ITEM_COLUMNS)}`, [input.parent_id, input.position, input.child_id]);
38
51
  return assert_row(row, 'INSERT INTO cell_item');
39
52
  };
40
53
  /**
@@ -44,7 +57,7 @@ export const query_cell_item_insert = async (deps, input) => {
44
57
  * @returns the row or `null` when not found
45
58
  */
46
59
  export const query_cell_item_get = async (deps, parent_id, position) => {
47
- const row = await deps.db.query_one(`SELECT * FROM cell_item WHERE parent_id = $1 AND position = $2`, [parent_id, position]);
60
+ const row = await deps.db.query_one(`SELECT ${columns_sql(CELL_ITEM_COLUMNS)} FROM cell_item WHERE parent_id = $1 AND position = $2`, [parent_id, position]);
48
61
  return row ?? null;
49
62
  };
50
63
  /**
@@ -64,7 +77,7 @@ export const query_cell_item_move = async (deps, parent_id, position_old, positi
64
77
  const row = await deps.db.query_one(`UPDATE cell_item
65
78
  SET position = $3
66
79
  WHERE parent_id = $1 AND position = $2
67
- RETURNING *`, [parent_id, position_old, position_new]);
80
+ RETURNING ${columns_sql(CELL_ITEM_COLUMNS)}`, [parent_id, position_old, position_new]);
68
81
  return row ?? null;
69
82
  };
70
83
  /**
@@ -76,7 +89,8 @@ export const query_cell_item_move = async (deps, parent_id, position_old, positi
76
89
  * @mutates `cell_item` - deletes one row
77
90
  */
78
91
  export const query_cell_item_delete = async (deps, parent_id, position) => {
79
- const row = await deps.db.query_one(`DELETE FROM cell_item WHERE parent_id = $1 AND position = $2 RETURNING *`, [parent_id, position]);
92
+ const row = await deps.db.query_one(`DELETE FROM cell_item WHERE parent_id = $1 AND position = $2
93
+ RETURNING ${columns_sql(CELL_ITEM_COLUMNS)}`, [parent_id, position]);
80
94
  return row ?? null;
81
95
  };
82
96
  /**
@@ -86,12 +100,16 @@ export const query_cell_item_delete = async (deps, parent_id, position) => {
86
100
  * cells don't surface; the parent filter is the caller's responsibility
87
101
  * (gated upstream by `can_view_cell(parent)`).
88
102
  *
89
- * @param limit - optional row cap (passes through to SQL `LIMIT`)
103
+ * @param deps - query deps
104
+ * @param parent_id - parent cell id
105
+ * @param options - `limit` caps the row count (passes through to SQL `LIMIT`);
106
+ * `position_after` resumes after a position (keyset pagination)
107
+ * @returns matching rows in lex `position` order
90
108
  */
91
109
  export const query_cell_item_list_for_parent = async (deps, parent_id, options) => {
92
110
  const limit = options?.limit ?? null;
93
111
  const position_after = options?.position_after ?? null;
94
- return deps.db.query(`SELECT i.* FROM cell_item i
112
+ return deps.db.query(`SELECT ${qualify_columns(CELL_ITEM_COLUMNS, 'i')} FROM cell_item i
95
113
  JOIN cell c ON c.id = i.child_id
96
114
  WHERE i.parent_id = $1
97
115
  AND c.deleted_at IS NULL
@@ -111,7 +129,7 @@ export const query_cell_item_list_for_parent = async (deps, parent_id, options)
111
129
  * inbound-linked child can't force an unbounded fetch + per-parent authz
112
130
  * pass on the public, IP-rate-limited reverse endpoint.
113
131
  */
114
- export const query_cell_item_list_for_child = async (deps, child_id, options) => deps.db.query(`SELECT i.* FROM cell_item i
132
+ export const query_cell_item_list_for_child = async (deps, child_id, options) => deps.db.query(`SELECT ${qualify_columns(CELL_ITEM_COLUMNS, 'i')} FROM cell_item i
115
133
  JOIN cell p ON p.id = i.parent_id
116
134
  WHERE i.child_id = $1
117
135
  AND p.deleted_at IS NULL
@@ -56,6 +56,35 @@ export interface CellRow {
56
56
  updated_by: Uuid | null;
57
57
  grant_count: number;
58
58
  }
59
+ /**
60
+ * The full `cell` column set, named explicitly so a row read fails loud on
61
+ * schema drift — a `SELECT *` silently omits a dropped column and the
62
+ * `deleted_at IS NULL`-shaped predicates on the hydrated row then misread
63
+ * `undefined` (see `ACCOUNT_COLUMNS` in `auth/account_queries.ts` for the
64
+ * outage class). Column order mirrors the Rust twin's `cell_columns(alias)`
65
+ * (`fuz_cell`), which decodes the same projection positionally. The derived
66
+ * `grant_count` is not a column — `cell_row_projection` appends it. Keep
67
+ * in sync with `CellRow` and the `cell` DDL in `db/cell_ddl.ts`.
68
+ */
69
+ export declare const CELL_COLUMNS: readonly ["id", "data", "kind", "visibility", "path", "refs", "parent_id", "root_id", "moderation", "created_at", "updated_at", "deleted_at", "created_by", "updated_by"];
70
+ /**
71
+ * The full `CellRow` projection — `CELL_COLUMNS` qualified by `alias` plus
72
+ * the derived `grant_count` (a correlated subquery against `cell_grant`,
73
+ * served by `idx_cell_grant_cell`) — used by every cell-row SELECT /
74
+ * RETURNING. `alias` is the row qualifier: the table name (`'cell'`) for
75
+ * single-table reads + `RETURNING`, the query alias (`'c'`) for
76
+ * `query_cell_list`. Twin of `fuz_cell`'s `cell_columns(alias)`.
77
+ *
78
+ * `::int` narrows the count from `bigint` so the JS row hydrates a `number`
79
+ * rather than a bigint primitive — counts on a single cell are trivially
80
+ * within int32.
81
+ *
82
+ * Exported for consumers writing their own cell-row reads: `CELL_COLUMNS`
83
+ * alone hydrates a row without `grant_count`, which typechecks as a
84
+ * `CellRow` but fails at `to_cell_json` — this is the projection that
85
+ * actually produces one.
86
+ */
87
+ export declare const cell_row_projection: (alias: string) => string;
59
88
  /**
60
89
  * Input for `query_cell_create`. `refs` is derived from `data`. `kind` is
61
90
  * the write-once capability axis (`cell.kind` column) — set here at INSERT
@@ -1 +1 @@
1
- {"version":3,"file":"cell_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_queries.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cell_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_queries.ts"],"names":[],"mappings":"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"}
@@ -22,18 +22,51 @@
22
22
  */
23
23
  import { fact_hash_extract_refs } from '@fuzdev/fuz_util/fact_hash.ts';
24
24
  import { assert_row } from './assert_row.js';
25
+ import { qualify_columns } from './sql_columns.js';
25
26
  /**
26
- * SQL fragment for the `grant_count` projection — correlated subquery
27
- * against `cell_grant`. Inlined in every cell-row SELECT / RETURNING so
28
- * `CellRow` carries the count uniformly. Pass the cell alias used in
29
- * the outer query (`'cell'` for table-name references, `'c'` for the
30
- * aliased form in `query_cell_list`).
27
+ * The full `cell` column set, named explicitly so a row read fails loud on
28
+ * schema drift — a `SELECT *` silently omits a dropped column and the
29
+ * `deleted_at IS NULL`-shaped predicates on the hydrated row then misread
30
+ * `undefined` (see `ACCOUNT_COLUMNS` in `auth/account_queries.ts` for the
31
+ * outage class). Column order mirrors the Rust twin's `cell_columns(alias)`
32
+ * (`fuz_cell`), which decodes the same projection positionally. The derived
33
+ * `grant_count` is not a column — `cell_row_projection` appends it. Keep
34
+ * in sync with `CellRow` and the `cell` DDL in `db/cell_ddl.ts`.
35
+ */
36
+ export const CELL_COLUMNS = [
37
+ 'id',
38
+ 'data',
39
+ 'kind',
40
+ 'visibility',
41
+ 'path',
42
+ 'refs',
43
+ 'parent_id',
44
+ 'root_id',
45
+ 'moderation',
46
+ 'created_at',
47
+ 'updated_at',
48
+ 'deleted_at',
49
+ 'created_by',
50
+ 'updated_by'
51
+ ];
52
+ /**
53
+ * The full `CellRow` projection — `CELL_COLUMNS` qualified by `alias` plus
54
+ * the derived `grant_count` (a correlated subquery against `cell_grant`,
55
+ * served by `idx_cell_grant_cell`) — used by every cell-row SELECT /
56
+ * RETURNING. `alias` is the row qualifier: the table name (`'cell'`) for
57
+ * single-table reads + `RETURNING`, the query alias (`'c'`) for
58
+ * `query_cell_list`. Twin of `fuz_cell`'s `cell_columns(alias)`.
59
+ *
60
+ * `::int` narrows the count from `bigint` so the JS row hydrates a `number`
61
+ * rather than a bigint primitive — counts on a single cell are trivially
62
+ * within int32.
31
63
  *
32
- * `::int` narrows from `bigint` to `int` so the JS row hydrates as a
33
- * `number` rather than a bigint primitive — counts on a single cell are
34
- * trivially within int32.
64
+ * Exported for consumers writing their own cell-row reads: `CELL_COLUMNS`
65
+ * alone hydrates a row without `grant_count`, which typechecks as a
66
+ * `CellRow` but fails at `to_cell_json` — this is the projection that
67
+ * actually produces one.
35
68
  */
36
- const grant_count_projection = (cell_alias) => `(SELECT COUNT(*)::int FROM cell_grant WHERE cell_id = ${cell_alias}.id) AS grant_count`;
69
+ export const cell_row_projection = (alias) => `${qualify_columns(CELL_COLUMNS, alias)}, (SELECT COUNT(*)::int FROM cell_grant WHERE cell_id = ${alias}.id) AS grant_count`;
37
70
  /**
38
71
  * Insert a cell row, deriving `refs` from `data`.
39
72
  *
@@ -53,7 +86,7 @@ export const query_cell_create = async (deps, input) => {
53
86
  const row = await deps.db.query_one(`INSERT INTO cell
54
87
  (data, kind, visibility, path, refs, parent_id, root_id, moderation, created_by)
55
88
  VALUES ($1::jsonb, $2, COALESCE($3::cell_visibility, 'private'::cell_visibility), $4, $5::text[], $6, $7, $8, $9)
56
- RETURNING *, ${grant_count_projection('cell')}`, [
89
+ RETURNING ${cell_row_projection('cell')}`, [
57
90
  JSON.stringify(input.data),
58
91
  input.kind ?? null,
59
92
  input.visibility ?? null,
@@ -76,7 +109,7 @@ export const query_cell_create = async (deps, input) => {
76
109
  */
77
110
  export const query_cell_get = async (deps, id, options) => {
78
111
  const include_deleted = options?.include_deleted === true;
79
- const row = await deps.db.query_one(`SELECT *, ${grant_count_projection('cell')}
112
+ const row = await deps.db.query_one(`SELECT ${cell_row_projection('cell')}
80
113
  FROM cell
81
114
  WHERE id = $1
82
115
  AND ($2::bool OR deleted_at IS NULL)`, [id, include_deleted]);
@@ -92,7 +125,7 @@ export const query_cell_get = async (deps, id, options) => {
92
125
  * @returns the row or `null` when not found
93
126
  */
94
127
  export const query_cell_get_by_path = async (deps, path) => {
95
- const row = await deps.db.query_one(`SELECT *, ${grant_count_projection('cell')}
128
+ const row = await deps.db.query_one(`SELECT ${cell_row_projection('cell')}
96
129
  FROM cell
97
130
  WHERE path = $1 AND deleted_at IS NULL`, [path]);
98
131
  return row ?? null;
@@ -111,7 +144,7 @@ export const query_cell_get_by_path = async (deps, path) => {
111
144
  export const query_cell_load_many = async (deps, ids) => {
112
145
  if (ids.length === 0)
113
146
  return [];
114
- return deps.db.query(`SELECT *, ${grant_count_projection('cell')}
147
+ return deps.db.query(`SELECT ${cell_row_projection('cell')}
115
148
  FROM cell
116
149
  WHERE id = ANY($1::uuid[]) AND deleted_at IS NULL`, [ids]);
117
150
  };
@@ -140,7 +173,7 @@ export const query_cell_update = async (deps, id, patch) => {
140
173
  visibility = CASE WHEN $9::bool THEN $10::cell_visibility ELSE visibility END,
141
174
  updated_at = NOW()
142
175
  WHERE id = $1 AND deleted_at IS NULL
143
- RETURNING *, ${grant_count_projection('cell')}`, [
176
+ RETURNING ${cell_row_projection('cell')}`, [
144
177
  id,
145
178
  data_provided,
146
179
  data_provided ? JSON.stringify(patch.data) : null,
@@ -199,7 +232,7 @@ export const query_cell_set_moderation = async (deps, id, moderation, options) =
199
232
  updated_by = $4,
200
233
  updated_at = NOW()
201
234
  WHERE id = $1 AND deleted_at IS NULL
202
- RETURNING *, ${grant_count_projection('cell')}`, [id, moderation, options.set_visibility_public, options.updated_by ?? null]);
235
+ RETURNING ${cell_row_projection('cell')}`, [id, moderation, options.set_visibility_public, options.updated_by ?? null]);
203
236
  return row ?? null;
204
237
  };
205
238
  /**
@@ -211,7 +244,7 @@ export const query_cell_set_moderation = async (deps, id, moderation, options) =
211
244
  * @param options - pagination
212
245
  * @returns matching active rows
213
246
  */
214
- export const query_cell_list_by_kind = async (deps, kind, options) => deps.db.query(`SELECT *, ${grant_count_projection('cell')}
247
+ export const query_cell_list_by_kind = async (deps, kind, options) => deps.db.query(`SELECT ${cell_row_projection('cell')}
215
248
  FROM cell
216
249
  WHERE kind = $1
217
250
  AND deleted_at IS NULL
@@ -226,7 +259,7 @@ export const query_cell_list_by_kind = async (deps, kind, options) => deps.db.qu
226
259
  * @param options - pagination
227
260
  * @returns matching active rows
228
261
  */
229
- export const query_cell_list_by_creator = async (deps, actor_id, options) => deps.db.query(`SELECT *, ${grant_count_projection('cell')}
262
+ export const query_cell_list_by_creator = async (deps, actor_id, options) => deps.db.query(`SELECT ${cell_row_projection('cell')}
230
263
  FROM cell
231
264
  WHERE created_by = $1 AND deleted_at IS NULL
232
265
  ORDER BY created_at DESC
@@ -365,7 +398,7 @@ const CALLER_ROLE_GRANTS_CTE = `WITH caller_role_grants AS (
365
398
  )`;
366
399
  /** General `cell_list` SQL: cell-driven scan, full visibility predicate. */
367
400
  const build_general_sql = (order_column, order_direction) => `${CALLER_ROLE_GRANTS_CTE}
368
- SELECT c.*, ${grant_count_projection('c')} FROM cell c
401
+ SELECT ${cell_row_projection('c')} FROM cell c
369
402
  WHERE ($1::bool OR c.deleted_at IS NULL)
370
403
  AND ($2::text IS NULL OR c.kind = $2::text)
371
404
  AND ($14::cell_visibility IS NULL OR c.visibility = $14::cell_visibility)
@@ -403,7 +436,7 @@ const build_general_sql = (order_column, order_direction) => `${CALLER_ROLE_GRAN
403
436
  * "what's shared with me" wants their grant footprint, not every cell.
404
437
  */
405
438
  const build_shared_with_sql = (order_column, order_direction) => `${CALLER_ROLE_GRANTS_CTE}
406
- SELECT c.*, ${grant_count_projection('c')} FROM cell c
439
+ SELECT ${cell_row_projection('c')} FROM cell c
407
440
  WHERE ($1::bool OR c.deleted_at IS NULL)
408
441
  AND ($2::text IS NULL OR c.kind = $2::text)
409
442
  AND ($14::cell_visibility IS NULL OR c.visibility = $14::cell_visibility)
@@ -422,7 +455,7 @@ const build_shared_with_sql = (order_column, order_direction) => `${CALLER_ROLE_
422
455
  -- $6 (viewer_is_admin) intentionally not consulted: an admin
423
456
  -- asking "what's shared with me" wants their grant footprint, not
424
457
  -- every cell. Cast-only reference keeps the param's type known to
425
- -- the planner so the shared 14-param positional layout stays valid.
458
+ -- the planner so the shared 16-param positional layout stays valid.
426
459
  AND $6::bool IS NOT NULL
427
460
  ORDER BY c.${order_column} ${order_direction} NULLS LAST
428
461
  LIMIT $8 OFFSET $9`;
@@ -22,6 +22,13 @@ export interface FactRow {
22
22
  size: number | string;
23
23
  created_at: Date;
24
24
  }
25
+ /**
26
+ * The full `fact` column set, named explicitly so a row read fails loud on
27
+ * schema drift (see `ACCOUNT_COLUMNS` in `auth/account_queries.ts` for the
28
+ * outage class). Keep in sync with `FactRow` and the `fact` DDL in
29
+ * `db/fact_ddl.ts`.
30
+ */
31
+ export declare const FACT_COLUMNS: readonly ["hash", "bytes", "external_url", "content_type", "size", "created_at"];
25
32
  /** Subset returned by metadata-only queries (no `bytes` payload). */
26
33
  export interface FactMetaRow {
27
34
  hash: FactHash;
@@ -1 +1 @@
1
- {"version":3,"file":"fact_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/fact_queries.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"fact_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/fact_queries.ts"],"names":[],"mappings":"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"}
@@ -11,6 +11,23 @@
11
11
  *
12
12
  * @module
13
13
  */
14
+ import { columns_sql, omit_columns } from './sql_columns.js';
15
+ /**
16
+ * The full `fact` column set, named explicitly so a row read fails loud on
17
+ * schema drift (see `ACCOUNT_COLUMNS` in `auth/account_queries.ts` for the
18
+ * outage class). Keep in sync with `FactRow` and the `fact` DDL in
19
+ * `db/fact_ddl.ts`.
20
+ */
21
+ export const FACT_COLUMNS = [
22
+ 'hash',
23
+ 'bytes',
24
+ 'external_url',
25
+ 'content_type',
26
+ 'size',
27
+ 'created_at'
28
+ ];
29
+ /** `FACT_COLUMNS` minus the payload — the `FactMetaRow` projection. */
30
+ const FACT_META_COLUMNS = omit_columns(FACT_COLUMNS, 'bytes');
14
31
  /**
15
32
  * Idempotently insert a fact row.
16
33
  *
@@ -44,7 +61,7 @@ export const query_put_fact_refs = async (deps, source_hash, target_hashes) => {
44
61
  * `FactStore.get`; cheaper accessors live below.
45
62
  */
46
63
  export const query_get_fact = async (deps, hash) => {
47
- const row = await deps.db.query_one(`SELECT hash, bytes, external_url, content_type, size, created_at
64
+ const row = await deps.db.query_one(`SELECT ${columns_sql(FACT_COLUMNS)}
48
65
  FROM fact WHERE hash = $1`, [hash]);
49
66
  return row ?? null;
50
67
  };
@@ -52,7 +69,7 @@ export const query_get_fact = async (deps, hash) => {
52
69
  * Fetch metadata only — skips the (potentially large) `bytes` column.
53
70
  */
54
71
  export const query_get_fact_meta = async (deps, hash) => {
55
- const row = await deps.db.query_one(`SELECT hash, external_url, content_type, size, created_at
72
+ const row = await deps.db.query_one(`SELECT ${columns_sql(FACT_META_COLUMNS)}
56
73
  FROM fact WHERE hash = $1`, [hash]);
57
74
  return row ?? null;
58
75
  };
@@ -47,9 +47,14 @@ export type ExpectedSchema = Record<string, ReadonlyArray<string>>;
47
47
  * never-migrated DB then correctly fails readiness instead of passing on an
48
48
  * empty expectation.
49
49
  *
50
+ * @param db - the database to introspect
51
+ * @param options - `table` narrows the scan to one relation (the drift guard's
52
+ * one-table question); omitted, every `public` relation is returned
50
53
  * @returns relation name → sorted column names
51
54
  */
52
- export declare const query_public_columns: (db: Db) => Promise<Record<string, Array<string>>>;
55
+ export declare const query_public_columns: (db: Db, options?: {
56
+ table?: string;
57
+ }) => Promise<Record<string, Array<string>>>;
53
58
  /** Columns the live DB is missing for a table the expected schema declares. */
54
59
  export interface MissingColumns {
55
60
  table: string;
@@ -1 +1 @@
1
- {"version":3,"file":"schema_ready.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/schema_ready.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAElC,iFAAiF;AACjF,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC;AAOnE;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,oBAAoB,GAAU,IAAI,EAAE,KAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAWxF,CAAC;AAEF,+EAA+E;AAC/E,MAAM,WAAW,cAAc;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACvB;AAED,uCAAuC;AACvC,MAAM,WAAW,iBAAiB;IACjC,EAAE,EAAE,OAAO,CAAC;IACZ,+CAA+C;IAC/C,cAAc,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC9B,6EAA6E;IAC7E,eAAe,EAAE,KAAK,CAAC,cAAc,CAAC,CAAC;CACvC;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAC9B,IAAI,EAAE,EACN,UAAU,cAAc,KACtB,OAAO,CAAC,iBAAiB,CAmB3B,CAAC;AAEF,qEAAqE;AACrE,eAAO,MAAM,mBAAmB,GAAI,OAAO,iBAAiB,KAAG,MAO9D,CAAC;AAEF,sFAAsF;AACtF,eAAO,MAAM,WAAW;;;CAGd,CAAC"}
1
+ {"version":3,"file":"schema_ready.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/schema_ready.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAElC,iFAAiF;AACjF,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC;AAOnE;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,oBAAoB,GAChC,IAAI,EAAE,EACN,UAAU;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,KAC1B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAavC,CAAC;AAEF,+EAA+E;AAC/E,MAAM,WAAW,cAAc;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACvB;AAED,uCAAuC;AACvC,MAAM,WAAW,iBAAiB;IACjC,EAAE,EAAE,OAAO,CAAC;IACZ,+CAA+C;IAC/C,cAAc,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC9B,6EAA6E;IAC7E,eAAe,EAAE,KAAK,CAAC,cAAc,CAAC,CAAC;CACvC;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAC9B,IAAI,EAAE,EACN,UAAU,cAAc,KACtB,OAAO,CAAC,iBAAiB,CAmB3B,CAAC;AAEF,qEAAqE;AACrE,eAAO,MAAM,mBAAmB,GAAI,OAAO,iBAAiB,KAAG,MAO9D,CAAC;AAEF,sFAAsF;AACtF,eAAO,MAAM,WAAW;;;CAGd,CAAC"}
@@ -44,12 +44,16 @@
44
44
  * never-migrated DB then correctly fails readiness instead of passing on an
45
45
  * empty expectation.
46
46
  *
47
+ * @param db - the database to introspect
48
+ * @param options - `table` narrows the scan to one relation (the drift guard's
49
+ * one-table question); omitted, every `public` relation is returned
47
50
  * @returns relation name → sorted column names
48
51
  */
49
- export const query_public_columns = async (db) => {
52
+ export const query_public_columns = async (db, options) => {
50
53
  const rows = await db.query(`SELECT table_name, column_name FROM information_schema.columns
51
54
  WHERE table_schema = 'public'
52
- ORDER BY table_name, column_name`);
55
+ AND ($1::text IS NULL OR table_name = $1)
56
+ ORDER BY table_name, column_name`, [options?.table ?? null]);
53
57
  const by_table = {};
54
58
  for (const { table_name, column_name } of rows) {
55
59
  (by_table[table_name] ??= []).push(column_name);
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Helpers for the named-column projection consts (`ACCOUNT_COLUMNS`,
3
+ * `CELL_COLUMNS`, …) — per-table column-name arrays that every table read
4
+ * projects through so a dropped or leftover column fails loud instead of
5
+ * vanishing under `SELECT *`.
6
+ *
7
+ * Each query module exports its table's const and derives every projection
8
+ * it needs from it at the read site — `columns_sql` for single-table reads,
9
+ * `qualify_columns` for reads that alias the table, `omit_columns` for a
10
+ * client-safe subset — rather than spelling a second column list, so the
11
+ * drift guard on the base const (`assert_columns_match_live` in
12
+ * `testing/db.ts`) covers every derived projection too. Twin of the Rust
13
+ * spine's `*_COLUMNS` consts + `fuz_db::qualify_columns`.
14
+ *
15
+ * Placement rule: a const lives in its table's query module. When two query
16
+ * modules project the same table (the revoke cascades in
17
+ * `auth/role_grant_queries.ts` supersede `role_grant_offer` rows while the
18
+ * offer module's accept path writes `role_grant` rows), it lives in the
19
+ * table's `*_ddl.ts` instead — the module both already import — so the query
20
+ * modules stay acyclic.
21
+ *
22
+ * Column arrays are literal consts; the alias passed to `qualify_columns` is
23
+ * the only input these helpers treat as dynamic (and validate). Each const
24
+ * also carries `satisfies ReadonlyArray<keyof Row>` so a column name the row
25
+ * type doesn't know is a compile error.
26
+ *
27
+ * @module
28
+ */
29
+ /**
30
+ * Render a `*_COLUMNS` const as a SQL select list, in projection order.
31
+ *
32
+ * @param columns - the column names
33
+ * @returns `a, b, c`
34
+ */
35
+ export declare const columns_sql: (columns: ReadonlyArray<string>) => string;
36
+ /**
37
+ * Render a `*_COLUMNS` const as a SQL select list with every column qualified
38
+ * by a table alias, for reads that alias the table (JOINs, the `c`-aliased
39
+ * `cell_list` scan).
40
+ *
41
+ * The alias is interpolated into SQL, so it must be a plain identifier —
42
+ * every in-repo call passes a literal, but a consumer threading a computed
43
+ * alias through gets a thrown error instead of a malformed (or injectable)
44
+ * projection.
45
+ *
46
+ * @param columns - the column names
47
+ * @param alias - the table alias or name to prefix each column with
48
+ * @returns `alias.a, alias.b, alias.c`
49
+ * @throws Error when `alias` is not a valid SQL identifier
50
+ */
51
+ export declare const qualify_columns: (columns: ReadonlyArray<string>, alias: string) => string;
52
+ /**
53
+ * Drop named columns from a `*_COLUMNS` const, preserving projection order —
54
+ * for a read that deliberately narrows the row (a client-safe listing, a
55
+ * metadata read that skips a payload column).
56
+ *
57
+ * With an `as const` column array the omitted names are checked at compile
58
+ * time; the runtime check covers plain `string[]` consts (a consumer's).
59
+ *
60
+ * @param columns - the column names
61
+ * @param omitted - column names to leave out
62
+ * @returns the columns without the omitted ones, in projection order
63
+ * @throws Error when an omitted name isn't in the list — a typo here would
64
+ * silently keep the column it meant to hide
65
+ */
66
+ export declare const omit_columns: <T extends string>(columns: ReadonlyArray<T>, ...omitted: Array<NoInfer<T>>) => ReadonlyArray<T>;
67
+ //# sourceMappingURL=sql_columns.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sql_columns.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/sql_columns.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAIH;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,SAAS,aAAa,CAAC,MAAM,CAAC,KAAG,MAA4B,CAAC;AAE1F;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,eAAe,GAAI,SAAS,aAAa,CAAC,MAAM,CAAC,EAAE,OAAO,MAAM,KAAG,MAG/E,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,YAAY,GAAI,CAAC,SAAS,MAAM,EAC5C,SAAS,aAAa,CAAC,CAAC,CAAC,EACzB,GAAG,SAAS,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAC3B,aAAa,CAAC,CAAC,CAOjB,CAAC"}
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Helpers for the named-column projection consts (`ACCOUNT_COLUMNS`,
3
+ * `CELL_COLUMNS`, …) — per-table column-name arrays that every table read
4
+ * projects through so a dropped or leftover column fails loud instead of
5
+ * vanishing under `SELECT *`.
6
+ *
7
+ * Each query module exports its table's const and derives every projection
8
+ * it needs from it at the read site — `columns_sql` for single-table reads,
9
+ * `qualify_columns` for reads that alias the table, `omit_columns` for a
10
+ * client-safe subset — rather than spelling a second column list, so the
11
+ * drift guard on the base const (`assert_columns_match_live` in
12
+ * `testing/db.ts`) covers every derived projection too. Twin of the Rust
13
+ * spine's `*_COLUMNS` consts + `fuz_db::qualify_columns`.
14
+ *
15
+ * Placement rule: a const lives in its table's query module. When two query
16
+ * modules project the same table (the revoke cascades in
17
+ * `auth/role_grant_queries.ts` supersede `role_grant_offer` rows while the
18
+ * offer module's accept path writes `role_grant` rows), it lives in the
19
+ * table's `*_ddl.ts` instead — the module both already import — so the query
20
+ * modules stay acyclic.
21
+ *
22
+ * Column arrays are literal consts; the alias passed to `qualify_columns` is
23
+ * the only input these helpers treat as dynamic (and validate). Each const
24
+ * also carries `satisfies ReadonlyArray<keyof Row>` so a column name the row
25
+ * type doesn't know is a compile error.
26
+ *
27
+ * @module
28
+ */
29
+ import { assert_valid_sql_identifier } from './sql_identifier.js';
30
+ /**
31
+ * Render a `*_COLUMNS` const as a SQL select list, in projection order.
32
+ *
33
+ * @param columns - the column names
34
+ * @returns `a, b, c`
35
+ */
36
+ export const columns_sql = (columns) => columns.join(', ');
37
+ /**
38
+ * Render a `*_COLUMNS` const as a SQL select list with every column qualified
39
+ * by a table alias, for reads that alias the table (JOINs, the `c`-aliased
40
+ * `cell_list` scan).
41
+ *
42
+ * The alias is interpolated into SQL, so it must be a plain identifier —
43
+ * every in-repo call passes a literal, but a consumer threading a computed
44
+ * alias through gets a thrown error instead of a malformed (or injectable)
45
+ * projection.
46
+ *
47
+ * @param columns - the column names
48
+ * @param alias - the table alias or name to prefix each column with
49
+ * @returns `alias.a, alias.b, alias.c`
50
+ * @throws Error when `alias` is not a valid SQL identifier
51
+ */
52
+ export const qualify_columns = (columns, alias) => {
53
+ assert_valid_sql_identifier(alias);
54
+ return columns_sql(columns.map((c) => `${alias}.${c}`));
55
+ };
56
+ /**
57
+ * Drop named columns from a `*_COLUMNS` const, preserving projection order —
58
+ * for a read that deliberately narrows the row (a client-safe listing, a
59
+ * metadata read that skips a payload column).
60
+ *
61
+ * With an `as const` column array the omitted names are checked at compile
62
+ * time; the runtime check covers plain `string[]` consts (a consumer's).
63
+ *
64
+ * @param columns - the column names
65
+ * @param omitted - column names to leave out
66
+ * @returns the columns without the omitted ones, in projection order
67
+ * @throws Error when an omitted name isn't in the list — a typo here would
68
+ * silently keep the column it meant to hide
69
+ */
70
+ export const omit_columns = (columns, ...omitted) => {
71
+ for (const o of omitted) {
72
+ if (!columns.includes(o)) {
73
+ throw new Error(`omit_columns: "${o}" is not in the column list "${columns.join(', ')}"`);
74
+ }
75
+ }
76
+ return columns.filter((c) => !omitted.includes(c));
77
+ };
@@ -1,10 +1,11 @@
1
1
  /**
2
- * SQL identifier validation for dynamic DDL queries.
2
+ * SQL identifier validation for the places an identifier must be
3
+ * interpolated rather than parameterized.
3
4
  *
4
- * PostgreSQL DDL operations (DROP TABLE, TRUNCATE, ALTER) do not support
5
- * parameterized table/column names — only values can be parameterized.
6
- * This validator ensures identifiers are safe for string interpolation
7
- * in those specific cases.
5
+ * PostgreSQL parameterizes values only — table and column names in DDL
6
+ * (DROP TABLE, TRUNCATE, ALTER) and the alias `qualify_columns` prefixes
7
+ * onto a projection (`db/sql_columns.ts`) have to be string-interpolated.
8
+ * This validator ensures such identifiers are safe to interpolate.
8
9
  *
9
10
  * @module
10
11
  */
@@ -1 +1 @@
1
- {"version":3,"file":"sql_identifier.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/sql_identifier.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;;;GAGG;AACH,eAAO,MAAM,oBAAoB,QAA6B,CAAC;AAE/D;;;;;;;;;;GAUG;AACH,eAAO,MAAM,2BAA2B,GAAI,MAAM,MAAM,KAAG,MAK1D,CAAC"}
1
+ {"version":3,"file":"sql_identifier.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/sql_identifier.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH;;;GAGG;AACH,eAAO,MAAM,oBAAoB,QAA6B,CAAC;AAE/D;;;;;;;;;;GAUG;AACH,eAAO,MAAM,2BAA2B,GAAI,MAAM,MAAM,KAAG,MAK1D,CAAC"}
@@ -1,10 +1,11 @@
1
1
  /**
2
- * SQL identifier validation for dynamic DDL queries.
2
+ * SQL identifier validation for the places an identifier must be
3
+ * interpolated rather than parameterized.
3
4
  *
4
- * PostgreSQL DDL operations (DROP TABLE, TRUNCATE, ALTER) do not support
5
- * parameterized table/column names — only values can be parameterized.
6
- * This validator ensures identifiers are safe for string interpolation
7
- * in those specific cases.
5
+ * PostgreSQL parameterizes values only — table and column names in DDL
6
+ * (DROP TABLE, TRUNCATE, ALTER) and the alias `qualify_columns` prefixes
7
+ * onto a projection (`db/sql_columns.ts`) have to be string-interpolated.
8
+ * This validator ensures such identifiers are safe to interpolate.
8
9
  *
9
10
  * @module
10
11
  */
@@ -71,7 +71,7 @@ interface RouteContext {
71
71
  ```
72
72
 
73
73
  - **`route.db`** — handler's main DB work. Wrapped in a transaction when `transaction: true` (default for non-GET); routes that opt out (`transaction: false`, e.g. signup / bootstrap) get the pool here directly and may call `route.db.transaction(...)` for their own scope.
74
- - **`route.pending_effects`** — direct push for eager fire-and-forget pool writes (audit, session touch, api-token usage tracking).
74
+ - **`route.pending_effects`** — direct push for eager fire-and-forget pool writes (audit, api-token usage tracking).
75
75
  - **`route.post_commit_effects`** — do not push directly; reach for `emit_after_commit` from `http/pending_effects.ts`. The helper pushes a thunk that the flush middleware invokes after the handler returns, closing the microtask-ordering window that an eager `Promise.resolve().then(fn)` leaves open inside the wrapping `db.transaction`.
76
76
 
77
77
  Pool-level fire-and-forget writes (audit logs, etc.) run through the bound
@@ -83,8 +83,8 @@ export interface RouteContext {
83
83
  db: Db;
84
84
  /**
85
85
  * Eager fire-and-forget queue — push the in-flight `Promise<void>` for
86
- * pool writes already running (audit emits, session touch, api-token
87
- * usage tracking). The flush middleware drains via
86
+ * pool writes already running (audit emits, api-token usage
87
+ * tracking). The flush middleware drains via
88
88
  * `flush_pending_effects` after the handler returns.
89
89
  */
90
90
  pending_effects: Array<Promise<void>>;