@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
@@ -7,7 +7,30 @@
7
7
  * @module
8
8
  */
9
9
  import { assert_row } from '../db/assert_row.js';
10
- import { ROLE_GRANT_OFFER_SCOPE_KIND_GLOBAL_TOKEN, ROLE_GRANT_OFFER_SCOPE_SENTINEL_UUID } from './role_grant_offer_ddl.js';
10
+ import { columns_sql, qualify_columns } from '../db/sql_columns.js';
11
+ import { ROLE_GRANT_OFFER_COLUMNS, ROLE_GRANT_OFFER_SCOPE_KIND_GLOBAL_TOKEN, ROLE_GRANT_OFFER_SCOPE_SENTINEL_UUID, ROLE_GRANT_OFFER_WITH_GRANTOR_SELECT } from './role_grant_offer_ddl.js';
12
+ /**
13
+ * The full `role_grant` column set, named explicitly so a row read fails loud
14
+ * on schema drift (see `ACCOUNT_COLUMNS` in `auth/account_queries.ts` for the
15
+ * outage class). Column order follows `RoleGrant` — the frozen `fuz_auth` v0
16
+ * body plus the appended `scope_kind` / `scope_id` / `revoked_reason` /
17
+ * `source_offer_id` columns. Keep in sync with `RoleGrant` and the
18
+ * `role_grant` migration chain in `auth/auth_ddl.ts`.
19
+ */
20
+ export const ROLE_GRANT_COLUMNS = [
21
+ 'id',
22
+ 'actor_id',
23
+ 'role',
24
+ 'scope_kind',
25
+ 'scope_id',
26
+ 'created_at',
27
+ 'expires_at',
28
+ 'revoked_at',
29
+ 'revoked_by',
30
+ 'revoked_reason',
31
+ 'granted_by',
32
+ 'source_offer_id'
33
+ ];
11
34
  /**
12
35
  * Grant a role_grant to an actor.
13
36
  * Idempotent — if an active role_grant already exists for this actor, role, and
@@ -39,7 +62,7 @@ export const query_create_role_grant = async (deps, input) => {
39
62
  )
40
63
  WHERE revoked_at IS NULL
41
64
  DO NOTHING
42
- RETURNING *`, [
65
+ RETURNING ${columns_sql(ROLE_GRANT_COLUMNS)}`, [
43
66
  input.actor_id,
44
67
  input.role,
45
68
  input.scope_kind ?? null,
@@ -51,7 +74,7 @@ export const query_create_role_grant = async (deps, input) => {
51
74
  if (inserted)
52
75
  return inserted;
53
76
  // Active role_grant already exists — return it (idempotent grant).
54
- const existing = await deps.db.query_one(`SELECT * FROM role_grant
77
+ const existing = await deps.db.query_one(`SELECT ${columns_sql(ROLE_GRANT_COLUMNS)} FROM role_grant
55
78
  WHERE actor_id = $1
56
79
  AND role = $2
57
80
  AND scope_kind IS NOT DISTINCT FROM $3
@@ -59,6 +82,15 @@ export const query_create_role_grant = async (deps, input) => {
59
82
  AND revoked_at IS NULL`, [input.actor_id, input.role, input.scope_kind ?? null, input.scope_id ?? null]);
60
83
  return assert_row(existing, 'idempotent role_grant grant');
61
84
  };
85
+ /**
86
+ * Find a role_grant by id, regardless of revoked / expired state.
87
+ *
88
+ * Unscoped by design — the one caller is `query_accept_offer`'s race-loser
89
+ * path, which resolves an already-accepted offer's `resulting_role_grant_id`
90
+ * (a row the offer's own IDOR guard has already vouched for). Authority
91
+ * checks stay at the call site.
92
+ */
93
+ export const query_role_grant_by_id = async (deps, id) => deps.db.query_one(`SELECT ${columns_sql(ROLE_GRANT_COLUMNS)} FROM role_grant WHERE id = $1`, [id]);
62
94
  /**
63
95
  * Look up the role of an active role_grant (constrained to a specific
64
96
  * actor) plus the actor's `account_id`.
@@ -136,11 +168,9 @@ export const query_revoke_role_grant = async (deps, role_grant_id, actor_id, rev
136
168
  AND o.declined_at IS NULL
137
169
  AND o.retracted_at IS NULL
138
170
  AND o.superseded_at IS NULL
139
- RETURNING o.*
171
+ RETURNING ${qualify_columns(ROLE_GRANT_OFFER_COLUMNS, 'o')}
140
172
  )
141
- SELECT u.*, grantor.account_id AS from_account_id
142
- FROM updated u
143
- JOIN actor grantor ON grantor.id = u.from_actor_id`, [actor_id, revoked.role, revoked.scope_id]);
173
+ ${ROLE_GRANT_OFFER_WITH_GRANTOR_SELECT}`, [actor_id, revoked.role, revoked.scope_id]);
144
174
  return {
145
175
  id: revoked.id,
146
176
  role: revoked.role,
@@ -153,7 +183,7 @@ export const query_revoke_role_grant = async (deps, role_grant_id, actor_id, rev
153
183
  * Find all active (non-revoked, non-expired) role_grants for an actor.
154
184
  */
