@catalyst-cloud/schema 0.1.1 → 0.1.3
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 +3 -7
- package/src/index.ts +122 -0
- package/{dist/migrate.js → src/migrate.ts} +90 -47
- package/src/migrations.generated.ts +99 -0
- package/{dist/mirror.js → src/mirror.ts} +155 -88
- package/dist/index.d.ts +0 -6782
- package/dist/index.js +0 -72
- package/dist/migrate.d.ts +0 -36
- package/dist/migrations.generated.d.ts +0 -65
- package/dist/migrations.generated.js +0 -81
- package/dist/mirror.d.ts +0 -3517
|
@@ -15,14 +15,22 @@
|
|
|
15
15
|
// • `relations` join on issues.identifier (issue_identifier), NOT issues.id.
|
|
16
16
|
// • `check_runs` join a PR by head_sha, NOT PR number.
|
|
17
17
|
// • PR mergeable / mergeable_state are nullable until a reconcile resolves them.
|
|
18
|
+
|
|
18
19
|
import { sqliteTable, text, integer, real, primaryKey, index } from "drizzle-orm/sqlite-core";
|
|
20
|
+
|
|
19
21
|
// ── Linear ───────────────────────────────────────────────────────────────────────────────────────
|
|
20
|
-
|
|
22
|
+
|
|
23
|
+
export const issues = sqliteTable(
|
|
24
|
+
"issues",
|
|
25
|
+
{
|
|
21
26
|
id: text("id").primaryKey(),
|
|
22
27
|
identifier: text("identifier"),
|
|
23
28
|
title: text("title"),
|
|
24
29
|
description: text("description"),
|
|
25
30
|
state: text("state"),
|
|
31
|
+
// CTC-148: the workflow state id behind the `state` NAME — lets a consumer key on state without
|
|
32
|
+
// string-matching the renameable name.
|
|
33
|
+
state_id: text("state_id"),
|
|
26
34
|
assignee: text("assignee"),
|
|
27
35
|
// CTC-USERS: the assignee's user id (FK → users.id), so the row JOINs to a name + avatar. The
|
|
28
36
|
// legacy `assignee` name string is KEPT for back-compat (the SSE list row still carries it).
|
|
@@ -32,6 +40,10 @@ export const issues = sqliteTable("issues", {
|
|
|
32
40
|
project_id: text("project_id"),
|
|
33
41
|
cycle_id: text("cycle_id"),
|
|
34
42
|
team_id: text("team_id"),
|
|
43
|
+
// CTC-148: denormalized team key + name (off the issue's `team` ref) so a row labels its team
|
|
44
|
+
// ("ENG / Engineering") without a teams lookup. team_id is KEPT.
|
|
45
|
+
team_key: text("team_key"),
|
|
46
|
+
team_name: text("team_name"),
|
|
35
47
|
delegate_id: text("delegate_id"),
|
|
36
48
|
delegate_name: text("delegate_name"),
|
|
37
49
|
bot_actor_name: text("bot_actor_name"),
|
|
@@ -57,7 +69,8 @@ export const issues = sqliteTable("issues", {
|
|
|
57
69
|
archived_at: integer("archived_at"),
|
|
58
70
|
removed_at: integer("removed_at"),
|
|
59
71
|
raw: text("raw"),
|
|
60
|
-
},
|
|
72
|
+
},
|
|
73
|
+
(t) => [
|
|
61
74
|
index("idx_issues_team").on(t.team_id),
|
|
62
75
|
// CTC-59: the list query — `WHERE removed_at IS NULL ORDER BY updated_at DESC` (buildIssuesView).
|
|
63
76
|
// The composite lets SQLite seek the live rows then walk updated_at in order (no temp b-tree).
|
|
@@ -68,14 +81,17 @@ export const issues = sqliteTable("issues", {
|
|
|
68
81
|
// Drizzle SSOT ONLY (NOT schema.sql) — migrate.ts swallows a duplicate CREATE INDEX on replay
|
|
69
82
|
// (CTC-59), so the generated `CREATE INDEX` is replay-safe; schema.sql would break the adoption test.
|
|
70
83
|
index("idx_issues_sort_order").on(t.sort_order),
|
|
71
|
-
]
|
|
84
|
+
],
|
|
85
|
+
);
|
|
86
|
+
|
|
72
87
|
export const labels = sqliteTable("labels", {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
88
|
+
id: text("id").primaryKey(),
|
|
89
|
+
name: text("name"),
|
|
90
|
+
color: text("color"),
|
|
91
|
+
updated_at: integer("updated_at"),
|
|
92
|
+
removed_at: integer("removed_at"),
|
|
78
93
|
});
|
|
94
|
+
|
|
79
95
|
// Teammate identities (CTC-USERS): resolves issue assignees + delegate AGENTS (and comment/PR
|
|
80
96
|
// authors/reviewers) to a name + avatar so the UI never renders a gray "Unknown" chip. AGENT
|
|
81
97
|
// identities are first-class: `type` is human | agent | bot | app. Populated three ways — a ROSTER
|
|
@@ -86,27 +102,38 @@ export const labels = sqliteTable("labels", {
|
|
|
86
102
|
// in place, never tombstoned (an avatar/name that goes missing just stops being referenced). `id` is
|
|
87
103
|
// the source-native id: Linear's user node id, the bot actor id, or a `github:<login>` synthetic id.
|
|
88
104
|
export const users = sqliteTable("users", {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
105
|
+
id: text("id").primaryKey(),
|
|
106
|
+
name: text("name"),
|
|
107
|
+
display_name: text("display_name"),
|
|
108
|
+
avatar_url: text("avatar_url"),
|
|
109
|
+
type: text("type"), // human | agent | bot | app
|
|
110
|
+
source: text("source"), // linear | github
|
|
111
|
+
updated_at: integer("updated_at"),
|
|
96
112
|
});
|
|
97
|
-
|
|
113
|
+
|
|
114
|
+
export const issue_labels = sqliteTable(
|
|
115
|
+
"issue_labels",
|
|
116
|
+
{
|
|
98
117
|
issue_id: text("issue_id").notNull(),
|
|
99
118
|
label_id: text("label_id").notNull(),
|
|
100
|
-
},
|
|
119
|
+
},
|
|
120
|
+
(t) => [primaryKey({ columns: [t.issue_id, t.label_id] })],
|
|
121
|
+
);
|
|
122
|
+
|
|
101
123
|
// FULL-poll-and-diff every reconcile pass (Linear fires NO issueRelations webhooks). Delete-absent is
|
|
102
124
|
// load-bearing — and there is intentionally NO removed_at: a stale edge is HARD-deleted, never soft.
|
|
103
|
-
export const relations = sqliteTable(
|
|
125
|
+
export const relations = sqliteTable(
|
|
126
|
+
"relations",
|
|
127
|
+
{
|
|
104
128
|
id: text("id").primaryKey(),
|
|
105
129
|
type: text("type"), // blocks | duplicate | related
|
|
106
130
|
issue_identifier: text("issue_identifier"),
|
|
107
131
|
related_identifier: text("related_identifier"),
|
|
108
132
|
updated_at: integer("updated_at"),
|
|
109
|
-
},
|
|
133
|
+
},
|
|
134
|
+
(t) => [index("idx_relations_issue").on(t.issue_identifier)],
|
|
135
|
+
);
|
|
136
|
+
|
|
110
137
|
// Linear IssueHistory events (CTC-14) — the per-issue change timeline. Captured INLINE on the issues
|
|
111
138
|
// delta sweep (a nested history(last:100) connection), NOT a separate leg and NOT a webhook path. Each
|
|
112
139
|
// node is IMMUTABLE with a stable `id`; HARD upsert keyed on `id` (NO removed_at — a history row never
|
|
@@ -115,7 +142,9 @@ export const relations = sqliteTable("relations", {
|
|
|
115
142
|
// name — no states table). from_due_date/to_due_date are TimelessDate "YYYY-MM-DD" stored AS-IS as
|
|
116
143
|
// TEXT (the trap), never a timestamp. added_label_ids/removed_label_ids are JSON array strings; the
|
|
117
144
|
// boolean columns are the mirror's 0/1 ints. raw is the full node JSON (full fidelity / forward-compat).
|
|
118
|
-
export const issue_history = sqliteTable(
|
|
145
|
+
export const issue_history = sqliteTable(
|
|
146
|
+
"issue_history",
|
|
147
|
+
{
|
|
119
148
|
id: text("id").primaryKey(),
|
|
120
149
|
issue_id: text("issue_id").notNull(), // FK → issues.id
|
|
121
150
|
actor_id: text("actor_id"), // FK → users.id; nullable (system events have no actor)
|
|
@@ -149,67 +178,79 @@ export const issue_history = sqliteTable("issue_history", {
|
|
|
149
178
|
auto_closed: integer("auto_closed"), // 0/1
|
|
150
179
|
trashed: integer("trashed"), // 0/1
|
|
151
180
|
raw: text("raw"), // JSON.stringify(node) — full fidelity / forward-compat (like issues.raw)
|
|
152
|
-
},
|
|
153
|
-
// The timeline index lives in the Drizzle SSOT ONLY (NOT schema.sql) — migrate.ts makes the generated
|
|
154
|
-
// CREATE INDEX replay-safe (same as the issues read-path indexes); putting it in schema.sql would
|
|
155
|
-
// break the adoption test.
|
|
156
|
-
(t) => [index("idx_issue_history_issue_created").on(t.issue_id, t.created_at)]
|
|
181
|
+
},
|
|
182
|
+
// The timeline index lives in the Drizzle SSOT ONLY (NOT schema.sql) — migrate.ts makes the generated
|
|
183
|
+
// CREATE INDEX replay-safe (same as the issues read-path indexes); putting it in schema.sql would
|
|
184
|
+
// break the adoption test.
|
|
185
|
+
(t) => [index("idx_issue_history_issue_created").on(t.issue_id, t.created_at)],
|
|
186
|
+
);
|
|
187
|
+
|
|
157
188
|
// CTC-HIERARCHY enriches the project row with the lead (FK → users.id), description, lifecycle dates,
|
|
158
189
|
// progress, health, and presentation (icon/color). `target_date` / `start_date` are Linear
|
|
159
190
|
// TimelessDates ("YYYY-MM-DD") — stored AS-IS as TEXT, NEVER through isoToMs (the same trap as
|
|
160
191
|
// issues.due_date). `progress` is a Float (0..1).
|
|
161
192
|
export const projects = sqliteTable("projects", {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
193
|
+
id: text("id").primaryKey(),
|
|
194
|
+
name: text("name"),
|
|
195
|
+
state: text("state"),
|
|
196
|
+
description: text("description"),
|
|
197
|
+
lead_id: text("lead_id"), // FK → users.id (CTC-HIERARCHY); upserted from the project lead.
|
|
198
|
+
target_date: text("target_date"), // TimelessDate "YYYY-MM-DD" — TEXT AS-IS (the trap), not a ts.
|
|
199
|
+
start_date: text("start_date"), // TimelessDate "YYYY-MM-DD" — TEXT AS-IS (the trap), not a ts.
|
|
200
|
+
progress: real("progress"),
|
|
201
|
+
health: text("health"), // onTrack | atRisk | offTrack (null until a project update reports it).
|
|
202
|
+
icon: text("icon"),
|
|
203
|
+
color: text("color"),
|
|
204
|
+
updated_at: integer("updated_at"),
|
|
205
|
+
removed_at: integer("removed_at"),
|
|
175
206
|
});
|
|
207
|
+
|
|
176
208
|
// CTC-HIERARCHY enriches the cycle row with its display name + progress + current scope/issue counts.
|
|
177
209
|
// Linear exposes scope/issue counts only as weekly history arrays (completedScopeHistory etc.); we
|
|
178
210
|
// store the CURRENT value (the last array element) as a scalar — not the whole history.
|
|
179
211
|
export const cycles = sqliteTable("cycles", {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
212
|
+
id: text("id").primaryKey(),
|
|
213
|
+
number: integer("number"),
|
|
214
|
+
name: text("name"),
|
|
215
|
+
progress: real("progress"),
|
|
216
|
+
completed_scope: real("completed_scope"), // last(completedScopeHistory) — current completed points.
|
|
217
|
+
scope: real("scope"), // last(scopeHistory) — current total scope points.
|
|
218
|
+
completed_issue_count: integer("completed_issue_count"), // last(completedIssueCountHistory).
|
|
219
|
+
issue_count: integer("issue_count"), // last(issueCountHistory).
|
|
220
|
+
starts_at: integer("starts_at"),
|
|
221
|
+
ends_at: integer("ends_at"),
|
|
222
|
+
updated_at: integer("updated_at"),
|
|
191
223
|
});
|
|
224
|
+
|
|
192
225
|
// Linear initiatives (CTC-HIERARCHY) — the top of the HIERARCHY (initiative → project → cycle →
|
|
193
226
|
// issue). PK `id` (Linear node id). `status` is the InitiativeStatus enum string; `target_date` is a
|
|
194
227
|
// TimelessDate ("YYYY-MM-DD") stored AS-IS as TEXT (the trap). `owner_id` is the FK → users.id.
|
|
195
228
|
// HARD upsert — NO removed_at (a vanished initiative just stops being referenced by the join table).
|
|
196
229
|
export const initiatives = sqliteTable("initiatives", {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
230
|
+
id: text("id").primaryKey(),
|
|
231
|
+
name: text("name"),
|
|
232
|
+
description: text("description"),
|
|
233
|
+
status: text("status"),
|
|
234
|
+
target_date: text("target_date"), // TimelessDate "YYYY-MM-DD" — TEXT AS-IS (the trap), not a ts.
|
|
235
|
+
owner_id: text("owner_id"), // FK → users.id (CTC-HIERARCHY); upserted from the initiative owner.
|
|
236
|
+
updated_at: integer("updated_at"),
|
|
204
237
|
});
|
|
238
|
+
|
|
205
239
|
// project↔initiative join (CTC-HIERARCHY) — Linear models this as many:many (a project belongs to
|
|
206
240
|
// many initiatives), so it's an edge table modeled on issue_labels: composite PK, no updated_at, full
|
|
207
241
|
// poll-and-diff per project leg in the reconcile (Linear fires no membership webhook for it).
|
|
208
|
-
export const project_initiatives = sqliteTable(
|
|
242
|
+
export const project_initiatives = sqliteTable(
|
|
243
|
+
"project_initiatives",
|
|
244
|
+
{
|
|
209
245
|
project_id: text("project_id").notNull(),
|
|
210
246
|
initiative_id: text("initiative_id").notNull(),
|
|
211
|
-
},
|
|
212
|
-
|
|
247
|
+
},
|
|
248
|
+
(t) => [primaryKey({ columns: [t.project_id, t.initiative_id] })],
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
export const comments = sqliteTable(
|
|
252
|
+
"comments",
|
|
253
|
+
{
|
|
213
254
|
id: text("id").primaryKey(),
|
|
214
255
|
issue_id: text("issue_id"),
|
|
215
256
|
body: text("body"),
|
|
@@ -222,12 +263,17 @@ export const comments = sqliteTable("comments", {
|
|
|
222
263
|
created_at: integer("created_at"),
|
|
223
264
|
updated_at: integer("updated_at"),
|
|
224
265
|
removed_at: integer("removed_at"),
|
|
225
|
-
},
|
|
226
|
-
// CTC-59: the detail comments fetch — `WHERE issue_id = ? AND removed_at IS NULL ORDER BY updated_at`.
|
|
227
|
-
// Composite covers the filter AND the sort, so SQLite seeks instead of building a temp b-tree.
|
|
228
|
-
(t) => [index("idx_comments_issue").on(t.issue_id, t.removed_at, t.updated_at)]
|
|
266
|
+
},
|
|
267
|
+
// CTC-59: the detail comments fetch — `WHERE issue_id = ? AND removed_at IS NULL ORDER BY updated_at`.
|
|
268
|
+
// Composite covers the filter AND the sort, so SQLite seeks instead of building a temp b-tree.
|
|
269
|
+
(t) => [index("idx_comments_issue").on(t.issue_id, t.removed_at, t.updated_at)],
|
|
270
|
+
);
|
|
271
|
+
|
|
229
272
|
// ── GitHub ───────────────────────────────────────────────────────────────────────────────────────
|
|
230
|
-
|
|
273
|
+
|
|
274
|
+
export const pull_requests = sqliteTable(
|
|
275
|
+
"pull_requests",
|
|
276
|
+
{
|
|
231
277
|
repo_id: text("repo_id").notNull(),
|
|
232
278
|
number: integer("number").notNull(),
|
|
233
279
|
node_id: text("node_id"),
|
|
@@ -254,12 +300,17 @@ export const pull_requests = sqliteTable("pull_requests", {
|
|
|
254
300
|
milestone_title: text("milestone_title"),
|
|
255
301
|
updated_at: integer("updated_at"),
|
|
256
302
|
synced_at: integer("synced_at"),
|
|
257
|
-
},
|
|
303
|
+
},
|
|
304
|
+
(t) => [
|
|
258
305
|
primaryKey({ columns: [t.repo_id, t.number] }),
|
|
259
306
|
// CTC-59: the pulls list — `ORDER BY updated_at DESC` (buildPullsView).
|
|
260
307
|
index("idx_pulls_updated").on(t.updated_at),
|
|
261
|
-
]
|
|
262
|
-
|
|
308
|
+
],
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
export const check_runs = sqliteTable(
|
|
312
|
+
"check_runs",
|
|
313
|
+
{
|
|
263
314
|
repo_id: text("repo_id").notNull(),
|
|
264
315
|
check_run_id: text("check_run_id").primaryKey(),
|
|
265
316
|
head_sha: text("head_sha"),
|
|
@@ -268,41 +319,57 @@ export const check_runs = sqliteTable("check_runs", {
|
|
|
268
319
|
conclusion: text("conclusion"),
|
|
269
320
|
check_suite_id: text("check_suite_id"),
|
|
270
321
|
updated_at: integer("updated_at"),
|
|
271
|
-
},
|
|
272
|
-
|
|
322
|
+
},
|
|
323
|
+
(t) => [index("idx_check_runs_sha").on(t.head_sha)],
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
export const commit_statuses = sqliteTable(
|
|
327
|
+
"commit_statuses",
|
|
328
|
+
{
|
|
273
329
|
repo_id: text("repo_id").notNull(),
|
|
274
330
|
sha: text("sha").notNull(),
|
|
275
331
|
context: text("context").notNull(),
|
|
276
332
|
state: text("state"),
|
|
277
333
|
target_url: text("target_url"),
|
|
278
334
|
updated_at: integer("updated_at"),
|
|
279
|
-
},
|
|
335
|
+
},
|
|
336
|
+
(t) => [primaryKey({ columns: [t.repo_id, t.sha, t.context] })],
|
|
337
|
+
);
|
|
338
|
+
|
|
280
339
|
export const reviews = sqliteTable("reviews", {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
340
|
+
repo_id: text("repo_id").notNull(),
|
|
341
|
+
pr_number: integer("pr_number").notNull(),
|
|
342
|
+
review_id: text("review_id").primaryKey(),
|
|
343
|
+
user_id: text("user_id"),
|
|
344
|
+
state: text("state"),
|
|
345
|
+
submitted_at: integer("submitted_at"),
|
|
287
346
|
});
|
|
347
|
+
|
|
288
348
|
// ── Mirror infra (not domain entities — never ride the change-feed) ─────────────────────────────────
|
|
349
|
+
|
|
289
350
|
export const processed_events = sqliteTable("processed_events", {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
351
|
+
delivery_id: text("delivery_id").primaryKey(),
|
|
352
|
+
source: text("source"),
|
|
353
|
+
event_type: text("event_type"),
|
|
354
|
+
received_at: integer("received_at"),
|
|
294
355
|
});
|
|
356
|
+
|
|
295
357
|
export const change_log = sqliteTable("change_log", {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
358
|
+
seq: integer("seq").primaryKey({ autoIncrement: true }),
|
|
359
|
+
entity: text("entity"),
|
|
360
|
+
entity_id: text("entity_id"),
|
|
361
|
+
op: text("op"), // upsert | delete
|
|
362
|
+
payload: text("payload"),
|
|
363
|
+
ts: integer("ts"),
|
|
302
364
|
});
|
|
303
|
-
|
|
365
|
+
|
|
366
|
+
export const sync_cursors = sqliteTable(
|
|
367
|
+
"sync_cursors",
|
|
368
|
+
{
|
|
304
369
|
source: text("source").notNull(),
|
|
305
370
|
entity: text("entity").notNull(),
|
|
306
371
|
cursor: text("cursor"),
|
|
307
372
|
last_run_at: integer("last_run_at"),
|
|
308
|
-
},
|
|
373
|
+
},
|
|
374
|
+
(t) => [primaryKey({ columns: [t.source, t.entity] })],
|
|
375
|
+
);
|