@fuzdev/fuz_app 0.67.1 → 0.68.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 (164) hide show
  1. package/dist/auth/CLAUDE.md +99 -5
  2. package/dist/auth/account_queries.d.ts +87 -4
  3. package/dist/auth/account_queries.d.ts.map +1 -1
  4. package/dist/auth/account_queries.js +107 -17
  5. package/dist/auth/account_schema.d.ts +19 -0
  6. package/dist/auth/account_schema.d.ts.map +1 -1
  7. package/dist/auth/account_schema.js +8 -0
  8. package/dist/auth/admin_action_specs.d.ts +168 -0
  9. package/dist/auth/admin_action_specs.d.ts.map +1 -1
  10. package/dist/auth/admin_action_specs.js +146 -1
  11. package/dist/auth/admin_actions.d.ts.map +1 -1
  12. package/dist/auth/admin_actions.js +218 -4
  13. package/dist/auth/audit_log_ddl.d.ts +10 -1
  14. package/dist/auth/audit_log_ddl.d.ts.map +1 -1
  15. package/dist/auth/audit_log_ddl.js +13 -4
  16. package/dist/auth/audit_log_schema.d.ts +34 -1
  17. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  18. package/dist/auth/audit_log_schema.js +73 -0
  19. package/dist/auth/auth_ddl.d.ts +2 -2
  20. package/dist/auth/auth_ddl.d.ts.map +1 -1
  21. package/dist/auth/auth_ddl.js +10 -2
  22. package/dist/auth/cell_action_specs.d.ts +1295 -0
  23. package/dist/auth/cell_action_specs.d.ts.map +1 -0
  24. package/dist/auth/cell_action_specs.js +397 -0
  25. package/dist/auth/cell_actions.d.ts +63 -0
  26. package/dist/auth/cell_actions.d.ts.map +1 -0
  27. package/dist/auth/cell_actions.js +546 -0
  28. package/dist/auth/cell_audit_action_specs.d.ts +131 -0
  29. package/dist/auth/cell_audit_action_specs.d.ts.map +1 -0
  30. package/dist/auth/cell_audit_action_specs.js +70 -0
  31. package/dist/auth/cell_audit_actions.d.ts +18 -0
  32. package/dist/auth/cell_audit_actions.d.ts.map +1 -0
  33. package/dist/auth/cell_audit_actions.js +59 -0
  34. package/dist/auth/cell_audit_events.d.ts +28 -0
  35. package/dist/auth/cell_audit_events.d.ts.map +1 -0
  36. package/dist/auth/cell_audit_events.js +42 -0
  37. package/dist/auth/cell_audit_metadata.d.ts +48 -0
  38. package/dist/auth/cell_audit_metadata.d.ts.map +1 -0
  39. package/dist/auth/cell_audit_metadata.js +46 -0
  40. package/dist/auth/cell_authorize.d.ts +88 -0
  41. package/dist/auth/cell_authorize.d.ts.map +1 -0
  42. package/dist/auth/cell_authorize.js +172 -0
  43. package/dist/auth/cell_data_schema.d.ts +44 -0
  44. package/dist/auth/cell_data_schema.d.ts.map +1 -0
  45. package/dist/auth/cell_data_schema.js +42 -0
  46. package/dist/auth/cell_field_action_specs.d.ts +244 -0
  47. package/dist/auth/cell_field_action_specs.d.ts.map +1 -0
  48. package/dist/auth/cell_field_action_specs.js +136 -0
  49. package/dist/auth/cell_field_actions.d.ts +34 -0
  50. package/dist/auth/cell_field_actions.d.ts.map +1 -0
  51. package/dist/auth/cell_field_actions.js +153 -0
  52. package/dist/auth/cell_field_audit_metadata.d.ts +30 -0
  53. package/dist/auth/cell_field_audit_metadata.d.ts.map +1 -0
  54. package/dist/auth/cell_field_audit_metadata.js +28 -0
  55. package/dist/auth/cell_grant_action_specs.d.ts +333 -0
  56. package/dist/auth/cell_grant_action_specs.d.ts.map +1 -0
  57. package/dist/auth/cell_grant_action_specs.js +148 -0
  58. package/dist/auth/cell_grant_actions.d.ts +50 -0
  59. package/dist/auth/cell_grant_actions.d.ts.map +1 -0
  60. package/dist/auth/cell_grant_actions.js +208 -0
  61. package/dist/auth/cell_grant_audit_metadata.d.ts +75 -0
  62. package/dist/auth/cell_grant_audit_metadata.d.ts.map +1 -0
  63. package/dist/auth/cell_grant_audit_metadata.js +54 -0
  64. package/dist/auth/cell_item_action_specs.d.ts +331 -0
  65. package/dist/auth/cell_item_action_specs.d.ts.map +1 -0
  66. package/dist/auth/cell_item_action_specs.js +182 -0
  67. package/dist/auth/cell_item_actions.d.ts +37 -0
  68. package/dist/auth/cell_item_actions.d.ts.map +1 -0
  69. package/dist/auth/cell_item_actions.js +204 -0
  70. package/dist/auth/cell_item_audit_metadata.d.ts +35 -0
  71. package/dist/auth/cell_item_audit_metadata.d.ts.map +1 -0
  72. package/dist/auth/cell_item_audit_metadata.js +32 -0
  73. package/dist/auth/cell_relation_visibility.d.ts +32 -0
  74. package/dist/auth/cell_relation_visibility.d.ts.map +1 -0
  75. package/dist/auth/cell_relation_visibility.js +57 -0
  76. package/dist/auth/deps.d.ts +9 -0
  77. package/dist/auth/deps.d.ts.map +1 -1
  78. package/dist/auth/role_grant_queries.d.ts +30 -0
  79. package/dist/auth/role_grant_queries.d.ts.map +1 -1
  80. package/dist/auth/role_grant_queries.js +54 -0
  81. package/dist/db/CLAUDE.md +118 -0
  82. package/dist/db/cell_audit_queries.d.ts +26 -0
  83. package/dist/db/cell_audit_queries.d.ts.map +1 -0
  84. package/dist/db/cell_audit_queries.js +53 -0
  85. package/dist/db/cell_ddl.d.ts +151 -0
  86. package/dist/db/cell_ddl.d.ts.map +1 -0
  87. package/dist/db/cell_ddl.js +247 -0
  88. package/dist/db/cell_field_queries.d.ts +105 -0
  89. package/dist/db/cell_field_queries.d.ts.map +1 -0
  90. package/dist/db/cell_field_queries.js +113 -0
  91. package/dist/db/cell_grant_queries.d.ts +132 -0
  92. package/dist/db/cell_grant_queries.d.ts.map +1 -0
  93. package/dist/db/cell_grant_queries.js +145 -0
  94. package/dist/db/cell_history_ddl.d.ts +38 -0
  95. package/dist/db/cell_history_ddl.d.ts.map +1 -0
  96. package/dist/db/cell_history_ddl.js +61 -0
  97. package/dist/db/cell_item_queries.d.ts +107 -0
  98. package/dist/db/cell_item_queries.d.ts.map +1 -0
  99. package/dist/db/cell_item_queries.js +119 -0
  100. package/dist/db/cell_queries.d.ts +327 -0
  101. package/dist/db/cell_queries.d.ts.map +1 -0
  102. package/dist/db/cell_queries.js +431 -0
  103. package/dist/db/fact_ddl.d.ts +38 -0
  104. package/dist/db/fact_ddl.d.ts.map +1 -0
  105. package/dist/db/fact_ddl.js +71 -0
  106. package/dist/db/fact_queries.d.ts +140 -0
  107. package/dist/db/fact_queries.d.ts.map +1 -0
  108. package/dist/db/fact_queries.js +161 -0
  109. package/dist/db/fact_store.d.ts +112 -0
  110. package/dist/db/fact_store.d.ts.map +1 -0
  111. package/dist/db/fact_store.js +225 -0
  112. package/dist/server/env.d.ts +2 -0
  113. package/dist/server/env.d.ts.map +1 -1
  114. package/dist/server/env.js +6 -0
  115. package/dist/server/fact_write.d.ts +32 -0
  116. package/dist/server/fact_write.d.ts.map +1 -0
  117. package/dist/server/fact_write.js +56 -0
  118. package/dist/server/file_fact_fetcher.d.ts +42 -0
  119. package/dist/server/file_fact_fetcher.d.ts.map +1 -0
  120. package/dist/server/file_fact_fetcher.js +60 -0
  121. package/dist/server/file_fact_url.d.ts +53 -0
  122. package/dist/server/file_fact_url.d.ts.map +1 -0
  123. package/dist/server/file_fact_url.js +52 -0
  124. package/dist/server/serve_fact_route.d.ts +78 -0
  125. package/dist/server/serve_fact_route.d.ts.map +1 -0
  126. package/dist/server/serve_fact_route.js +205 -0
  127. package/dist/testing/CLAUDE.md +58 -5
  128. package/dist/testing/app_server.d.ts +12 -0
  129. package/dist/testing/app_server.d.ts.map +1 -1
  130. package/dist/testing/app_server.js +36 -2
  131. package/dist/testing/audit_completeness.d.ts.map +1 -1
  132. package/dist/testing/audit_completeness.js +67 -1
  133. package/dist/testing/cross_backend/account_lifecycle.d.ts +10 -0
  134. package/dist/testing/cross_backend/account_lifecycle.d.ts.map +1 -0
  135. package/dist/testing/cross_backend/account_lifecycle.js +76 -0
  136. package/dist/testing/cross_backend/capabilities.d.ts +31 -0
  137. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  138. package/dist/testing/cross_backend/capabilities.js +3 -0
  139. package/dist/testing/cross_backend/cell_cross_helpers.d.ts +39 -0
  140. package/dist/testing/cross_backend/cell_cross_helpers.d.ts.map +1 -0
  141. package/dist/testing/cross_backend/cell_cross_helpers.js +45 -0
  142. package/dist/testing/cross_backend/cell_crud.d.ts +4 -0
  143. package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -0
  144. package/dist/testing/cross_backend/cell_crud.js +168 -0
  145. package/dist/testing/cross_backend/cell_relations.d.ts +4 -0
  146. package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -0
  147. package/dist/testing/cross_backend/cell_relations.js +229 -0
  148. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
  149. package/dist/testing/cross_backend/default_backend_configs.js +6 -0
  150. package/dist/testing/cross_backend/setup.d.ts.map +1 -1
  151. package/dist/testing/cross_backend/setup.js +5 -0
  152. package/dist/testing/entities.d.ts.map +1 -1
  153. package/dist/testing/entities.js +4 -0
  154. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  155. package/dist/testing/ws_round_trip.js +4 -0
  156. package/dist/ui/AdminAccounts.svelte +58 -0
  157. package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
  158. package/dist/ui/admin_accounts_state.svelte.d.ts +30 -2
  159. package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
  160. package/dist/ui/admin_accounts_state.svelte.js +45 -1
  161. package/dist/ui/admin_rpc_adapters.d.ts +6 -2
  162. package/dist/ui/admin_rpc_adapters.d.ts.map +1 -1
  163. package/dist/ui/admin_rpc_adapters.js +5 -1
  164. package/package.json +2 -2
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Raw queries against the `cell_grant` table.
3
+ *
4
+ * Resource-side ACL for cells: each row admits a principal at a `level`
5
+ * (`viewer` | `editor`). Principal is discriminated by which columns are
6
+ * set — `actor_id` (single actor) xor `(role, scope_id?)` (any holder
7
+ * of a matching role_grant). Owner is implicit on `cell.created_by` and never
8
+ * appears in this table.
9
+ *
10
+ * Convention: `deps: QueryDeps` first, no audit side effects, mutations
11
+ * return the affected row (or `null` for not-found).
12
+ *
13
+ * `query_cell_grant_create` upserts on the relevant partial unique index so
14
+ * re-granting the same principal updates `level` rather than producing
15
+ * duplicate rows. The two principal shapes use different indexes:
16
+ *
17
+ * - Actor-shaped: `idx_cell_grant_unique_actor` on `(cell_id, actor_id)`.
18
+ * - Role-shaped: `idx_cell_grant_unique_role_scope` on `(cell_id, role, scope_id)`
19
+ * with `NULLS NOT DISTINCT` so two `(role, NULL)` grants on the same cell
20
+ * collide.
21
+ *
22
+ * @module
23
+ */
24
+ import { assert_row } from './assert_row.js';
25
+ /**
26
+ * Insert a grant, or update the existing row's `level` + `granted_by` when
27
+ * one already exists for the same `(cell_id, principal)` pair.
28
+ *
29
+ * Idempotent re-share: caller doesn't need to check existence first. The
30
+ * UPSERT path runs even when the existing row's level matches — handlers
31
+ * reading the row's prior state for audit ("create vs. update") must do
32
+ * so before this call.
33
+ *
34
+ * @param deps - query deps
35
+ * @param input - cell, level, principal, grantor
36
+ * @returns the inserted-or-updated row
37
+ * @mutates `cell_grant` - inserts or updates one row
38
+ */
39
+ export const query_cell_grant_create = async (deps, input) => {
40
+ const { cell_id, level, principal, granted_by } = input;
41
+ if (principal.kind === 'actor') {
42
+ const row = await deps.db.query_one(`INSERT INTO cell_grant (cell_id, level, actor_id, granted_by)
43
+ VALUES ($1, $2, $3, $4)
44
+ ON CONFLICT (cell_id, actor_id) WHERE actor_id IS NOT NULL
45
+ DO UPDATE SET level = EXCLUDED.level, granted_by = EXCLUDED.granted_by
46
+ RETURNING *`, [cell_id, level, principal.actor_id, granted_by]);
47
+ return assert_row(row, 'INSERT INTO cell_grant (actor)');
48
+ }
49
+ const row = await deps.db.query_one(`INSERT INTO cell_grant (cell_id, level, role, scope_id, granted_by)
50
+ VALUES ($1, $2, $3, $4, $5)
51
+ ON CONFLICT (cell_id, role, scope_id) WHERE role IS NOT NULL
52
+ DO UPDATE SET level = EXCLUDED.level, granted_by = EXCLUDED.granted_by
53
+ RETURNING *`, [cell_id, level, principal.role, principal.scope_id, granted_by]);
54
+ return assert_row(row, 'INSERT INTO cell_grant (role)');
55
+ };
56
+ /**
57
+ * Fetch a grant by id.
58
+ *
59
+ * @param deps - query deps
60
+ * @param grant_id - grant id
61
+ * @returns the row or `null` when not found
62
+ */
63
+ 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
+ ]);
67
+ return row ?? null;
68
+ };
69
+ /**
70
+ * Delete a grant by id, returning the deleted row.
71
+ *
72
+ * Returning the row lets the caller audit the principal + level after the
73
+ * delete and (for self-revoke) recompute `still_admitted` against the
74
+ * remaining grants on the cell without a second fetch.
75
+ *
76
+ * @param deps - query deps
77
+ * @param grant_id - grant id
78
+ * @returns the deleted row or `null` when no row matched
79
+ * @mutates `cell_grant` - deletes one row
80
+ */
81
+ 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]);
83
+ return row ?? null;
84
+ };
85
+ /**
86
+ * List all grants on a cell, oldest first.
87
+ *
88
+ * Used by `cell_grant_list` (RPC) and by handlers that need grants
89
+ * alongside the cell row for the authorize predicate.
90
+ *
91
+ * @param deps - query deps
92
+ * @param cell_id - cell id
93
+ * @returns matching rows
94
+ */
95
+ export const query_cell_grant_list_for_cell = async (deps, cell_id) => deps.db.query(`SELECT * FROM cell_grant
96
+ WHERE cell_id = $1
97
+ ORDER BY created_at ASC`, [cell_id]);
98
+ /**
99
+ * List all grants across a set of cells, ordered by cell then creation.
100
+ * Used by the strict relation-read filter to test `can_view_cell` per
101
+ * target in memory — the caller groups the flat result by `cell_id`.
102
+ * Returns **every** grant on each cell (not caller-filtered), because
103
+ * `can_view_cell` needs the full grant list to decide admission.
104
+ *
105
+ * @param deps - query deps
106
+ * @param cell_ids - cells to fetch grants for (duplicates are harmless)
107
+ * @returns matching grant rows (group by `cell_id` caller-side)
108
+ */
109
+ export const query_cell_grant_list_for_cells = async (deps, cell_ids) => {
110
+ if (cell_ids.length === 0)
111
+ return [];
112
+ return deps.db.query(`SELECT * FROM cell_grant
113
+ WHERE cell_id = ANY($1::uuid[])
114
+ ORDER BY cell_id, created_at ASC`, [cell_ids]);
115
+ };
116
+ /**
117
+ * Load grants that admit the caller (by actor or role-scoped role_grants) across
118
+ * multiple cells. Used to enrich `cell_list` responses with context about what
119
+ * granted access. Returns grants for the given cells that match the caller's
120
+ * identity or role_grant set.
121
+ *
122
+ * @param cell_ids - cells to fetch grants for
123
+ * @param caller_actor_id - actor id of the caller (null for unauth)
124
+ * @param role_grant_roles - active role_grant roles (parallel array)
125
+ * @param role_grant_scope_ids - active role_grant scope ids (parallel array, parallel to roles)
126
+ * @returns matching grants (may include grants the caller doesn't match; caller's
127
+ * list handler must filter when returning to the API)
128
+ */
129
+ export const query_cell_grants_for_caller_in_cells = async (deps, cell_ids, caller_actor_id, role_grant_roles, role_grant_scope_ids) => {
130
+ if (cell_ids.length === 0) {
131
+ return [];
132
+ }
133
+ return deps.db.query(`SELECT g.* FROM cell_grant g
134
+ WHERE g.cell_id = ANY($1::uuid[])
135
+ AND (
136
+ g.actor_id = $2
137
+ OR EXISTS (
138
+ SELECT 1
139
+ FROM unnest($3::text[], $4::uuid[]) AS p(role, scope_id)
140
+ WHERE g.role = p.role
141
+ AND (g.scope_id IS NULL OR g.scope_id IS NOT DISTINCT FROM p.scope_id)
142
+ )
143
+ )
144
+ ORDER BY g.cell_id, g.created_at ASC`, [cell_ids, caller_actor_id, role_grant_roles, role_grant_scope_ids]);
145
+ };
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Cell history PG schema (schema only).
3
+ *
4
+ * Lightweight references to cell state snapshots. Heavy data (serialized
5
+ * cell bytes) lives in the fact store; `fact_hash` points there. The
6
+ * snapshot lifecycle (when to serialize, hash, store, and record) is
7
+ * deferred to a future iteration — this only stages the table so
8
+ * downstream code can target a stable schema. The table ships
9
+ * present-but-unwritten.
10
+ *
11
+ * `fact_hash` is intentionally **not** a foreign key to `facts(hash)` —
12
+ * snapshots may be evicted by GC policy while history rows remain as audit
13
+ * traces, and federation may target facts on another instance.
14
+ *
15
+ * Depends on `CELL_MIGRATION_NS` (FK on `cell.id`).
16
+ *
17
+ * @module
18
+ */
19
+ import type { Migration, MigrationNamespace } from './migrate.js';
20
+ /** `cell_history` table — append-only log of cell snapshot references. */
21
+ export declare const CELL_HISTORY_SCHEMA = "\nCREATE TABLE IF NOT EXISTS cell_history (\n\tid BIGSERIAL PRIMARY KEY,\n\tcell_id UUID NOT NULL REFERENCES cell(id) ON DELETE CASCADE,\n\tfact_hash TEXT NOT NULL,\n\taction_id UUID,\n\tcreated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n)";
22
+ /**
23
+ * Cell-history indexes.
24
+ *
25
+ * - `idx_cell_history_cell`: per-cell timeline reads (newest first).
26
+ * - `idx_cell_history_fact`: fact → cells reverse lookup for GC liveness
27
+ * and provenance queries.
28
+ */
29
+ export declare const CELL_HISTORY_INDEXES: Array<string>;
30
+ /** Tables created by `CELL_HISTORY_MIGRATION_NS`, in drop order. */
31
+ export declare const CELL_HISTORY_DROP_TABLES: readonly ["cell_history"];
32
+ /** Cell-history migrations. */
33
+ export declare const CELL_HISTORY_MIGRATIONS: Array<Migration>;
34
+ /** Namespace identifier for cell-history migrations. */
35
+ export declare const CELL_HISTORY_MIGRATION_NAMESPACE = "fuz_cell_history";
36
+ /** Migration namespace consumed by `run_migrations`. */
37
+ export declare const CELL_HISTORY_MIGRATION_NS: MigrationNamespace;
38
+ //# sourceMappingURL=cell_history_ddl.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cell_history_ddl.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_history_ddl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,KAAK,EAAC,SAAS,EAAE,kBAAkB,EAAC,MAAM,cAAc,CAAC;AAEhE,0EAA0E;AAC1E,eAAO,MAAM,mBAAmB,gPAO9B,CAAC;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,EAAE,KAAK,CAAC,MAAM,CAK9C,CAAC;AAEF,oEAAoE;AACpE,eAAO,MAAM,wBAAwB,2BAA4B,CAAC;AAElE,+BAA+B;AAC/B,eAAO,MAAM,uBAAuB,EAAE,KAAK,CAAC,SAAS,CAUpD,CAAC;AAEF,wDAAwD;AACxD,eAAO,MAAM,gCAAgC,qBAAqB,CAAC;AAEnE,wDAAwD;AACxD,eAAO,MAAM,yBAAyB,EAAE,kBAGvC,CAAC"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Cell history PG schema (schema only).
3
+ *
4
+ * Lightweight references to cell state snapshots. Heavy data (serialized
5
+ * cell bytes) lives in the fact store; `fact_hash` points there. The
6
+ * snapshot lifecycle (when to serialize, hash, store, and record) is
7
+ * deferred to a future iteration — this only stages the table so
8
+ * downstream code can target a stable schema. The table ships
9
+ * present-but-unwritten.
10
+ *
11
+ * `fact_hash` is intentionally **not** a foreign key to `facts(hash)` —
12
+ * snapshots may be evicted by GC policy while history rows remain as audit
13
+ * traces, and federation may target facts on another instance.
14
+ *
15
+ * Depends on `CELL_MIGRATION_NS` (FK on `cell.id`).
16
+ *
17
+ * @module
18
+ */
19
+ /** `cell_history` table — append-only log of cell snapshot references. */
20
+ export const CELL_HISTORY_SCHEMA = `
21
+ CREATE TABLE IF NOT EXISTS cell_history (
22
+ id BIGSERIAL PRIMARY KEY,
23
+ cell_id UUID NOT NULL REFERENCES cell(id) ON DELETE CASCADE,
24
+ fact_hash TEXT NOT NULL,
25
+ action_id UUID,
26
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
27
+ )`;
28
+ /**
29
+ * Cell-history indexes.
30
+ *
31
+ * - `idx_cell_history_cell`: per-cell timeline reads (newest first).
32
+ * - `idx_cell_history_fact`: fact → cells reverse lookup for GC liveness
33
+ * and provenance queries.
34
+ */
35
+ export const CELL_HISTORY_INDEXES = [
36
+ `CREATE INDEX IF NOT EXISTS idx_cell_history_cell
37
+ ON cell_history(cell_id, created_at DESC)`,
38
+ `CREATE INDEX IF NOT EXISTS idx_cell_history_fact
39
+ ON cell_history(fact_hash)`,
40
+ ];
41
+ /** Tables created by `CELL_HISTORY_MIGRATION_NS`, in drop order. */
42
+ export const CELL_HISTORY_DROP_TABLES = ['cell_history'];
43
+ /** Cell-history migrations. */
44
+ export const CELL_HISTORY_MIGRATIONS = [
45
+ {
46
+ name: 'cell_history_v0',
47
+ up: async (db) => {
48
+ await db.query(CELL_HISTORY_SCHEMA);
49
+ for (const sql of CELL_HISTORY_INDEXES) {
50
+ await db.query(sql);
51
+ }
52
+ },
53
+ },
54
+ ];
55
+ /** Namespace identifier for cell-history migrations. */
56
+ export const CELL_HISTORY_MIGRATION_NAMESPACE = 'fuz_cell_history';
57
+ /** Migration namespace consumed by `run_migrations`. */
58
+ export const CELL_HISTORY_MIGRATION_NS = {
59
+ namespace: CELL_HISTORY_MIGRATION_NAMESPACE,
60
+ migrations: CELL_HISTORY_MIGRATIONS,
61
+ };
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Raw queries against the `cell_item` table.
3
+ *
4
+ * Ordered-child membership: each row is `(parent_id, position) → child_id`.
5
+ * `position` is opaque text (fractional-indexing key) — lex ordering is
6
+ * the contract. The PK on `(parent_id, position)` enforces one cell per
7
+ * slot but allows the same `child_id` to appear at multiple positions
8
+ * (the primitive is JSON-array-shaped — ordered multiset, not set;
9
+ * domain dedup rules ride on top).
10
+ *
11
+ * Reads filter both endpoints by `cell.deleted_at IS NULL` so items
12
+ * dangling off a soft-deleted cell don't surface.
13
+ *
14
+ * `query_cell_item_insert` returns the inserted row OR throws the
15
+ * underlying `23505` (Postgres unique violation) on a `(parent_id,
16
+ * position)` collision. Handlers convert this into the
17
+ * `cell_item_position_taken` JSON-RPC error so the client retries with a
18
+ * refreshed bracket.
19
+ *
20
+ * @module
21
+ */
22
+ import type { QueryDeps } from './query_deps.js';
23
+ import type { Uuid } from '@fuzdev/fuz_util/id.js';
24
+ /** Row shape returned by `cell_item` SELECTs. */
25
+ export interface CellItemRow {
26
+ parent_id: Uuid;
27
+ position: string;
28
+ child_id: Uuid;
29
+ created_at: Date;
30
+ }
31
+ /** Input for `query_cell_item_insert`. */
32
+ export interface CellItemInsertQueryInput {
33
+ parent_id: Uuid;
34
+ position: string;
35
+ child_id: Uuid;
36
+ }
37
+ /**
38
+ * Insert one item row at the caller-supplied `position`.
39
+ *
40
+ * Throws on `(parent_id, position)` collision (Postgres `23505`); handler
41
+ * callers detect via `is_pg_unique_violation` and surface as
42
+ * `cell_item_position_taken`. Helper-side jitter (`fractional_index`)
43
+ * makes the collision rate negligible at realistic UX concurrency, so
44
+ * the throw is the cold-path safety net, not the hot path.
45
+ *
46
+ * @mutates `cell_item` - inserts one row
47
+ */
48
+ export declare const query_cell_item_insert: (deps: QueryDeps, input: CellItemInsertQueryInput) => Promise<CellItemRow>;
49
+ /**
50
+ * Fetch one item row by `(parent_id, position)`. Used by move + delete
51
+ * handlers to confirm the row exists before issuing the mutation.
52
+ *
53
+ * @returns the row or `null` when not found
54
+ */
55
+ export declare const query_cell_item_get: (deps: QueryDeps, parent_id: Uuid, position: string) => Promise<CellItemRow | null>;
56
+ /**
57
+ * Move an item row from `position_old` to `position_new` (same parent).
58
+ *
59
+ * Implemented as an UPDATE on the PK; throws `23505` on collision with
60
+ * an existing row at `position_new` so handlers can surface
61
+ * `cell_item_position_taken`. The caller-supplied `position_new` is what
62
+ * fractional-indexing produced for the new slot — collisions are rare
63
+ * but the error path keeps the client truthful.
64
+ *
65
+ * @returns the updated row, or `null` when the source row was missing
66
+ * (raced with a deleter)
67
+ * @mutates `cell_item` - updates one row's `position`
68
+ */
69
+ export declare const query_cell_item_move: (deps: QueryDeps, parent_id: Uuid, position_old: string, position_new: string) => Promise<CellItemRow | null>;
70
+ /**
71
+ * Delete one item row by `(parent_id, position)`. Returns the deleted
72
+ * row so callers can audit `child_id` after the delete without a
73
+ * pre-fetch.
74
+ *
75
+ * @returns the deleted row, or `null` when nothing matched
76
+ * @mutates `cell_item` - deletes one row
77
+ */
78
+ export declare const query_cell_item_delete: (deps: QueryDeps, parent_id: Uuid, position: string) => Promise<CellItemRow | null>;
79
+ /**
80
+ * Forward items list (`parent.items[]`), ordered by lex `position`.
81
+ *
82
+ * Filters child by `deleted_at IS NULL` so items pointing at tombstoned
83
+ * cells don't surface; the parent filter is the caller's responsibility
84
+ * (gated upstream by `can_view_cell(parent)`).
85
+ *
86
+ * @param limit - optional row cap (passes through to SQL `LIMIT`)
87
+ */
88
+ export declare const query_cell_item_list_for_parent: (deps: QueryDeps, parent_id: Uuid, options?: {
89
+ limit?: number;
90
+ position_after?: string;
91
+ }) => Promise<Array<CellItemRow>>;
92
+ /**
93
+ * Reverse items list (`child.lists[]`).
94
+ *
95
+ * Returns rows whose `child_id = $1`, joined to `cell` on `parent_id` so
96
+ * items from tombstoned parents don't surface. The caller-side authz
97
+ * filter (per-parent `can_view_cell`) runs after the SQL fetch — see
98
+ * the 2-layer authz contract on `cell_item_list({child_id})`.
99
+ *
100
+ * Bounded by `limit` (the wire `cell_item_list` cap) so a heavily
101
+ * inbound-linked child can't force an unbounded fetch + per-parent authz
102
+ * pass on the public, IP-rate-limited reverse endpoint.
103
+ */
104
+ export declare const query_cell_item_list_for_child: (deps: QueryDeps, child_id: Uuid, options?: {
105
+ limit?: number;
106
+ }) => Promise<Array<CellItemRow>>;
107
+ //# sourceMappingURL=cell_item_queries.d.ts.map
@@ -0,0 +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,EAAC,SAAS,EAAC,MAAM,iBAAiB,CAAC;AAC/C,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,wBAAwB,CAAC;AAGjD,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;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CAAC,KACjD,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAa5B,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,8BAA8B,GAC1C,MAAM,SAAS,EACf,UAAU,IAAI,EACd,UAAU;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAC,KACxB,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAS3B,CAAC"}
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Raw queries against the `cell_item` table.
3
+ *
4
+ * Ordered-child membership: each row is `(parent_id, position) → child_id`.
5
+ * `position` is opaque text (fractional-indexing key) — lex ordering is
6
+ * the contract. The PK on `(parent_id, position)` enforces one cell per
7
+ * slot but allows the same `child_id` to appear at multiple positions
8
+ * (the primitive is JSON-array-shaped — ordered multiset, not set;
9
+ * domain dedup rules ride on top).
10
+ *
11
+ * Reads filter both endpoints by `cell.deleted_at IS NULL` so items
12
+ * dangling off a soft-deleted cell don't surface.
13
+ *
14
+ * `query_cell_item_insert` returns the inserted row OR throws the
15
+ * underlying `23505` (Postgres unique violation) on a `(parent_id,
16
+ * position)` collision. Handlers convert this into the
17
+ * `cell_item_position_taken` JSON-RPC error so the client retries with a
18
+ * refreshed bracket.
19
+ *
20
+ * @module
21
+ */
22
+ import { assert_row } from './assert_row.js';
23
+ /**
24
+ * Insert one item row at the caller-supplied `position`.
25
+ *
26
+ * Throws on `(parent_id, position)` collision (Postgres `23505`); handler
27
+ * callers detect via `is_pg_unique_violation` and surface as
28
+ * `cell_item_position_taken`. Helper-side jitter (`fractional_index`)
29
+ * makes the collision rate negligible at realistic UX concurrency, so
30
+ * the throw is the cold-path safety net, not the hot path.
31
+ *
32
+ * @mutates `cell_item` - inserts one row
33
+ */
34
+ export const query_cell_item_insert = async (deps, input) => {
35
+ const row = await deps.db.query_one(`INSERT INTO cell_item (parent_id, position, child_id)
36
+ VALUES ($1, $2, $3)
37
+ RETURNING *`, [input.parent_id, input.position, input.child_id]);
38
+ return assert_row(row, 'INSERT INTO cell_item');
39
+ };
40
+ /**
41
+ * Fetch one item row by `(parent_id, position)`. Used by move + delete
42
+ * handlers to confirm the row exists before issuing the mutation.
43
+ *
44
+ * @returns the row or `null` when not found
45
+ */
46
+ 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]);
48
+ return row ?? null;
49
+ };
50
+ /**
51
+ * Move an item row from `position_old` to `position_new` (same parent).
52
+ *
53
+ * Implemented as an UPDATE on the PK; throws `23505` on collision with
54
+ * an existing row at `position_new` so handlers can surface
55
+ * `cell_item_position_taken`. The caller-supplied `position_new` is what
56
+ * fractional-indexing produced for the new slot — collisions are rare
57
+ * but the error path keeps the client truthful.
58
+ *
59
+ * @returns the updated row, or `null` when the source row was missing
60
+ * (raced with a deleter)
61
+ * @mutates `cell_item` - updates one row's `position`
62
+ */
63
+ export const query_cell_item_move = async (deps, parent_id, position_old, position_new) => {
64
+ const row = await deps.db.query_one(`UPDATE cell_item
65
+ SET position = $3
66
+ WHERE parent_id = $1 AND position = $2
67
+ RETURNING *`, [parent_id, position_old, position_new]);
68
+ return row ?? null;
69
+ };
70
+ /**
71
+ * Delete one item row by `(parent_id, position)`. Returns the deleted
72
+ * row so callers can audit `child_id` after the delete without a
73
+ * pre-fetch.
74
+ *
75
+ * @returns the deleted row, or `null` when nothing matched
76
+ * @mutates `cell_item` - deletes one row
77
+ */
78
+ 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]);
80
+ return row ?? null;
81
+ };
82
+ /**
83
+ * Forward items list (`parent.items[]`), ordered by lex `position`.
84
+ *
85
+ * Filters child by `deleted_at IS NULL` so items pointing at tombstoned
86
+ * cells don't surface; the parent filter is the caller's responsibility
87
+ * (gated upstream by `can_view_cell(parent)`).
88
+ *
89
+ * @param limit - optional row cap (passes through to SQL `LIMIT`)
90
+ */
91
+ export const query_cell_item_list_for_parent = async (deps, parent_id, options) => {
92
+ const limit = options?.limit ?? null;
93
+ const position_after = options?.position_after ?? null;
94
+ return deps.db.query(`SELECT i.* FROM cell_item i
95
+ JOIN cell c ON c.id = i.child_id
96
+ WHERE i.parent_id = $1
97
+ AND c.deleted_at IS NULL
98
+ AND ($3::text IS NULL OR i.position > $3)
99
+ ORDER BY i.position ASC
100
+ LIMIT $2`, [parent_id, limit, position_after]);
101
+ };
102
+ /**
103
+ * Reverse items list (`child.lists[]`).
104
+ *
105
+ * Returns rows whose `child_id = $1`, joined to `cell` on `parent_id` so
106
+ * items from tombstoned parents don't surface. The caller-side authz
107
+ * filter (per-parent `can_view_cell`) runs after the SQL fetch — see
108
+ * the 2-layer authz contract on `cell_item_list({child_id})`.
109
+ *
110
+ * Bounded by `limit` (the wire `cell_item_list` cap) so a heavily
111
+ * inbound-linked child can't force an unbounded fetch + per-parent authz
112
+ * pass on the public, IP-rate-limited reverse endpoint.
113
+ */
114
+ export const query_cell_item_list_for_child = async (deps, child_id, options) => deps.db.query(`SELECT i.* FROM cell_item i
115
+ JOIN cell p ON p.id = i.parent_id
116
+ WHERE i.child_id = $1
117
+ AND p.deleted_at IS NULL
118
+ ORDER BY i.created_at ASC
119
+ LIMIT $2`, [child_id, options?.limit ?? null]);