155
185
  export const query_role_grant_find_active_for_actor = async (deps, actor_id) => {
156
- return deps.db.query(`SELECT * FROM role_grant
186
+ return deps.db.query(`SELECT ${columns_sql(ROLE_GRANT_COLUMNS)} FROM role_grant
157
187
  WHERE actor_id = $1
158
188
  AND revoked_at IS NULL
159
189
  AND (expires_at IS NULL OR expires_at > NOW())
@@ -284,7 +314,7 @@ export const query_count_active_accounts_with_global_role = async (deps, role) =
284
314
  * List all role_grants for an actor (including revoked/expired).
285
315
  */
286
316
  export const query_role_grant_list_for_actor = async (deps, actor_id) => {
287
- return deps.db.query(`SELECT * FROM role_grant WHERE actor_id = $1 ORDER BY created_at DESC`, [actor_id]);
317
+ return deps.db.query(`SELECT ${columns_sql(ROLE_GRANT_COLUMNS)} FROM role_grant WHERE actor_id = $1 ORDER BY created_at DESC`, [actor_id]);
288
318
  };
289
319
  /**
290
320
  * Find the account ID of an account that holds an active role_grant for a given role.
@@ -366,11 +396,9 @@ export const query_role_grant_revoke_for_scope = async (deps, scope_id, revoked_
366
396
  AND o.declined_at IS NULL
367
397
  AND o.retracted_at IS NULL
368
398
  AND o.superseded_at IS NULL
369
- RETURNING o.*
399
+ RETURNING ${qualify_columns(ROLE_GRANT_OFFER_COLUMNS, 'o')}
370
400
  )
371
- SELECT u.*, grantor.account_id AS from_account_id
372
- FROM updated u
373
- JOIN actor grantor ON grantor.id = u.from_actor_id`, [scope_id]);
401
+ ${ROLE_GRANT_OFFER_WITH_GRANTOR_SELECT}`, [scope_id]);
374
402
  return { revoked, superseded_offers };
375
403
  };
376
404
  /**
@@ -420,10 +448,8 @@ export const query_role_grant_revoke_role = async (deps, actor_id, role, revoked
420
448
  AND o.declined_at IS NULL
421
449
  AND o.retracted_at IS NULL
422
450
  AND o.superseded_at IS NULL
423
- RETURNING o.*
451
+ RETURNING ${qualify_columns(ROLE_GRANT_OFFER_COLUMNS, 'o')}
424
452
  )
425
- SELECT u.*, grantor.account_id AS from_account_id
426
- FROM updated u
427
- JOIN actor grantor ON grantor.id = u.from_actor_id`, [actor_id, role]);
453
+ ${ROLE_GRANT_OFFER_WITH_GRANTOR_SELECT}`, [actor_id, role]);
428
454
  return { revoked, superseded_offers };
429
455
  };
@@ -43,6 +43,17 @@ export declare const generate_session_token: () => string;
43
43
  * @mutates `auth_session` table - inserts a row keyed by `token_hash`
44
44
  */
45
45
  export declare const query_create_session: (deps: QueryDeps, token_hash: string, account_id: string, expires_at: Date) => Promise<void>;
46
+ /**
47
+ * The full `auth_session` column set, named explicitly so a row read fails
48
+ * loud on schema drift — `SELECT *` would silently carry a dropped or
49
+ * leftover column into the strict-validated wire shapes (see
50
+ * `ACCOUNT_COLUMNS` in `auth/account_queries.ts` for the outage class this
51
+ * discipline exists to prevent; the Rust twin already names columns at every
52
+ * session site). Keep in sync with `AuthSession` and the migration chain's
53
+ * end state — not the frozen v0 DDL in `auth/auth_ddl.ts`, which still
54
+ * creates `last_seen_at` for the appended drop migration to remove.
55
+ */
56
+ export declare const AUTH_SESSION_COLUMNS: readonly ["id", "account_id", "created_at", "expires_at"];
46
57
  /**
47
58
  * Get a session if it exists, is not expired, and has not been revoked.
48
59
  *
@@ -132,9 +143,7 @@ export declare const query_session_enforce_limit: (deps: QueryDeps, account_id:
132
143
  * List all active sessions across all accounts with usernames.
133
144
  *
134
145
  * Ordered by `created_at DESC` (newest session first), matching the
135
- * per-account listing. `last_seen_at` is still selected for the wire shape
136
- * but is decorative — nothing updates it post-mint (it always equals
137
- * `created_at`); it is slated for removal on its own twin migration.
146
+ * per-account listing.
138
147
  *
139
148
  * @param deps - query dependencies
140
149
  * @param limit - maximum entries to return
@@ -1 +1 @@
1
- {"version":3,"file":"session_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/session_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAElE;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,QAA2B,CAAC;AAEjE;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAAI,OAAO,MAAM,KAAG,SAElD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,QAAO,MAEzC,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,GAChC,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,YAAY,MAAM,EAClB,YAAY,IAAI,KACd,OAAO,CAAC,IAAI,CAMd,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,WAAW,GAAG,SAAS,CAKjC,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,qCAAqC,GACjD,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,IAAI,CAEd,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gCAAgC,GAC5C,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,YAAY,MAAM,KAChB,OAAO,CAAC,OAAO,CAMjB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,oCAAoC,GAChD,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,MAAM,CAMhB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,8BAA8B,GAC1C,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,cAAU,KACR,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAK5B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,eAAO,MAAM,2BAA2B,GACvC,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,cAAc,MAAM,KAClB,OAAO,CAAC,MAAM,CAYhB,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,6BAA6B,GACzC,MAAM,SAAS,EACf,cAAW,KACT,OAAO,CAAC,KAAK,CAAC,WAAW,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CASnD,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,6BAA6B,GAAU,MAAM,SAAS,KAAG,OAAO,CAAC,MAAM,CAKnF,CAAC"}
1
+ {"version":3,"file":"session_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/session_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAErD,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAElE;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,QAA2B,CAAC;AAEjE;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAAI,OAAO,MAAM,KAAG,SAElD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,QAAO,MAEzC,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,GAChC,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,YAAY,MAAM,EAClB,YAAY,IAAI,KACd,OAAO,CAAC,IAAI,CAMd,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,2DAKoB,CAAC;AAEtD;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,WAAW,GAAG,SAAS,CAKjC,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,qCAAqC,GACjD,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,IAAI,CAEd,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gCAAgC,GAC5C,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,YAAY,MAAM,KAChB,OAAO,CAAC,OAAO,CAMjB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,oCAAoC,GAChD,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,MAAM,CAMhB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,8BAA8B,GAC1C,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,cAAU,KACR,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAK5B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,eAAO,MAAM,2BAA2B,GACvC,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,cAAc,MAAM,KAClB,OAAO,CAAC,MAAM,CAYhB,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,6BAA6B,GACzC,MAAM,SAAS,EACf,cAAW,KACT,OAAO,CAAC,KAAK,CAAC,WAAW,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CASnD,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,6BAA6B,GAAU,MAAM,SAAS,KAAG,OAAO,CAAC,MAAM,CAKnF,CAAC"}
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { hash_blake3 } from '@fuzdev/fuz_util/hash_blake3.ts';
10
10
  import { generate_random_base64url } from '../crypto.js';
11
+ import { columns_sql, qualify_columns } from '../db/sql_columns.js';
11
12
  /**
12
13
  * Session lifetime in milliseconds (30 days).
13
14
  *
@@ -53,6 +54,22 @@ export const query_create_session = async (deps, token_hash, account_id, expires
53
54
  expires_at.toISOString()
54
55
  ]);
55
56
  };
57
+ /**
58
+ * The full `auth_session` column set, named explicitly so a row read fails
59
+ * loud on schema drift — `SELECT *` would silently carry a dropped or
60
+ * leftover column into the strict-validated wire shapes (see
61
+ * `ACCOUNT_COLUMNS` in `auth/account_queries.ts` for the outage class this
62
+ * discipline exists to prevent; the Rust twin already names columns at every
63
+ * session site). Keep in sync with `AuthSession` and the migration chain's
64
+ * end state — not the frozen v0 DDL in `auth/auth_ddl.ts`, which still
65
+ * creates `last_seen_at` for the appended drop migration to remove.
66
+ */
67
+ export const AUTH_SESSION_COLUMNS = [
68
+ 'id',
69
+ 'account_id',
70
+ 'created_at',
71
+ 'expires_at'
72
+ ];
56
73
  /**
57
74
  * Get a session if it exists, is not expired, and has not been revoked.
58
75
  *
@@ -60,7 +77,7 @@ export const query_create_session = async (deps, token_hash, account_id, expires
60
77
  * @param token_hash - blake3 hash of the session token
61
78
  */
62
79
  export const query_session_get_valid = async (deps, token_hash) => {
63
- return deps.db.query_one(`SELECT * FROM auth_session WHERE id = $1 AND expires_at > NOW()`, [token_hash]);
80
+ return deps.db.query_one(`SELECT ${columns_sql(AUTH_SESSION_COLUMNS)} FROM auth_session WHERE id = $1 AND expires_at > NOW()`, [token_hash]);
64
81
  };
65
82
  /**
66
83
  * Revoke (delete) a session by its token hash, with no account scoping.
@@ -106,7 +123,7 @@ export const query_session_revoke_all_for_account = async (deps, account_id) =>
106
123
  * List sessions for an account, newest first.
107
124
  */
108
125
  export const query_session_list_for_account = async (deps, account_id, limit = 50) => {
109
- return deps.db.query(`SELECT * FROM auth_session WHERE account_id = $1 ORDER BY created_at DESC LIMIT $2`, [account_id, limit]);
126
+ return deps.db.query(`SELECT ${columns_sql(AUTH_SESSION_COLUMNS)} FROM auth_session WHERE account_id = $1 ORDER BY created_at DESC LIMIT $2`, [account_id, limit]);
110
127
  };
111
128
  /**
112
129
  * Enforce a per-account session limit by evicting the oldest sessions.
@@ -163,16 +180,14 @@ export const query_session_enforce_limit = async (deps, account_id, max_sessions
163
180
  * List all active sessions across all accounts with usernames.
164
181
  *
165
182
  * Ordered by `created_at DESC` (newest session first), matching the
166
- * per-account listing. `last_seen_at` is still selected for the wire shape
167
- * but is decorative — nothing updates it post-mint (it always equals
168
- * `created_at`); it is slated for removal on its own twin migration.
183
+ * per-account listing.
169
184
  *
170
185
  * @param deps - query dependencies
171
186
  * @param limit - maximum entries to return
172
187
  * @returns active sessions joined with account usernames, newest first
173
188
  */
174
189
  export const query_session_list_all_active = async (deps, limit = 200) => {
175
- return deps.db.query(`SELECT s.id, s.account_id, s.created_at, s.expires_at, s.last_seen_at, a.username
190
+ return deps.db.query(`SELECT ${qualify_columns(AUTH_SESSION_COLUMNS, 's')}, a.username
176
191
  FROM auth_session s
177
192
  JOIN account a ON a.id = s.account_id
178
193
  WHERE s.expires_at > NOW()
package/dist/db/CLAUDE.md CHANGED
@@ -27,6 +27,10 @@ the code.
27
27
  - `assert_row.ts` — `assert_row(row, context)` for INSERT … RETURNING.
28
28
  - `pg_error.ts` — `pg_error_code` (SQLSTATE extractor for `pg`/PGlite errors), `is_pg_unique_violation` (Postgres `23505`).
29
29
  - `sql_identifier.ts` — `assert_valid_sql_identifier`.
30
+ - `sql_columns.ts` — `columns_sql` / `qualify_columns` / `omit_columns`,
31
+ the derivation helpers every `*_COLUMNS` projection const's variants
32
+ (alias-qualified JOIN reads, the client-safe `minus token_hash` listing)
33
+ are built from, so the drift guard on the base const covers them.
30
34
  - `status.ts` — CLI DB status utility.
31
35
  - `schema_ready.ts` — `/ready` deploy-gate core: `query_public_columns`
32
36
  (keeps `schema_version`, unlike `query_schema_snapshot`), pure
@@ -98,6 +102,17 @@ child_id`, fractional-index keyed): `query_cell_item_insert` (throws
98
102
  `target_id`), bitmap-OR over `idx_audit_log_metadata` (the `jsonb_path_ops`
99
103
  GIN the `audit_log_metadata_gin_index` auth migration ships).
100
104
 
105
+ **Named projections** (rationale + helpers: ../../../docs/architecture.md
106
+ §Query Modules). The consts on this side: `CELL_COLUMNS`,
107
+ `CELL_GRANT_COLUMNS`, `CELL_FIELD_COLUMNS`, `CELL_ITEM_COLUMNS`, `FACT_COLUMNS`
108
+ (the meta read derives `minus bytes`). The derived `grant_count` stays an
109
+ appended expression in the exported `cell_row_projection(alias)` rather than a
110
+ `CELL_COLUMNS` entry, so the const is exactly the table's column set. Column
111
+ order twins the Rust `fuz_cell` `*_COLUMNS` consts. Every const is pinned to
112
+ the live schema — and every public table forced to carry a const or a named
113
+ exemption — by `src/test/db/column_projections.db.test.ts`; the cell
114
+ `*_queries.db.test.ts` files pin the aliased JOIN reads and `grant_count`.
115
+
101
116
  ## Fact layer
102
117
 
103
118
  Content-addressed byte store. Cells reference facts by blake3 hash
@@ -1 +1 @@
1
- {"version":3,"file":"cell_audit_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_audit_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAoBnD,MAAM,WAAW,oBAAoB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,gDAAgD;IAChD,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,eAAO,MAAM,4BAA4B,GACxC,MAAM,SAAS,EACf,SAAS,IAAI,EACb,SAAS,oBAAoB,KAC3B,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,CAwB9B,CAAC"}
1
+ {"version":3,"file":"cell_audit_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_audit_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAGjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAoBnD,MAAM,WAAW,oBAAoB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,gDAAgD;IAChD,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,eAAO,MAAM,4BAA4B,GACxC,MAAM,SAAS,EACf,SAAS,IAAI,EACb,SAAS,oBAAoB,KAC3B,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,CAwB9B,CAAC"}
@@ -12,6 +12,8 @@
12
12
  *
13
13
  * @module
14
14
  */
15
+ import { AUDIT_LOG_COLUMNS } from '../auth/audit_log_queries.js';
16
+ import { columns_sql } from './sql_columns.js';
15
17
  /**
16
18
  * Metadata-jsonb keys cell-domain events use to name the cell. This
17
19
  * array is the **only** source these key strings can come from —
@@ -51,5 +53,5 @@ export const query_audit_log_list_by_cell = async (deps, cell_id, options) => {
51
53
  const where = `WHERE ${conditions.join(' AND ')}`;
52
54
  const limit_placeholder = `$${i++}`;
53
55
  params.push(options.limit);
54
- return deps.db.query(`SELECT * FROM audit_log ${where} ORDER BY seq DESC LIMIT ${limit_placeholder}`, params);
56
+ return deps.db.query(`SELECT ${columns_sql(AUDIT_LOG_COLUMNS)} FROM audit_log ${where} ORDER BY seq DESC LIMIT ${limit_placeholder}`, params);
55
57
  };
@@ -25,6 +25,13 @@ export interface CellFieldRow {
25
25
  target_id: Uuid;
26
26
  created_at: Date;
27
27
  }
28
+ /**
29
+ * The full `cell_field` column set, named explicitly so a row read fails
30
+ * loud on schema drift (see `CELL_COLUMNS` in `db/cell_queries.ts`). Column
31
+ * order mirrors the Rust twin's `field_columns(alias)` (`fuz_cell`). Keep in
32
+ * sync with `CellFieldRow` and the `cell_field` DDL in `db/cell_ddl.ts`.
33
+ */
34
+ export declare const CELL_FIELD_COLUMNS: readonly ["source_id", "name", "target_id", "created_at"];
28
35
  /** Input for `query_cell_field_set`. */
29
36
  export interface CellFieldSetQueryInput {
30
37
  source_id: Uuid;
@@ -76,7 +83,9 @@ export declare const query_cell_field_delete: (deps: QueryDeps, source_id: Uuid,
76
83
  *
77
84
  * @param deps - query deps
78
85
  * @param source_id - source cell id
79
- * @returns matching rows, oldest first by name (lex order)
86
+ * @param options - `limit` caps the row count; `name_after` resumes after a
87
+ * name (keyset pagination)
88
+ * @returns matching rows in lex `name` order
80
89
  */
81
90
  export declare const query_cell_field_list_for_source: (deps: QueryDeps, source_id: Uuid, options?: {
82
91
  limit?: number;
@@ -97,7 +106,7 @@ export declare const query_cell_field_list_for_source: (deps: QueryDeps, source_
97
106
  * @param deps - query deps
98
107
  * @param target_id - target cell id
99
108
  * @param options - `limit` caps the row count
100
- * @returns matching rows, oldest first by source created_at
109
+ * @returns matching rows, oldest field first (by the field row's `created_at`)
101
110
  */
102
111
  export declare const query_cell_field_list_for_target: (deps: QueryDeps, target_id: Uuid, options?: {
103
112
  limit?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"cell_field_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_field_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAGnD,kDAAkD;AAClD,MAAM,WAAW,YAAY;IAC5B,SAAS,EAAE,IAAI,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,IAAI,CAAC;IAChB,UAAU,EAAE,IAAI,CAAC;CACjB;AAED,wCAAwC;AACxC,MAAM,WAAW,sBAAsB;IACtC,SAAS,EAAE,IAAI,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,IAAI,CAAC;CAChB;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,oBAAoB,GAChC,MAAM,SAAS,EACf,OAAO,sBAAsB,KAC3B,OAAO,CAAC,YAAY,CAUtB,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,oBAAoB,GAChC,MAAM,SAAS,EACf,WAAW,IAAI,EACf,MAAM,MAAM,KACV,OAAO,CAAC,YAAY,GAAG,IAAI,CAM7B,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,SAAS,EACf,WAAW,IAAI,EACf,MAAM,MAAM,KACV,OAAO,CAAC,YAAY,GAAG,IAAI,CAM7B,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gCAAgC,GAC5C,MAAM,SAAS,EACf,WAAW,IAAI,EACf,UAAU;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,KAC/C,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAa7B,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,gCAAgC,GAC5C,MAAM,SAAS,EACf,WAAW,IAAI,EACf,UAAU;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,KAC1B,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAS5B,CAAC"}
1
+ {"version":3,"file":"cell_field_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_field_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAInD,kDAAkD;AAClD,MAAM,WAAW,YAAY;IAC5B,SAAS,EAAE,IAAI,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,IAAI,CAAC;IAChB,UAAU,EAAE,IAAI,CAAC;CACjB;AAED;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,2DAKuB,CAAC;AAEvD,wCAAwC;AACxC,MAAM,WAAW,sBAAsB;IACtC,SAAS,EAAE,IAAI,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,IAAI,CAAC;CAChB;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,oBAAoB,GAChC,MAAM,SAAS,EACf,OAAO,sBAAsB,KAC3B,OAAO,CAAC,YAAY,CAUtB,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,oBAAoB,GAChC,MAAM,SAAS,EACf,WAAW,IAAI,EACf,MAAM,MAAM,KACV,OAAO,CAAC,YAAY,GAAG,IAAI,CAM7B,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,SAAS,EACf,WAAW,IAAI,EACf,MAAM,MAAM,KACV,OAAO,CAAC,YAAY,GAAG,IAAI,CAO7B,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gCAAgC,GAC5C,MAAM,SAAS,EACf,WAAW,IAAI,EACf,UAAU;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,KAC/C,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAa7B,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,gCAAgC,GAC5C,MAAM,SAAS,EACf,WAAW,IAAI,EACf,UAAU;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,KAC1B,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAS5B,CAAC"}
@@ -17,6 +17,19 @@
17
17
  * @module
18
18
  */
19
19
  import { assert_row } from './assert_row.js';
20
+ import { columns_sql, qualify_columns } from './sql_columns.js';
21
+ /**
22
+ * The full `cell_field` column set, named explicitly so a row read fails
23
+ * loud on schema drift (see `CELL_COLUMNS` in `db/cell_queries.ts`). Column
24
+ * order mirrors the Rust twin's `field_columns(alias)` (`fuz_cell`). Keep in
25
+ * sync with `CellFieldRow` and the `cell_field` DDL in `db/cell_ddl.ts`.
26
+ */
27
+ export const CELL_FIELD_COLUMNS = [
28
+ 'source_id',
29
+ 'name',
30
+ 'target_id',
31
+ 'created_at'
32
+ ];
20
33
  /**
21
34
  * Insert or update a field row.
22
35
  *
@@ -35,7 +48,7 @@ export const query_cell_field_set = async (deps, input) => {
35
48
  VALUES ($1, $2, $3)
36
49
  ON CONFLICT (source_id, name)
37
50
  DO UPDATE SET target_id = EXCLUDED.target_id, created_at = NOW()
38
- RETURNING *`, [input.source_id, input.name, input.target_id]);
51
+ RETURNING ${columns_sql(CELL_FIELD_COLUMNS)}`, [input.source_id, input.name, input.target_id]);
39
52
  return assert_row(row, 'INSERT INTO cell_field');
40
53
  };
41
54
  /**
@@ -51,7 +64,7 @@ export const query_cell_field_set = async (deps, input) => {
51
64
  * @returns the row or `null` when not found
52
65
  */
53
66
  export const query_cell_field_get = async (deps, source_id, name) => {
54
- const row = await deps.db.query_one(`SELECT * FROM cell_field WHERE source_id = $1 AND name = $2`, [source_id, name]);
67
+ const row = await deps.db.query_one(`SELECT ${columns_sql(CELL_FIELD_COLUMNS)} FROM cell_field WHERE source_id = $1 AND name = $2`, [source_id, name]);
55
68
  return row ?? null;
56
69
  };
57
70
  /**
@@ -63,7 +76,8 @@ export const query_cell_field_get = async (deps, source_id, name) => {
63
76
  * @mutates `cell_field` - deletes one row
64
77
  */
65
78
  export const query_cell_field_delete = async (deps, source_id, name) => {
66
- const row = await deps.db.query_one(`DELETE FROM cell_field WHERE source_id = $1 AND name = $2 RETURNING *`, [source_id, name]);
79
+ const row = await deps.db.query_one(`DELETE FROM cell_field WHERE source_id = $1 AND name = $2
80
+ RETURNING ${columns_sql(CELL_FIELD_COLUMNS)}`, [source_id, name]);
67
81
  return row ?? null;
68
82
  };
69
83
  /**
@@ -75,12 +89,14 @@ export const query_cell_field_delete = async (deps, source_id, name) => {
75
89
  *
76
90
  * @param deps - query deps
77
91
  * @param source_id - source cell id
78
- * @returns matching rows, oldest first by name (lex order)
92
+ * @param options - `limit` caps the row count; `name_after` resumes after a
93
+ * name (keyset pagination)
94
+ * @returns matching rows in lex `name` order
79
95
  */
80
96
  export const query_cell_field_list_for_source = async (deps, source_id, options) => {
81
97
  const limit = options?.limit ?? null;
82
98
  const name_after = options?.name_after ?? null;
83
- return deps.db.query(`SELECT f.* FROM cell_field f
99
+ return deps.db.query(`SELECT ${qualify_columns(CELL_FIELD_COLUMNS, 'f')} FROM cell_field f
84
100
  JOIN cell t ON t.id = f.target_id
85
101
  WHERE f.source_id = $1
86
102
  AND t.deleted_at IS NULL
@@ -103,9 +119,9 @@ export const query_cell_field_list_for_source = async (deps, source_id, options)
103
119
  * @param deps - query deps
104
120
  * @param target_id - target cell id
105
121
  * @param options - `limit` caps the row count
106
- * @returns matching rows, oldest first by source created_at
122
+ * @returns matching rows, oldest field first (by the field row's `created_at`)
107
123
  */
108
- export const query_cell_field_list_for_target = async (deps, target_id, options) => deps.db.query(`SELECT f.* FROM cell_field f
124
+ export const query_cell_field_list_for_target = async (deps, target_id, options) => deps.db.query(`SELECT ${qualify_columns(CELL_FIELD_COLUMNS, 'f')} FROM cell_field f
109
125
  JOIN cell s ON s.id = f.source_id
110
126
  WHERE f.target_id = $1
111
127
  AND s.deleted_at IS NULL
@@ -35,6 +35,13 @@ export interface CellGrantRow {
35
35
  granted_by: Uuid | null;
36
36
  created_at: Date;
37
37
  }
38
+ /**
39
+ * The full `cell_grant` column set, named explicitly so a row read fails
40
+ * loud on schema drift (see `CELL_COLUMNS` in `db/cell_queries.ts`). Column
41
+ * order mirrors the Rust twin's `grant_columns(alias)` (`fuz_cell`). Keep in
42
+ * sync with `CellGrantRow` and the `cell_grant` DDL in `db/cell_ddl.ts`.
43
+ */
44
+ export declare const CELL_GRANT_COLUMNS: readonly ["id", "cell_id", "level", "actor_id", "role", "scope_id", "granted_by", "created_at"];
38
45
  /**
39
46
  * Discriminated principal input for `query_cell_grant_create`. Wire
40
47
  * and query shapes are aligned — actor-shaped principals carry a
@@ -121,6 +128,7 @@ export declare const query_cell_grant_list_for_cells: (deps: QueryDeps, cell_ids
121
128
  * granted access. Returns grants for the given cells that match the caller's
122
129
  * identity or role_grant set.
123
130
  *
131
+ * @param deps - query deps
124
132
  * @param cell_ids - cells to fetch grants for
125
133
  * @param caller_actor_id - actor id of the caller (null for unauth)
126
134
  * @param role_grant_roles - active role_grant roles (parallel array)
@@ -128,5 +136,5 @@ export declare const query_cell_grant_list_for_cells: (deps: QueryDeps, cell_ids
128
136
  * @returns matching grants (may include grants the caller doesn't match; caller's
129
137
  * list handler must filter when returning to the API)
130
138
  */
131
- export declare const query_cell_grants_for_caller_in_cells: (deps: QueryDeps, cell_ids: Array<Uuid>, caller_actor_id: Uuid | null, role_grant_roles: Array<string>, role_grant_scope_ids: Array<Uuid | null>) => Promise<Array<CellGrantRow>>;
139
+ export declare const query_cell_grants_for_caller_in_cells: (deps: QueryDeps, cell_ids: ReadonlyArray<Uuid>, caller_actor_id: Uuid | null, role_grant_roles: ReadonlyArray<string>, role_grant_scope_ids: ReadonlyArray<Uuid | null>) => Promise<Array<CellGrantRow>>;
132
140
  //# sourceMappingURL=cell_grant_queries.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cell_grant_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_grant_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAGzE,kDAAkD;AAClD,MAAM,WAAW,YAAY;IAC5B,EAAE,EAAE,IAAI,CAAC;IACT,OAAO,EAAE,IAAI,CAAC;IACd,KAAK,EAAE,cAAc,CAAC;IACtB,QAAQ,EAAE,IAAI,GAAG,IAAI,CAAC;IACtB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,QAAQ,EAAE,IAAI,GAAG,IAAI,CAAC;IACtB,UAAU,EAAE,IAAI,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,IAAI,CAAC;CACjB;AAED;;;;;GAKG;AACH,MAAM,MAAM,4BAA4B,GACvC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,IAAI,GAAG,IAAI,CAAA;CAAE,CAAC;AAE3F,2CAA2C;AAC3C,MAAM,WAAW,yBAAyB;IACzC,OAAO,EAAE,IAAI,CAAC;IACd,KAAK,EAAE,cAAc,CAAC;IACtB,SAAS,EAAE,4BAA4B,CAAC;IACxC,UAAU,EAAE,IAAI,GAAG,IAAI,CAAC;CACxB;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,SAAS,EACf,OAAO,yBAAyB,KAC9B,OAAO,CAAC,YAAY,CAsBtB,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAChC,MAAM,SAAS,EACf,UAAU,IAAI,KACZ,OAAO,CAAC,YAAY,GAAG,IAAI,CAK7B,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,SAAS,EACf,UAAU,IAAI,KACZ,OAAO,CAAC,YAAY,GAAG,IAAI,CAM7B,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,8BAA8B,GAC1C,MAAM,SAAS,EACf,SAAS,IAAI,KACX,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAM5B,CAAC;AAEH;;;;;;;;;;GAUG;AACH,eAAO,MAAM,+BAA+B,GAC3C,MAAM,SAAS,EACf,UAAU,aAAa,CAAC,IAAI,CAAC,KAC3B,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAQ7B,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,qCAAqC,GACjD,MAAM,SAAS,EACf,UAAU,KAAK,CAAC,IAAI,CAAC,EACrB,iBAAiB,IAAI,GAAG,IAAI,EAC5B,kBAAkB,KAAK,CAAC,MAAM,CAAC,EAC/B,sBAAsB,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,KACtC,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAmB7B,CAAC"}
1
+ {"version":3,"file":"cell_grant_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_grant_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAIzE,kDAAkD;AAClD,MAAM,WAAW,YAAY;IAC5B,EAAE,EAAE,IAAI,CAAC;IACT,OAAO,EAAE,IAAI,CAAC;IACd,KAAK,EAAE,cAAc,CAAC;IACtB,QAAQ,EAAE,IAAI,GAAG,IAAI,CAAC;IACtB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,QAAQ,EAAE,IAAI,GAAG,IAAI,CAAC;IACtB,UAAU,EAAE,IAAI,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,IAAI,CAAC;CACjB;AAED;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,iGASuB,CAAC;AAEvD;;;;;GAKG;AACH,MAAM,MAAM,4BAA4B,GACvC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,IAAI,GAAG,IAAI,CAAA;CAAE,CAAC;AAE3F,2CAA2C;AAC3C,MAAM,WAAW,yBAAyB;IACzC,OAAO,EAAE,IAAI,CAAC;IACd,KAAK,EAAE,cAAc,CAAC;IACtB,SAAS,EAAE,4BAA4B,CAAC;IACxC,UAAU,EAAE,IAAI,GAAG,IAAI,CAAC;CACxB;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,SAAS,EACf,OAAO,yBAAyB,KAC9B,OAAO,CAAC,YAAY,CAsBtB,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAChC,MAAM,SAAS,EACf,UAAU,IAAI,KACZ,OAAO,CAAC,YAAY,GAAG,IAAI,CAM7B,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,SAAS,EACf,UAAU,IAAI,KACZ,OAAO,CAAC,YAAY,GAAG,IAAI,CAM7B,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,8BAA8B,GAC1C,MAAM,SAAS,EACf,SAAS,IAAI,KACX,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAM5B,CAAC;AAEH;;;;;;;;;;GAUG;AACH,eAAO,MAAM,+BAA+B,GAC3C,MAAM,SAAS,EACf,UAAU,aAAa,CAAC,IAAI,CAAC,KAC3B,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAQ7B,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,qCAAqC,GACjD,MAAM,SAAS,EACf,UAAU,aAAa,CAAC,IAAI,CAAC,EAC7B,iBAAiB,IAAI,GAAG,IAAI,EAC5B,kBAAkB,aAAa,CAAC,MAAM,CAAC,EACvC,sBAAsB,aAAa,CAAC,IAAI,GAAG,IAAI,CAAC,KAC9C,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAmB7B,CAAC"}
@@ -22,6 +22,23 @@
22
22
  * @module
23
23
  */
24
24
  import { assert_row } from './assert_row.js';
25
+ import { columns_sql, qualify_columns } from './sql_columns.js';
26
+ /**
27
+ * The full `cell_grant` column set, named explicitly so a row read fails
28
+ * loud on schema drift (see `CELL_COLUMNS` in `db/cell_queries.ts`). Column
29
+ * order mirrors the Rust twin's `grant_columns(alias)` (`fuz_cell`). Keep in
30
+ * sync with `CellGrantRow` and the `cell_grant` DDL in `db/cell_ddl.ts`.
31
+ */
32
+ export const CELL_GRANT_COLUMNS = [
33
+ 'id',
34
+ 'cell_id',
35
+ 'level',
36
+ 'actor_id',
37
+ 'role',
38
+ 'scope_id',
39
+ 'granted_by',
40
+ 'created_at'
41
+ ];
25
42
  /**
26
43
  * Insert a grant, or update the existing row's `level` + `granted_by` when
27
44
  * one already exists for the same `(cell_id, principal)` pair.
@@ -43,14 +60,14 @@ export const query_cell_grant_create = async (deps, input) => {
43
60
  VALUES ($1, $2, $3, $4)
44
61
  ON CONFLICT (cell_id, actor_id) WHERE actor_id IS NOT NULL
45
62
  DO UPDATE SET level = EXCLUDED.level, granted_by = EXCLUDED.granted_by
46
- RETURNING *`, [cell_id, level, principal.actor_id, granted_by]);
63
+ RETURNING ${columns_sql(CELL_GRANT_COLUMNS)}`, [cell_id, level, principal.actor_id, granted_by]);
47
64
  return assert_row(row, 'INSERT INTO cell_grant (actor)');
48
65
  }
49
66
  const row = await deps.db.query_one(`INSERT INTO cell_grant (cell_id, level, role, scope_id, granted_by)
50
67
  VALUES ($1, $2, $3, $4, $5)
51
68
  ON CONFLICT (cell_id, role, scope_id) WHERE role IS NOT NULL
52
69
  DO UPDATE SET level = EXCLUDED.level, granted_by = EXCLUDED.granted_by
53
- RETURNING *`, [cell_id, level, principal.role, principal.scope_id, granted_by]);
70
+ RETURNING ${columns_sql(CELL_GRANT_COLUMNS)}`, [cell_id, level, principal.role, principal.scope_id, granted_by]);
54
71
  return assert_row(row, 'INSERT INTO cell_grant (role)');
55
72
  };
56
73
  /**
@@ -61,9 +78,7 @@ export const query_cell_grant_create = async (deps, input) => {
61
78
  * @returns the row or `null` when not found
62
79
  */
63
80
  export const query_cell_grant_get = async (deps, grant_id) => {
64
- const row = await deps.db.query_one(`SELECT * FROM cell_grant WHERE id = $1`, [
65
- grant_id
66
- ]);
81
+ const row = await deps.db.query_one(`SELECT ${columns_sql(CELL_GRANT_COLUMNS)} FROM cell_grant WHERE id = $1`, [grant_id]);
67
82
  return row ?? null;
68
83
  };
69
84
  /**
@@ -79,7 +94,7 @@ export const query_cell_grant_get = async (deps, grant_id) => {
79
94
  * @mutates `cell_grant` - deletes one row
80
95
  */
81
96
  export const query_cell_grant_delete = async (deps, grant_id) => {
82
- const row = await deps.db.query_one(`DELETE FROM cell_grant WHERE id = $1 RETURNING *`, [grant_id]);
97
+ const row = await deps.db.query_one(`DELETE FROM cell_grant WHERE id = $1 RETURNING ${columns_sql(CELL_GRANT_COLUMNS)}`, [grant_id]);
83
98
  return row ?? null;
84
99
  };
85
100
  /**
@@ -92,7 +107,7 @@ export const query_cell_grant_delete = async (deps, grant_id) => {
92
107
  * @param cell_id - cell id
93
108
  * @returns matching rows
94
109
  */
95
- export const query_cell_grant_list_for_cell = async (deps, cell_id) => deps.db.query(`SELECT * FROM cell_grant
110
+ export const query_cell_grant_list_for_cell = async (deps, cell_id) => deps.db.query(`SELECT ${columns_sql(CELL_GRANT_COLUMNS)} FROM cell_grant
96
111
  WHERE cell_id = $1
97
112
  ORDER BY created_at ASC`, [cell_id]);
98
113
  /**
@@ -109,7 +124,7 @@ export const query_cell_grant_list_for_cell = async (deps, cell_id) => deps.db.q
109
124
  export const query_cell_grant_list_for_cells = async (deps, cell_ids) => {
110
125
  if (cell_ids.length === 0)
111
126
  return [];
112
- return deps.db.query(`SELECT * FROM cell_grant
127
+ return deps.db.query(`SELECT ${columns_sql(CELL_GRANT_COLUMNS)} FROM cell_grant
113
128
  WHERE cell_id = ANY($1::uuid[])
114
129
  ORDER BY cell_id, created_at ASC`, [cell_ids]);
115
130
  };
@@ -119,6 +134,7 @@ export const query_cell_grant_list_for_cells = async (deps, cell_ids) => {
119
134
  * granted access. Returns grants for the given cells that match the caller's
120
135
  * identity or role_grant set.
121
136
  *
137
+ * @param deps - query deps
122
138
  * @param cell_ids - cells to fetch grants for
123
139
  * @param caller_actor_id - actor id of the caller (null for unauth)
124
140
  * @param role_grant_roles - active role_grant roles (parallel array)
@@ -130,7 +146,7 @@ export const query_cell_grants_for_caller_in_cells = async (deps, cell_ids, call
130
146
  if (cell_ids.length === 0) {
131
147
  return [];
132
148
  }
133
- return deps.db.query(`SELECT g.* FROM cell_grant g
149
+ return deps.db.query(`SELECT ${qualify_columns(CELL_GRANT_COLUMNS, 'g')} FROM cell_grant g
134
150
  WHERE g.cell_id = ANY($1::uuid[])
135
151
  AND (
136
152
  g.actor_id = $2
@@ -28,6 +28,13 @@ export interface CellItemRow {
28
28
  child_id: Uuid;
29
29
  created_at: Date;
30
30
  }
31
+ /**
32
+ * The full `cell_item` column set, named explicitly so a row read fails
33
+ * loud on schema drift (see `CELL_COLUMNS` in `db/cell_queries.ts`). Column
34
+ * order mirrors the Rust twin's `item_columns(alias)` (`fuz_cell`). Keep in
35
+ * sync with `CellItemRow` and the `cell_item` DDL in `db/cell_ddl.ts`.
36
+ */
37
+ export declare const CELL_ITEM_COLUMNS: readonly ["parent_id", "position", "child_id", "created_at"];
31
38
  /** Input for `query_cell_item_insert`. */
32
39
  export interface CellItemInsertQueryInput {
33
40
  parent_id: Uuid;
@@ -83,7 +90,11 @@ export declare const query_cell_item_delete: (deps: QueryDeps, parent_id: Uuid,
83
90
  * cells don't surface; the parent filter is the caller's responsibility
84
91
  * (gated upstream by `can_view_cell(parent)`).
85
92
  *
86
- * @param limit - optional row cap (passes through to SQL `LIMIT`)
93
+ * @param deps - query deps
94
+ * @param parent_id - parent cell id
95
+ * @param options - `limit` caps the row count (passes through to SQL `LIMIT`);
96
+ * `position_after` resumes after a position (keyset pagination)
97
+ * @returns matching rows in lex `position` order
87
98
  */
88
99
  export declare const query_cell_item_list_for_parent: (deps: QueryDeps, parent_id: Uuid, options?: {
89
100
  limit?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"cell_item_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_item_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAGnD,iDAAiD;AACjD,MAAM,WAAW,WAAW;IAC3B,SAAS,EAAE,IAAI,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,IAAI,CAAC;IACf,UAAU,EAAE,IAAI,CAAC;CACjB;AAED,0CAA0C;AAC1C,MAAM,WAAW,wBAAwB;IACxC,SAAS,EAAE,IAAI,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,IAAI,CAAC;CACf;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,SAAS,EACf,OAAO,wBAAwB,KAC7B,OAAO,CAAC,WAAW,CAQrB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,GAC/B,MAAM,SAAS,EACf,WAAW,IAAI,EACf,UAAU,MAAM,KACd,OAAO,CAAC,WAAW,GAAG,IAAI,CAM5B,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,oBAAoB,GAChC,MAAM,SAAS,EACf,WAAW,IAAI,EACf,cAAc,MAAM,EACpB,cAAc,MAAM,KAClB,OAAO,CAAC,WAAW,GAAG,IAAI,CAS5B,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,SAAS,EACf,WAAW,IAAI,EACf,UAAU,MAAM,KACd,OAAO,CAAC,WAAW,GAAG,IAAI,CAM5B,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,+BAA+B,GAC3C,MAAM,SAAS,EACf,WAAW,IAAI,EACf,UAAU;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CAAE,KACnD,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAa5B,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,8BAA8B,GAC1C,MAAM,SAAS,EACf,UAAU,IAAI,EACd,UAAU;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,KAC1B,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAS3B,CAAC"}
1
+ {"version":3,"file":"cell_item_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_item_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAInD,iDAAiD;AACjD,MAAM,WAAW,WAAW;IAC3B,SAAS,EAAE,IAAI,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,IAAI,CAAC;IACf,UAAU,EAAE,IAAI,CAAC;CACjB;AAED;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,8DAKuB,CAAC;AAEtD,0CAA0C;AAC1C,MAAM,WAAW,wBAAwB;IACxC,SAAS,EAAE,IAAI,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,IAAI,CAAC;CACf;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,SAAS,EACf,OAAO,wBAAwB,KAC7B,OAAO,CAAC,WAAW,CAQrB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,GAC/B,MAAM,SAAS,EACf,WAAW,IAAI,EACf,UAAU,MAAM,KACd,OAAO,CAAC,WAAW,GAAG,IAAI,CAM5B,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,oBAAoB,GAChC,MAAM,SAAS,EACf,WAAW,IAAI,EACf,cAAc,MAAM,EACpB,cAAc,MAAM,KAClB,OAAO,CAAC,WAAW,GAAG,IAAI,CAS5B,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,SAAS,EACf,WAAW,IAAI,EACf,UAAU,MAAM,KACd,OAAO,CAAC,WAAW,GAAG,IAAI,CAO5B,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,+BAA+B,GAC3C,MAAM,SAAS,EACf,WAAW,IAAI,EACf,UAAU;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CAAE,KACnD,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAa5B,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,8BAA8B,GAC1C,MAAM,SAAS,EACf,UAAU,IAAI,EACd,UAAU;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,KAC1B,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAS3B,CAAC"}