@catalyst-cloud/schema 0.1.12 → 0.1.14

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@catalyst-cloud/schema",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "type": "module",
5
5
  "description": "Typed Drizzle schema = single source of truth for the per-tenant Mirror DO SQLite store (CTC-13 / ADR-0002). Shared by the mirror Worker, the host-sync replica, and the browser OPFS replica.",
6
6
  "license": "MIT",
package/src/index.ts CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  issue_history,
19
19
  projects,
20
20
  cycles,
21
+ workflow_states,
21
22
  initiatives,
22
23
  project_initiatives,
23
24
  comments,
@@ -40,6 +41,7 @@ import {
40
41
  pr_commits,
41
42
  pr_events,
42
43
  pr_ancillary_backfill,
44
+ push_subscriptions,
43
45
  } from "./mirror.js";
44
46
 
45
47
  export * from "./mirror.js";
@@ -76,6 +78,7 @@ export const mirrorSchema = {
76
78
  issue_history,
77
79
  projects,
78
80
  cycles,
81
+ workflow_states,
79
82
  initiatives,
80
83
  project_initiatives,
81
84
  comments,
@@ -110,6 +113,9 @@ export const mirrorSchema = {
110
113
  pr_commits,
111
114
  pr_events,
112
115
  pr_ancillary_backfill,
116
+ // CTC-619: a member's Web Push subscription. Rides the change feed like any other entity — a
117
+ // member-facing settings UI can reflect subscribe/unsubscribe reactively.
118
+ push_subscriptions,
113
119
  } as const;
114
120
 
115
121
  /**
@@ -136,6 +142,7 @@ export const mirrorEntityTables = {
136
142
  issue_history,
137
143
  projects,
138
144
  cycles,
145
+ workflow_states,
139
146
  initiatives,
140
147
  project_initiatives,
141
148
  comments,
@@ -154,6 +161,8 @@ export const mirrorEntityTables = {
154
161
  pr_conversation_comments,
155
162
  pr_commits,
156
163
  pr_events,
164
+ // CTC-619: a member's Web Push subscription.
165
+ push_subscriptions,
157
166
  } as const;
158
167
 
159
168
  /** The wire `entity` name of a change-feed-carried table (keys of mirrorEntityTables). */
@@ -165,6 +165,20 @@ export const MIRROR_MIGRATIONS = {
165
165
  tag: "0021_worried_annihilus",
166
166
  breakpoints: true,
167
167
  },
168
+ {
169
+ idx: 22,
170
+ version: "6",
171
+ when: 1786928302892,
172
+ tag: "0022_chilly_gorgon",
173
+ breakpoints: true,
174
+ },
175
+ {
176
+ idx: 23,
177
+ version: "6",
178
+ when: 1786935258669,
179
+ tag: "0023_ambiguous_micromax",
180
+ breakpoints: true,
181
+ },
168
182
  ],
169
183
  },
170
184
  migrations: {
@@ -210,5 +224,9 @@ export const MIRROR_MIGRATIONS = {
210
224
  "CREATE TABLE `pr_review_backfill` (\n\t`repo_id` text NOT NULL,\n\t`pr_number` integer NOT NULL,\n\t`backfilled_at` integer NOT NULL,\n\tPRIMARY KEY(`repo_id`, `pr_number`)\n);\n--> statement-breakpoint\nALTER TABLE `reviews` ADD `body` text;",
211
225
  "0021_worried_annihilus":
212
226
  "CREATE TABLE `pr_ancillary_backfill` (\n\t`repo_id` text NOT NULL,\n\t`pr_number` integer NOT NULL,\n\t`backfilled_at` integer NOT NULL,\n\tPRIMARY KEY(`repo_id`, `pr_number`)\n);\n--> statement-breakpoint\nCREATE TABLE `pr_commits` (\n\t`repo_id` text NOT NULL,\n\t`pr_number` integer NOT NULL,\n\t`sha` text NOT NULL,\n\t`message` text,\n\t`author_login` text,\n\t`author_name` text,\n\t`author_email` text,\n\t`authored_at` integer,\n\t`committed_at` integer,\n\tPRIMARY KEY(`repo_id`, `pr_number`, `sha`)\n);\n--> statement-breakpoint\nCREATE TABLE `pr_conversation_comments` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`repo_id` text NOT NULL,\n\t`pr_number` integer NOT NULL,\n\t`author_id` text,\n\t`body` text,\n\t`created_at` integer,\n\t`updated_at` integer,\n\t`removed_at` integer\n);\n--> statement-breakpoint\nCREATE INDEX `idx_pr_conversation_comments_pr` ON `pr_conversation_comments` (`repo_id`,`pr_number`);--> statement-breakpoint\nCREATE TABLE `pr_events` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`repo_id` text NOT NULL,\n\t`pr_number` integer NOT NULL,\n\t`action` text,\n\t`actor_id` text,\n\t`created_at` integer,\n\t`updated_at` integer,\n\t`raw` text\n);\n--> statement-breakpoint\nCREATE INDEX `idx_pr_events_pr` ON `pr_events` (`repo_id`,`pr_number`,`created_at`);--> statement-breakpoint\nCREATE TABLE `pr_review_comments` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`repo_id` text NOT NULL,\n\t`pr_number` integer NOT NULL,\n\t`review_id` text,\n\t`commit_id` text,\n\t`path` text,\n\t`line` integer,\n\t`diff_hunk` text,\n\t`in_reply_to_id` text,\n\t`author_id` text,\n\t`body` text,\n\t`created_at` integer,\n\t`updated_at` integer,\n\t`removed_at` integer\n);\n--> statement-breakpoint\nCREATE INDEX `idx_pr_review_comments_pr` ON `pr_review_comments` (`repo_id`,`pr_number`);",
227
+ "0022_chilly_gorgon":
228
+ "CREATE TABLE `workflow_states` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`team_id` text,\n\t`name` text,\n\t`type` text,\n\t`position` real,\n\t`color` text,\n\t`archived_at` integer,\n\t`updated_at` integer\n);\n--> statement-breakpoint\nCREATE INDEX `idx_workflow_states_team` ON `workflow_states` (`team_id`);--> statement-breakpoint\nALTER TABLE `issues` ADD `state_type` text;--> statement-breakpoint\nALTER TABLE `issues` ADD `state_position` real;",
229
+ "0023_ambiguous_micromax":
230
+ "CREATE TABLE `push_subscriptions` (\n\t`endpoint` text PRIMARY KEY NOT NULL,\n\t`member_id` text NOT NULL,\n\t`p256dh` text NOT NULL,\n\t`auth` text NOT NULL,\n\t`created_at` integer,\n\t`last_used_at` integer\n);\n--> statement-breakpoint\nCREATE INDEX `idx_push_subscriptions_member` ON `push_subscriptions` (`member_id`);",
213
231
  },
214
232
  } as const;
package/src/mirror.ts CHANGED
@@ -39,6 +39,11 @@ export const issues = sqliteTable(
39
39
  // CTC-148: the workflow state id behind the `state` NAME — lets a consumer key on state without
40
40
  // string-matching the renameable name.
41
41
  state_id: text("state_id"),
42
+ // CTC-308: the state's bucket (triage|backlog|unstarted|started|completed|canceled) + its manual
43
+ // column order — denormalized off `state` (the `workflow_states` FK, below) so a board can order
44
+ // its columns without a join on the hot path. Null on a partial webhook, exactly like state_id.
45
+ state_type: text("state_type"),
46
+ state_position: real("state_position"),
42
47
  assignee: text("assignee"),
43
48
  // CTC-USERS: the assignee's user id (FK → users.id), so the row JOINs to a name + avatar. The
44
49
  // legacy `assignee` name string is KEPT for back-compat (the SSE list row still carries it).
@@ -308,6 +313,28 @@ export const cycles = sqliteTable("cycles", {
308
313
  updated_at: integer("updated_at"),
309
314
  });
310
315
 
316
+ // Linear workflow states (CTC-308) — the id/type/position behind an issue's `state` NAME, mirrored as
317
+ // its own entity table so a board can order columns by (type, position) and key a column across teams
318
+ // (the prerequisite CTC-277's kanban board blocks on). Small, workspace-wide set — full sweep each
319
+ // pass, same discipline as `labels`/`cycles`. No `removed_at`: Linear ARCHIVES a workflow state rather
320
+ // than deleting it (`archived_at`), same convention `issues.archived_at` already uses.
321
+ export const workflow_states = sqliteTable(
322
+ "workflow_states",
323
+ {
324
+ id: text("id").primaryKey(),
325
+ team_id: text("team_id"),
326
+ name: text("name"),
327
+ // The state's bucket: triage | backlog | unstarted | started | completed | canceled.
328
+ type: text("type"),
329
+ // Manual sort rank within its team's workflow — a Float, Linear's own column order.
330
+ position: real("position"),
331
+ color: text("color"),
332
+ archived_at: integer("archived_at"),
333
+ updated_at: integer("updated_at"),
334
+ },
335
+ (t) => [index("idx_workflow_states_team").on(t.team_id)],
336
+ );
337
+
311
338
  // Linear initiatives (CTC-HIERARCHY) — the top of the HIERARCHY (initiative → project → cycle →
312
339
  // issue). PK `id` (Linear node id). `status` is the InitiativeStatus enum string; `target_date` is a
313
340
  // TimelessDate ("YYYY-MM-DD") stored AS-IS as TEXT (the trap). `owner_id` is the FK → users.id.
@@ -824,3 +851,25 @@ export const pr_ancillary_backfill = sqliteTable(
824
851
  },
825
852
  (t) => [primaryKey({ columns: [t.repo_id, t.pr_number] })],
826
853
  );
854
+
855
+ // CTC-619 — a member's Web Push subscription (one browser/device). PK is the subscription `endpoint`
856
+ // itself — a push endpoint URL is unique per browser install by construction, so keying on it is what
857
+ // makes "a duplicate subscribe for the same endpoint does not create a second row" (the ticket's own
858
+ // AC) a plain upsert, no synthetic id needed. `member_id` is the Linear user id (FK-shaped to
859
+ // users.id, same as issues.assignee_id) — the ingest trigger's own correlation key. HARD delete on
860
+ // unsubscribe (no removed_at): a stale/revoked push subscription has no historical value, matching
861
+ // `users`' own "HARD-UPSERT, no soft delete" precedent for similarly overwrite-in-place data.
862
+ export const push_subscriptions = sqliteTable(
863
+ "push_subscriptions",
864
+ {
865
+ endpoint: text("endpoint").primaryKey(),
866
+ member_id: text("member_id").notNull(),
867
+ p256dh: text("p256dh").notNull(),
868
+ auth: text("auth").notNull(),
869
+ created_at: integer("created_at"),
870
+ last_used_at: integer("last_used_at"),
871
+ },
872
+ // The ingest trigger's own read pattern: "every subscription for this member" (member_id, NOT the
873
+ // PK), keyed off issues.assignee_id — the same shape idx_workflow_states_team indexes on team_id.
874
+ (t) => [index("idx_push_subscriptions_member").on(t.member_id)],
875
+ );