@mastra/factory 0.26.0-alpha.2 → 0.26.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 (154) hide show
  1. package/README.md +74 -7
  2. package/dist/boards/define-board.d.ts +19 -0
  3. package/dist/boards/define-board.d.ts.map +1 -1
  4. package/dist/boards/define-board.js +41 -9
  5. package/dist/boards/define-board.js.map +1 -1
  6. package/dist/boards/project-boards.d.ts +78 -0
  7. package/dist/boards/project-boards.d.ts.map +1 -0
  8. package/dist/boards/project-boards.js +194 -0
  9. package/dist/boards/project-boards.js.map +1 -0
  10. package/dist/boards/review.js +2 -2
  11. package/dist/boards/review.js.map +1 -1
  12. package/dist/boards/stored-board.d.ts +101 -0
  13. package/dist/boards/stored-board.d.ts.map +1 -0
  14. package/dist/boards/stored-board.js +139 -0
  15. package/dist/boards/stored-board.js.map +1 -0
  16. package/dist/boards/work.d.ts.map +1 -1
  17. package/dist/boards/work.js +6 -3
  18. package/dist/boards/work.js.map +1 -1
  19. package/dist/capabilities/intake.d.ts +6 -0
  20. package/dist/capabilities/intake.d.ts.map +1 -1
  21. package/dist/factory.d.ts +15 -2
  22. package/dist/factory.d.ts.map +1 -1
  23. package/dist/factory.js +49 -10
  24. package/dist/factory.js.map +1 -1
  25. package/dist/integrations/jira/binding-scope.d.ts +30 -0
  26. package/dist/integrations/jira/binding-scope.d.ts.map +1 -0
  27. package/dist/integrations/jira/binding-scope.js +50 -0
  28. package/dist/integrations/jira/binding-scope.js.map +1 -0
  29. package/dist/integrations/jira/claim.d.ts +12 -0
  30. package/dist/integrations/jira/claim.d.ts.map +1 -1
  31. package/dist/integrations/jira/claim.js +24 -1
  32. package/dist/integrations/jira/claim.js.map +1 -1
  33. package/dist/integrations/jira/intake-jql.d.ts +32 -0
  34. package/dist/integrations/jira/intake-jql.d.ts.map +1 -0
  35. package/dist/integrations/jira/intake-jql.js +57 -0
  36. package/dist/integrations/jira/intake-jql.js.map +1 -0
  37. package/dist/integrations/jira/integration.d.ts +4 -3
  38. package/dist/integrations/jira/integration.d.ts.map +1 -1
  39. package/dist/integrations/jira/integration.js +14 -21
  40. package/dist/integrations/jira/integration.js.map +1 -1
  41. package/dist/integrations/jira/routes.d.ts +3 -0
  42. package/dist/integrations/jira/routes.d.ts.map +1 -1
  43. package/dist/integrations/jira/routes.js +16 -29
  44. package/dist/integrations/jira/routes.js.map +1 -1
  45. package/dist/integrations/jira/rules.d.ts.map +1 -1
  46. package/dist/integrations/jira/rules.js +8 -13
  47. package/dist/integrations/jira/rules.js.map +1 -1
  48. package/dist/integrations/platform/jira/integration.d.ts +7 -1
  49. package/dist/integrations/platform/jira/integration.d.ts.map +1 -1
  50. package/dist/integrations/platform/jira/integration.js +26 -24
  51. package/dist/integrations/platform/jira/integration.js.map +1 -1
  52. package/dist/integrations/platform/jira/routes.d.ts +3 -0
  53. package/dist/integrations/platform/jira/routes.d.ts.map +1 -1
  54. package/dist/integrations/platform/jira/routes.js +17 -28
  55. package/dist/integrations/platform/jira/routes.js.map +1 -1
  56. package/dist/routes/attention-providers.d.ts +2 -1
  57. package/dist/routes/attention-providers.d.ts.map +1 -1
  58. package/dist/routes/attention-providers.js +3 -2
  59. package/dist/routes/attention-providers.js.map +1 -1
  60. package/dist/routes/attention.d.ts +2 -1
  61. package/dist/routes/attention.d.ts.map +1 -1
  62. package/dist/routes/attention.js.map +1 -1
  63. package/dist/routes/boards.d.ts +30 -0
  64. package/dist/routes/boards.d.ts.map +1 -0
  65. package/dist/routes/boards.js +272 -0
  66. package/dist/routes/boards.js.map +1 -0
  67. package/dist/routes/contracts.d.ts +249 -0
  68. package/dist/routes/contracts.d.ts.map +1 -1
  69. package/dist/routes/contracts.js +107 -35
  70. package/dist/routes/contracts.js.map +1 -1
  71. package/dist/routes/intake.d.ts +15 -10
  72. package/dist/routes/intake.d.ts.map +1 -1
  73. package/dist/routes/intake.js +109 -22
  74. package/dist/routes/intake.js.map +1 -1
  75. package/dist/routes/projects.d.ts +3 -0
  76. package/dist/routes/projects.d.ts.map +1 -1
  77. package/dist/routes/projects.js +10 -1
  78. package/dist/routes/projects.js.map +1 -1
  79. package/dist/routes/surface.d.ts +10 -3
  80. package/dist/routes/surface.d.ts.map +1 -1
  81. package/dist/routes/surface.js +12 -1
  82. package/dist/routes/surface.js.map +1 -1
  83. package/dist/routes/tenant-credentials.d.ts +9 -0
  84. package/dist/routes/tenant-credentials.d.ts.map +1 -1
  85. package/dist/routes/tenant-credentials.js +24 -2
  86. package/dist/routes/tenant-credentials.js.map +1 -1
  87. package/dist/routes/work-items.d.ts +3 -1
  88. package/dist/routes/work-items.d.ts.map +1 -1
  89. package/dist/routes/work-items.js +64 -42
  90. package/dist/routes/work-items.js.map +1 -1
  91. package/dist/rules/dispatcher.d.ts +7 -1
  92. package/dist/rules/dispatcher.d.ts.map +1 -1
  93. package/dist/rules/dispatcher.js +70 -34
  94. package/dist/rules/dispatcher.js.map +1 -1
  95. package/dist/rules/processor.d.ts +3 -1
  96. package/dist/rules/processor.d.ts.map +1 -1
  97. package/dist/rules/processor.js +11 -6
  98. package/dist/rules/processor.js.map +1 -1
  99. package/dist/rules/tools.d.ts +2 -1
  100. package/dist/rules/tools.d.ts.map +1 -1
  101. package/dist/rules/tools.js +2 -1
  102. package/dist/rules/tools.js.map +1 -1
  103. package/dist/rules/transition-service.d.ts +3 -1
  104. package/dist/rules/transition-service.d.ts.map +1 -1
  105. package/dist/rules/transition-service.js +12 -12
  106. package/dist/rules/transition-service.js.map +1 -1
  107. package/dist/storage/domains/audit/actions.d.ts +1 -0
  108. package/dist/storage/domains/audit/actions.d.ts.map +1 -1
  109. package/dist/storage/domains/audit/actions.js +6 -1
  110. package/dist/storage/domains/audit/actions.js.map +1 -1
  111. package/dist/storage/domains/boards/base.d.ts +110 -0
  112. package/dist/storage/domains/boards/base.d.ts.map +1 -0
  113. package/dist/storage/domains/boards/base.js +292 -0
  114. package/dist/storage/domains/boards/base.js.map +1 -0
  115. package/dist/storage/domains/boards/schemas.d.ts +11 -0
  116. package/dist/storage/domains/boards/schemas.d.ts.map +1 -0
  117. package/dist/storage/domains/boards/schemas.js +106 -0
  118. package/dist/storage/domains/boards/schemas.js.map +1 -0
  119. package/dist/storage/domains/credentials/base.d.ts +4 -1
  120. package/dist/storage/domains/credentials/base.d.ts.map +1 -1
  121. package/dist/storage/domains/credentials/base.js +5 -2
  122. package/dist/storage/domains/credentials/base.js.map +1 -1
  123. package/dist/storage/domains/intake/base.d.ts +86 -5
  124. package/dist/storage/domains/intake/base.d.ts.map +1 -1
  125. package/dist/storage/domains/intake/base.js +298 -59
  126. package/dist/storage/domains/intake/base.js.map +1 -1
  127. package/dist/storage/domains/projects/base.d.ts +2 -1
  128. package/dist/storage/domains/projects/base.d.ts.map +1 -1
  129. package/dist/storage/domains/projects/base.js +15 -10
  130. package/dist/storage/domains/projects/base.js.map +1 -1
  131. package/dist/storage/domains/work-items/base.d.ts +14 -0
  132. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  133. package/dist/storage/domains/work-items/base.js +39 -11
  134. package/dist/storage/domains/work-items/base.js.map +1 -1
  135. package/dist/tools/read-tools.d.ts +3 -1
  136. package/dist/tools/read-tools.d.ts.map +1 -1
  137. package/dist/tools/read-tools.js +3 -2
  138. package/dist/tools/read-tools.js.map +1 -1
  139. package/dist/tools/session-tools.d.ts +3 -1
  140. package/dist/tools/session-tools.d.ts.map +1 -1
  141. package/dist/tools/session-tools.js +2 -1
  142. package/dist/tools/session-tools.js.map +1 -1
  143. package/dist/tools/sessions-state-processor.d.ts +3 -1
  144. package/dist/tools/sessions-state-processor.d.ts.map +1 -1
  145. package/dist/tools/sessions-state-processor.js +2 -1
  146. package/dist/tools/sessions-state-processor.js.map +1 -1
  147. package/dist/tools/work-item-tools.d.ts +3 -1
  148. package/dist/tools/work-item-tools.d.ts.map +1 -1
  149. package/dist/tools/work-item-tools.js +3 -2
  150. package/dist/tools/work-item-tools.js.map +1 -1
  151. package/dist/workspace.js +1 -1
  152. package/factory-skills/factory-plan/SKILL.md +1 -1
  153. package/factory-skills/factory-triage/SKILL.md +18 -7
  154. package/package.json +12 -12
@@ -8,6 +8,18 @@ import { FactoryStorageDomain, UniqueViolationError } from "@mastra/core/storage
8
8
  * source ids the organization selected. The selection feeds shared boards, so
9
9
  * every member reads the same one.
10
10
  */
11
+ var FactoryProjectNotFoundError = class extends Error {
12
+ constructor() {
13
+ super("Factory project not found for this organization.");
14
+ this.name = "FactoryProjectNotFoundError";
15
+ }
16
+ };
17
+ async function assertFactoryProject(ops, orgId, factoryProjectId) {
18
+ if (!await ops.findOne("factory_projects", {
19
+ org_id: orgId,
20
+ id: factoryProjectId
21
+ })) throw new FactoryProjectNotFoundError();
22
+ }
11
23
  const DEFAULT_INTAKE_CONFIG = {};
12
24
  const INTAKE_ORG_SETTINGS_SCHEMA = {
13
25
  name: "intake_org_settings",
@@ -63,6 +75,11 @@ function mergeIntakeSelections(configs) {
63
75
  * ingested item should land. GitHub items are naturally scoped by their linked
64
76
  * repository; providers without that link (Linear) need this explicit binding
65
77
  * so viewing one project's board cannot materialize another project's items.
78
+ *
79
+ * Frozen: its unique index allows one binding per source, and indexes cannot be
80
+ * narrowed, so bindings moved to {@link INTAKE_SOURCE_SUBSCRIPTIONS_SCHEMA}. Rows
81
+ * are copied once at boot and never written again; the collection stays
82
+ * registered so an earlier build that is rolled back to still finds them.
66
83
  */
67
84
  const INTAKE_SOURCE_BINDINGS_SCHEMA = {
68
85
  name: "intake_source_bindings",
@@ -98,6 +115,105 @@ const INTAKE_SOURCE_BINDINGS_SCHEMA = {
98
115
  }]
99
116
  };
100
117
  /**
118
+ * Binds an intake source to a Factory project, one row per (source, Factory
119
+ * project). Most integrations keep one binding per source and rebinding moves
120
+ * it; integrations with `Intake.bindingLabelFilters` let several Factory
121
+ * projects share a source, each taking the issues carrying its own labels.
122
+ */
123
+ const INTAKE_SOURCE_SUBSCRIPTIONS_SCHEMA = {
124
+ name: "intake_source_subscriptions",
125
+ columns: {
126
+ id: { type: "uuid-pk" },
127
+ org_id: { type: "text" },
128
+ integration_id: { type: "text" },
129
+ source_id: { type: "text" },
130
+ factory_project_id: { type: "text" },
131
+ /** Installed board the source feeds; `null` keeps the legacy source-type routing (issues → work, PRs → review). */
132
+ board: {
133
+ type: "text",
134
+ nullable: true
135
+ },
136
+ /** Labels whose issues this binding takes, as the user typed them; `null` takes every unclaimed issue. */
137
+ labels: {
138
+ type: "json",
139
+ nullable: true
140
+ },
141
+ created_by_user_id: {
142
+ type: "text",
143
+ nullable: true
144
+ },
145
+ created_at: { type: "timestamp" },
146
+ updated_at: { type: "timestamp" }
147
+ },
148
+ indexes: [{
149
+ name: "intake_source_subscriptions_project_idx",
150
+ columns: ["org_id", "factory_project_id"]
151
+ }],
152
+ uniqueIndexes: [{
153
+ name: "intake_source_subscriptions_org_integration_source_project_unique",
154
+ columns: [
155
+ "org_id",
156
+ "integration_id",
157
+ "source_id",
158
+ "factory_project_id"
159
+ ]
160
+ }]
161
+ };
162
+ /** One-time data migrations that have run, so none re-runs over rows users have changed since. */
163
+ const INTAKE_DATA_MIGRATIONS_SCHEMA = {
164
+ name: "intake_data_migrations",
165
+ columns: {
166
+ name: {
167
+ type: "text",
168
+ primaryKey: true
169
+ },
170
+ completed_at: { type: "timestamp" }
171
+ }
172
+ };
173
+ const COPY_LEGACY_BINDINGS_MIGRATION = "copy_intake_source_bindings_to_subscriptions";
174
+ /** Bindings on one source in precedence order: the oldest binding wins an issue two of them match. */
175
+ function inPrecedenceOrder(bindings) {
176
+ return [...bindings].sort((left, right) => left.createdAt.getTime() - right.createdAt.getTime() || left.factoryProjectId.localeCompare(right.factoryProjectId));
177
+ }
178
+ /**
179
+ * The slice of a shared source's issues `binding` takes, given every binding
180
+ * on that source (`siblings` may include `binding` itself). Label bindings beat
181
+ * the all-issues binding, and an issue carrying labels of two label bindings
182
+ * goes to the older one.
183
+ */
184
+ function bindingLabelSlice(binding, siblings) {
185
+ const labelled = inPrecedenceOrder(siblings.filter((sibling) => sibling.labels !== null && sibling.factoryProjectId !== binding.factoryProjectId));
186
+ if (binding.labels === null) return {
187
+ include: null,
188
+ exclude: labelled.flatMap((sibling) => sibling.labels ?? [])
189
+ };
190
+ const older = labelled.filter((sibling) => inPrecedenceOrder([sibling, binding])[0] === sibling);
191
+ return {
192
+ include: binding.labels,
193
+ exclude: older.flatMap((sibling) => sibling.labels ?? [])
194
+ };
195
+ }
196
+ /** Whether an issue carrying `labels` falls in `binding`'s slice of its source. */
197
+ function bindingTakesIssue(binding, siblings, labels) {
198
+ const carried = new Set(labels.map(normalizeIntakeLabel));
199
+ const { include, exclude } = bindingLabelSlice(binding, siblings);
200
+ if (exclude.some((label) => carried.has(normalizeIntakeLabel(label)))) return false;
201
+ return include === null || include.some((label) => carried.has(normalizeIntakeLabel(label)));
202
+ }
203
+ /** Trim and de-duplicate case-insensitively, keeping the first spelling the user typed. */
204
+ function normalizeBindingLabels(labels) {
205
+ const seen = /* @__PURE__ */ new Set();
206
+ const kept = [];
207
+ for (const label of labels) {
208
+ const trimmed = label.trim();
209
+ const key = normalizeIntakeLabel(trimmed);
210
+ if (!key || seen.has(key)) continue;
211
+ seen.add(key);
212
+ kept.push(trimmed);
213
+ }
214
+ return kept;
215
+ }
216
+ /**
101
217
  * Routes items carrying a label to an installed board, per Factory project.
102
218
  *
103
219
  * Repository-scoped providers (GitHub) already know which project an item
@@ -163,9 +279,43 @@ function toIntakeSourceBinding(row) {
163
279
  integrationId: row.integration_id,
164
280
  sourceId: row.source_id,
165
281
  factoryProjectId: row.factory_project_id,
166
- board: row.board ?? null
282
+ board: row.board ?? null,
283
+ labels: row.labels ?? null,
284
+ createdAt: row.created_at
167
285
  };
168
286
  }
287
+ /** Overlap between a requested binding and the other Factory projects' bindings on the same source. */
288
+ function bindingConflicts(factoryProjectId, labels, rows) {
289
+ const requested = new Set(labels?.map(normalizeIntakeLabel));
290
+ return rows.flatMap((row) => {
291
+ if (row.factory_project_id === factoryProjectId) return [];
292
+ const theirs = row.labels ?? null;
293
+ if (labels === null || theirs === null) return labels === null && theirs === null ? [{
294
+ factoryProjectId: row.factory_project_id,
295
+ labels: null
296
+ }] : [];
297
+ const overlap = theirs.filter((label) => requested.has(normalizeIntakeLabel(label)));
298
+ return overlap.length > 0 ? [{
299
+ factoryProjectId: row.factory_project_id,
300
+ labels: overlap
301
+ }] : [];
302
+ });
303
+ }
304
+ /** Update one (source, Factory project) binding in place, keeping its age, or insert it. */
305
+ async function upsertBinding(ops, key, patch, userId) {
306
+ const now = /* @__PURE__ */ new Date();
307
+ if (await ops.updateMany("intake_source_subscriptions", key, {
308
+ ...patch,
309
+ updated_at: now
310
+ }) > 0) return;
311
+ await ops.insertOne("intake_source_subscriptions", {
312
+ ...key,
313
+ ...patch,
314
+ created_by_user_id: userId ?? null,
315
+ created_at: now,
316
+ updated_at: now
317
+ });
318
+ }
169
319
  var IntakeStorage = class extends FactoryStorageDomain {
170
320
  constructor() {
171
321
  super("intake");
@@ -175,9 +325,49 @@ var IntakeStorage = class extends FactoryStorageDomain {
175
325
  INTAKE_ORG_SETTINGS_SCHEMA,
176
326
  LEGACY_INTAKE_USER_SETTINGS_SCHEMA,
177
327
  INTAKE_SOURCE_BINDINGS_SCHEMA,
328
+ INTAKE_SOURCE_SUBSCRIPTIONS_SCHEMA,
329
+ INTAKE_DATA_MIGRATIONS_SCHEMA,
178
330
  INTAKE_LABEL_ROUTES_SCHEMA
179
331
  ]);
180
332
  await this.#foldLegacyUserSelections();
333
+ await this.#copyLegacyBindings();
334
+ }
335
+ /**
336
+ * Copy `intake_source_bindings` into `intake_source_subscriptions` as
337
+ * all-issues bindings, once. The marker and the copy commit together, so a
338
+ * boot racing another cannot re-copy a binding removed after the first
339
+ * boot finished.
340
+ */
341
+ async #copyLegacyBindings() {
342
+ try {
343
+ await this.storage.withTransaction(async (ops) => {
344
+ if (await ops.findOne("intake_data_migrations", { name: COPY_LEGACY_BINDINGS_MIGRATION })) return;
345
+ const legacyRows = await ops.findMany("intake_source_bindings", {});
346
+ for (const row of legacyRows) {
347
+ const key = {
348
+ org_id: row.org_id,
349
+ integration_id: row.integration_id,
350
+ source_id: row.source_id,
351
+ factory_project_id: row.factory_project_id
352
+ };
353
+ if (await ops.findOne("intake_source_subscriptions", key)) continue;
354
+ await ops.insertOne("intake_source_subscriptions", {
355
+ ...key,
356
+ board: row.board ?? null,
357
+ labels: null,
358
+ created_by_user_id: row.created_by_user_id ?? null,
359
+ created_at: row.created_at,
360
+ updated_at: row.updated_at
361
+ });
362
+ }
363
+ await ops.insertOne("intake_data_migrations", {
364
+ name: COPY_LEGACY_BINDINGS_MIGRATION,
365
+ completed_at: /* @__PURE__ */ new Date()
366
+ });
367
+ }, { isolationLevel: "serializable" });
368
+ } catch (error) {
369
+ if (!(error instanceof UniqueViolationError)) throw error;
370
+ }
181
371
  }
182
372
  async #foldLegacyUserSelections() {
183
373
  const legacyRows = await this.ops.findMany("intake_settings", {});
@@ -192,6 +382,8 @@ var IntakeStorage = class extends FactoryStorageDomain {
192
382
  await this.ops.deleteMany("intake_org_settings", {});
193
383
  await this.ops.deleteMany("intake_settings", {});
194
384
  await this.ops.deleteMany("intake_source_bindings", {});
385
+ await this.ops.deleteMany("intake_source_subscriptions", {});
386
+ await this.ops.deleteMany("intake_data_migrations", {});
195
387
  await this.ops.deleteMany("intake_label_routes", {});
196
388
  }
197
389
  get #db() {
@@ -243,69 +435,120 @@ var IntakeStorage = class extends FactoryStorageDomain {
243
435
  throw error;
244
436
  }
245
437
  }
246
- /** Every source binding in the org, optionally narrowed to one integration. */
438
+ /** Every source binding in the org, optionally narrowed to one integration, oldest first. */
247
439
  async listBindings({ orgId, integrationId }) {
248
- return (await this.#db.findMany("intake_source_bindings", {
440
+ return inPrecedenceOrder((await this.#db.findMany("intake_source_subscriptions", {
249
441
  org_id: orgId,
250
442
  ...integrationId ? { integration_id: integrationId } : {}
251
- })).map(toIntakeSourceBinding);
443
+ })).map(toIntakeSourceBinding));
252
444
  }
253
445
  /** Source ids bound to one Factory project. Empty means "nothing bound". */
254
446
  async listBoundSourceIds({ orgId, integrationId, factoryProjectId }) {
255
- return (await this.#db.findMany("intake_source_bindings", {
447
+ return (await this.#db.findMany("intake_source_subscriptions", {
256
448
  org_id: orgId,
257
449
  integration_id: integrationId,
258
450
  factory_project_id: factoryProjectId
259
451
  })).map((row) => row.source_id);
260
452
  }
261
- /** The binding for one source, or `null` when it is unbound. */
262
- async getBinding({ orgId, integrationId, sourceId }) {
263
- const row = await this.#db.findOne("intake_source_bindings", {
453
+ /**
454
+ * One Factory project's binding of a source, or `null` when it has none.
455
+ * Without `factoryProjectId`, the source's oldest binding — for integrations
456
+ * that keep one binding per source.
457
+ */
458
+ async getBinding({ orgId, integrationId, sourceId, factoryProjectId }) {
459
+ return inPrecedenceOrder((await this.#db.findMany("intake_source_subscriptions", {
264
460
  org_id: orgId,
265
461
  integration_id: integrationId,
266
- source_id: sourceId
267
- });
268
- return row ? toIntakeSourceBinding(row) : null;
462
+ source_id: sourceId,
463
+ ...factoryProjectId ? { factory_project_id: factoryProjectId } : {}
464
+ })).map(toIntakeSourceBinding))[0] ?? null;
269
465
  }
270
- async clearBinding({ orgId, integrationId, sourceId }) {
466
+ /**
467
+ * Remove one Factory project's binding of a source, or — without
468
+ * `factoryProjectId` — every binding of it. Returns the (oldest) removed
469
+ * binding, or `null` when there was none.
470
+ */
471
+ async clearBinding({ orgId, integrationId, sourceId, factoryProjectId }) {
271
472
  const where = {
272
473
  org_id: orgId,
273
474
  integration_id: integrationId,
274
- source_id: sourceId
475
+ source_id: sourceId,
476
+ ...factoryProjectId ? { factory_project_id: factoryProjectId } : {}
275
477
  };
276
478
  return this.storage.withTransaction(async (ops) => {
277
- const row = await ops.findOne("intake_source_bindings", where);
278
- if (!row) return null;
279
- await ops.deleteMany("intake_source_bindings", where);
280
- return toIntakeSourceBinding(row);
479
+ const rows = await ops.findMany("intake_source_subscriptions", where);
480
+ if (rows.length === 0) return null;
481
+ await ops.deleteMany("intake_source_subscriptions", where);
482
+ return inPrecedenceOrder(rows.map(toIntakeSourceBinding))[0];
281
483
  }, { isolationLevel: "serializable" });
282
484
  }
485
+ /**
486
+ * Release every source a Factory project was bound to, so the others can take
487
+ * them. Pass the transaction deleting the project, so no binding lands in between.
488
+ */
489
+ async clearProjectBindings({ orgId, factoryProjectId }, ops = this.#db) {
490
+ await ops.deleteMany("intake_source_subscriptions", {
491
+ org_id: orgId,
492
+ factory_project_id: factoryProjectId
493
+ });
494
+ }
495
+ /**
496
+ * Bind a source to one Factory project, moving it away from any other: the
497
+ * source keeps a single binding, which takes all of its issues.
498
+ */
283
499
  async setBinding({ orgId, integrationId, sourceId, factoryProjectId, board = null, userId }) {
284
- const where = {
500
+ const source = {
285
501
  org_id: orgId,
286
502
  integration_id: integrationId,
287
503
  source_id: sourceId
288
504
  };
289
- const now = /* @__PURE__ */ new Date();
290
- const patch = {
291
- factory_project_id: factoryProjectId,
292
- board,
293
- updated_at: now
505
+ await this.storage.withTransaction(async (ops) => {
506
+ await assertFactoryProject(ops, orgId, factoryProjectId);
507
+ const rows = await ops.findMany("intake_source_subscriptions", source);
508
+ for (const row of rows) {
509
+ if (row.factory_project_id === factoryProjectId) continue;
510
+ await ops.deleteMany("intake_source_subscriptions", {
511
+ ...source,
512
+ factory_project_id: row.factory_project_id
513
+ });
514
+ }
515
+ await upsertBinding(ops, {
516
+ ...source,
517
+ factory_project_id: factoryProjectId
518
+ }, {
519
+ board,
520
+ labels: null
521
+ }, userId);
522
+ }, { isolationLevel: "serializable" });
523
+ }
524
+ /**
525
+ * Bind a source to one Factory project alongside other Factory projects'
526
+ * bindings of it, taking the issues carrying one of `labels` — or, with
527
+ * `labels: null`, every issue no label binding claims. Rejected with the
528
+ * overlapping bindings when another Factory project already takes some of
529
+ * those labels, or already takes all unclaimed issues.
530
+ */
531
+ async setSharedBinding({ orgId, integrationId, sourceId, factoryProjectId, board = null, labels, userId }) {
532
+ const source = {
533
+ org_id: orgId,
534
+ integration_id: integrationId,
535
+ source_id: sourceId
294
536
  };
295
- if (await this.#db.updateMany("intake_source_bindings", where, patch) > 0) return;
296
- try {
297
- await this.#db.insertOne("intake_source_bindings", {
298
- ...where,
299
- factory_project_id: factoryProjectId,
537
+ const kept = labels === null ? null : normalizeBindingLabels(labels);
538
+ return this.storage.withTransaction(async (ops) => {
539
+ await assertFactoryProject(ops, orgId, factoryProjectId);
540
+ const rows = await ops.findMany("intake_source_subscriptions", source);
541
+ const conflicts = bindingConflicts(factoryProjectId, kept, rows);
542
+ if (conflicts.length > 0) return { conflicts };
543
+ await upsertBinding(ops, {
544
+ ...source,
545
+ factory_project_id: factoryProjectId
546
+ }, {
300
547
  board,
301
- created_by_user_id: userId ?? null,
302
- created_at: now,
303
- updated_at: now
304
- });
305
- } catch (error) {
306
- if (!(error instanceof UniqueViolationError)) throw error;
307
- await this.#db.updateMany("intake_source_bindings", where, patch);
308
- }
548
+ labels: kept
549
+ }, userId);
550
+ return { conflicts: [] };
551
+ }, { isolationLevel: "serializable" });
309
552
  }
310
553
  /** Atomically replace legacy provider source ids in both selection and routing state. */
311
554
  async migrateSourceIds({ orgId, integrationId, migrations }) {
@@ -314,17 +557,19 @@ var IntakeStorage = class extends FactoryStorageDomain {
314
557
  conflicts: []
315
558
  };
316
559
  return this.storage.withTransaction(async (ops) => {
317
- const bindings = await ops.findMany("intake_source_bindings", {
560
+ const bindings = await ops.findMany("intake_source_subscriptions", {
318
561
  org_id: orgId,
319
562
  integration_id: integrationId
320
563
  });
321
- const bindingBySource = new Map(bindings.map((binding) => [binding.source_id, binding]));
564
+ const bindingsBySource = /* @__PURE__ */ new Map();
565
+ for (const binding of bindings) bindingsBySource.set(binding.source_id, [...bindingsBySource.get(binding.source_id) ?? [], binding]);
566
+ const destination = (rows) => rows.map((row) => `${row.factory_project_id}\0${row.board ?? ""}\0${JSON.stringify(row.labels ?? null)}`).sort().join("\n");
322
567
  const grouped = /* @__PURE__ */ new Map();
323
568
  for (const migration of migrations) grouped.set(migration.to, [...grouped.get(migration.to) ?? [], migration]);
324
569
  const conflicts = [];
325
570
  for (const [to, group] of grouped) {
326
- const candidates = [bindingBySource.get(to), ...group.map(({ from }) => bindingBySource.get(from))].filter((binding) => binding !== void 0);
327
- if (new Set(candidates.map((binding) => `${binding.factory_project_id}\0${binding.board ?? ""}`)).size > 1) conflicts.push(...group);
571
+ const candidates = [bindingsBySource.get(to), ...group.map(({ from }) => bindingsBySource.get(from))].filter((rows) => rows !== void 0);
572
+ if (new Set(candidates.map(destination)).size > 1) conflicts.push(...group);
328
573
  }
329
574
  if (conflicts.length > 0) return {
330
575
  migrations: [],
@@ -332,32 +577,26 @@ var IntakeStorage = class extends FactoryStorageDomain {
332
577
  };
333
578
  const now = /* @__PURE__ */ new Date();
334
579
  for (const [to, group] of grouped) {
335
- const canonical = bindingBySource.get(to);
336
- const legacy = group.flatMap(({ from }) => {
337
- const binding = bindingBySource.get(from);
338
- return binding ? [{
339
- from,
340
- binding
341
- }] : [];
342
- });
343
- if (!canonical && legacy.length > 0) {
580
+ const legacy = group.filter(({ from }) => bindingsBySource.has(from));
581
+ const scope = {
582
+ org_id: orgId,
583
+ integration_id: integrationId
584
+ };
585
+ if (!bindingsBySource.has(to) && legacy.length > 0) {
344
586
  const [first, ...duplicates] = legacy;
345
- await ops.updateMany("intake_source_bindings", {
346
- org_id: orgId,
347
- integration_id: integrationId,
587
+ await ops.updateMany("intake_source_subscriptions", {
588
+ ...scope,
348
589
  source_id: first.from
349
590
  }, {
350
591
  source_id: to,
351
592
  updated_at: now
352
593
  });
353
- for (const duplicate of duplicates) await ops.deleteMany("intake_source_bindings", {
354
- org_id: orgId,
355
- integration_id: integrationId,
594
+ for (const duplicate of duplicates) await ops.deleteMany("intake_source_subscriptions", {
595
+ ...scope,
356
596
  source_id: duplicate.from
357
597
  });
358
- } else if (canonical) for (const entry of legacy) await ops.deleteMany("intake_source_bindings", {
359
- org_id: orgId,
360
- integration_id: integrationId,
598
+ } else for (const entry of legacy) await ops.deleteMany("intake_source_subscriptions", {
599
+ ...scope,
361
600
  source_id: entry.from
362
601
  });
363
602
  }
@@ -434,6 +673,6 @@ var IntakeStorage = class extends FactoryStorageDomain {
434
673
  }, { isolationLevel: "serializable" });
435
674
  }
436
675
  };
437
- export { DEFAULT_INTAKE_CONFIG, INTAKE_LABEL_ROUTES_SCHEMA, INTAKE_ORG_SETTINGS_SCHEMA, INTAKE_SOURCE_BINDINGS_SCHEMA, IntakeStorage, LEGACY_INTAKE_USER_SETTINGS_SCHEMA, mergeIntakeSelections, normalizeIntakeLabel, resolveIntakeLabelRoute };
676
+ export { DEFAULT_INTAKE_CONFIG, FactoryProjectNotFoundError, INTAKE_DATA_MIGRATIONS_SCHEMA, INTAKE_LABEL_ROUTES_SCHEMA, INTAKE_ORG_SETTINGS_SCHEMA, INTAKE_SOURCE_BINDINGS_SCHEMA, INTAKE_SOURCE_SUBSCRIPTIONS_SCHEMA, IntakeStorage, LEGACY_INTAKE_USER_SETTINGS_SCHEMA, bindingLabelSlice, bindingTakesIssue, mergeIntakeSelections, normalizeBindingLabels, normalizeIntakeLabel, resolveIntakeLabelRoute };
438
677
 
439
678
  //# sourceMappingURL=base.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"base.js","names":[],"sources":["../../../../src/storage/domains/intake/base.ts"],"sourcesContent":["/**\n * Org-wide intake selections for every configured intake integration.\n *\n * Integration ids are dynamic. Each integration contributes provider-neutral\n * sources through `FactoryIntegration.intake`; this domain only persists which\n * source ids the organization selected. The selection feeds shared boards, so\n * every member reads the same one.\n */\n\nimport { FactoryStorageDomain, UniqueViolationError } from '@mastra/core/storage';\nimport type { CollectionSchema, FactoryStorageOps } from '@mastra/core/storage';\n\nexport interface IntakeSelection {\n enabled: boolean;\n /** Provider-owned source ids; `null` means nothing is selected. */\n sourceIds: string[] | null;\n repositoryByLinearProject?: Record<string, string>;\n}\n\nexport type IntakeConfig = Record<string, IntakeSelection>;\n\nexport const DEFAULT_INTAKE_CONFIG: IntakeConfig = {};\n\nexport const INTAKE_ORG_SETTINGS_SCHEMA: CollectionSchema = {\n name: 'intake_org_settings',\n columns: {\n id: { type: 'uuid-pk' },\n org_id: { type: 'text' },\n config: { type: 'json' },\n created_at: { type: 'timestamp' },\n updated_at: { type: 'timestamp' },\n },\n uniqueIndexes: [{ name: 'intake_org_settings_org_unique', columns: ['org_id'] }],\n};\n\n/** Collection evolution is additive, so the per-member unique index cannot be narrowed: rows stay and are folded at boot. */\nexport const LEGACY_INTAKE_USER_SETTINGS_SCHEMA: CollectionSchema = {\n name: 'intake_settings',\n columns: {\n id: { type: 'uuid-pk' },\n org_id: { type: 'text' },\n user_id: { type: 'text' },\n config: { type: 'json' },\n created_at: { type: 'timestamp' },\n updated_at: { type: 'timestamp' },\n },\n uniqueIndexes: [{ name: 'intake_settings_org_user_unique', columns: ['org_id', 'user_id'] }],\n};\n\n/** What the org effectively saw while selections were personal: a member's sources fed the shared board only while their switch was on. */\nexport function mergeIntakeSelections(configs: readonly IntakeConfig[]): IntakeConfig {\n const merged: IntakeConfig = Object.create(null);\n for (const config of configs) {\n for (const [integrationId, selection] of Object.entries(config)) {\n const current = merged[integrationId] ?? { enabled: false, sourceIds: null };\n const syncing = selection.enabled ? (selection.sourceIds ?? []) : [];\n const sourceIds = [...new Set([...(current.sourceIds ?? []), ...syncing])];\n merged[integrationId] = {\n enabled: current.enabled || selection.enabled,\n sourceIds: sourceIds.length ? sourceIds : null,\n };\n }\n }\n return merged;\n}\n\n/**\n * Binds an intake source to the Factory project its items belong to.\n *\n * Intake selections are org-wide, so they cannot say *where* an\n * ingested item should land. GitHub items are naturally scoped by their linked\n * repository; providers without that link (Linear) need this explicit binding\n * so viewing one project's board cannot materialize another project's items.\n */\nexport const INTAKE_SOURCE_BINDINGS_SCHEMA: CollectionSchema = {\n name: 'intake_source_bindings',\n columns: {\n id: { type: 'uuid-pk' },\n org_id: { type: 'text' },\n integration_id: { type: 'text' },\n source_id: { type: 'text' },\n factory_project_id: { type: 'text' },\n /** Installed board the source feeds; `null` keeps the legacy source-type routing (issues → work, PRs → review). */\n board: { type: 'text', nullable: true },\n created_by_user_id: { type: 'text', nullable: true },\n created_at: { type: 'timestamp' },\n updated_at: { type: 'timestamp' },\n },\n indexes: [{ name: 'intake_source_bindings_project_idx', columns: ['org_id', 'factory_project_id'] }],\n uniqueIndexes: [\n {\n name: 'intake_source_bindings_org_integration_source_unique',\n columns: ['org_id', 'integration_id', 'source_id'],\n },\n ],\n};\n\nexport interface IntakeSourceBinding {\n integrationId: string;\n sourceId: string;\n factoryProjectId: string;\n /** Installed board the source feeds, or `null` for legacy source-type routing. */\n board: string | null;\n}\n\n/**\n * Routes items carrying a label to an installed board, per Factory project.\n *\n * Repository-scoped providers (GitHub) already know which project an item\n * belongs to, but one repository can feed several boards: an issue labelled\n * `release` belongs on a release board while the rest stay on Work. Labels\n * without a route fall back to the provider's built-in board.\n */\nexport const INTAKE_LABEL_ROUTES_SCHEMA: CollectionSchema = {\n name: 'intake_label_routes',\n columns: {\n id: { type: 'uuid-pk' },\n org_id: { type: 'text' },\n factory_project_id: { type: 'text' },\n integration_id: { type: 'text' },\n /** Label as the provider reports it; matched case-insensitively. */\n label: { type: 'text' },\n board: { type: 'text' },\n created_by_user_id: { type: 'text', nullable: true },\n created_at: { type: 'timestamp' },\n updated_at: { type: 'timestamp' },\n },\n indexes: [{ name: 'intake_label_routes_project_idx', columns: ['org_id', 'factory_project_id'] }],\n uniqueIndexes: [\n {\n name: 'intake_label_routes_org_project_integration_label_unique',\n columns: ['org_id', 'factory_project_id', 'integration_id', 'label'],\n },\n ],\n};\n\nexport interface IntakeLabelRoute {\n factoryProjectId: string;\n integrationId: string;\n label: string;\n board: string;\n}\n\ntype IntakeLabelRouteRow = {\n factory_project_id: string;\n integration_id: string;\n label: string;\n board: string;\n};\nfunction toIntakeLabelRoute(row: IntakeLabelRouteRow): IntakeLabelRoute {\n return {\n factoryProjectId: row.factory_project_id,\n integrationId: row.integration_id,\n label: row.label,\n board: row.board,\n };\n}\n\n/** Labels are matched the way GitHub treats them: case-insensitively. */\nexport function normalizeIntakeLabel(label: string): string {\n return label.trim().toLowerCase();\n}\n\n/**\n * The board a labelled item should land on under `routes`, or `undefined` when\n * none of its labels is routed. Ties resolve to the first route in `routes`\n * order, so callers should pass a deterministically ordered list.\n */\nexport function resolveIntakeLabelRoute(\n routes: readonly IntakeLabelRoute[],\n labels: readonly string[] | undefined,\n): IntakeLabelRoute | undefined {\n if (!labels?.length) return undefined;\n const normalized = new Set(labels.map(normalizeIntakeLabel));\n return routes.find(route => normalized.has(normalizeIntakeLabel(route.label)));\n}\n\ntype IntakeSourceBindingRow = {\n integration_id: string;\n source_id: string;\n factory_project_id: string;\n board?: string | null;\n};\nfunction toIntakeSourceBinding(row: IntakeSourceBindingRow): IntakeSourceBinding {\n return {\n integrationId: row.integration_id,\n sourceId: row.source_id,\n factoryProjectId: row.factory_project_id,\n board: row.board ?? null,\n };\n}\n\nexport class IntakeStorage extends FactoryStorageDomain {\n constructor() {\n super('intake');\n }\n\n async init(): Promise<void> {\n await this.ensureCollections([\n INTAKE_ORG_SETTINGS_SCHEMA,\n LEGACY_INTAKE_USER_SETTINGS_SCHEMA,\n INTAKE_SOURCE_BINDINGS_SCHEMA,\n INTAKE_LABEL_ROUTES_SCHEMA,\n ]);\n await this.#foldLegacyUserSelections();\n }\n\n async #foldLegacyUserSelections(): Promise<void> {\n const legacyRows = await this.ops.findMany<{ org_id: string; config: IntakeConfig }>('intake_settings', {});\n const byOrg = new Map<string, IntakeConfig[]>();\n for (const row of legacyRows) {\n byOrg.set(row.org_id, [...(byOrg.get(row.org_id) ?? []), row.config]);\n }\n for (const [orgId, configs] of byOrg) {\n const folded = await this.ops.findOne('intake_org_settings', { org_id: orgId });\n if (folded) continue;\n await this.#insertConfigIfAbsent(orgId, mergeIntakeSelections(configs));\n }\n }\n\n async dangerouslyClearAll(): Promise<void> {\n await this.ops.deleteMany('intake_org_settings', {});\n await this.ops.deleteMany('intake_settings', {});\n await this.ops.deleteMany('intake_source_bindings', {});\n await this.ops.deleteMany('intake_label_routes', {});\n }\n\n get #db(): FactoryStorageOps {\n return this.ops;\n }\n\n async getConfig({\n orgId,\n integrationIds,\n }: {\n orgId: string;\n /**\n * When provided, the result contains exactly these integration ids, with\n * unset integrations defaulting to `{ enabled: true, sourceIds: null }`.\n */\n integrationIds?: string[];\n }): Promise<IntakeConfig> {\n const row = await this.#db.findOne<{ config: IntakeConfig }>('intake_org_settings', { org_id: orgId });\n const saved = structuredClone(row?.config ?? DEFAULT_INTAKE_CONFIG);\n if (!integrationIds) return saved;\n return Object.fromEntries(\n integrationIds.map(integrationId => [integrationId, saved[integrationId] ?? { enabled: true, sourceIds: null }]),\n );\n }\n\n async saveConfig({ orgId, config }: { orgId: string; config: IntakeConfig }): Promise<void> {\n const now = new Date();\n const where = { org_id: orgId };\n const updated = await this.#db.updateMany('intake_org_settings', where, { config, updated_at: now });\n if (updated > 0) return;\n if (await this.#insertConfigIfAbsent(orgId, config)) return;\n await this.#db.updateMany('intake_org_settings', where, { config, updated_at: now });\n }\n\n /** Overlay `config` onto the stored selections; integrations it omits keep their stored value. */\n async mergeConfig({ orgId, config }: { orgId: string; config: IntakeConfig }): Promise<IntakeConfig> {\n const merged: IntakeConfig = Object.assign(Object.create(null), await this.getConfig({ orgId }), config);\n await this.saveConfig({ orgId, config: merged });\n return merged;\n }\n\n async #insertConfigIfAbsent(orgId: string, config: IntakeConfig): Promise<boolean> {\n const now = new Date();\n try {\n await this.#db.insertOne('intake_org_settings', { org_id: orgId, config, created_at: now, updated_at: now });\n return true;\n } catch (error) {\n if (error instanceof UniqueViolationError) return false;\n throw error;\n }\n }\n\n /** Every source binding in the org, optionally narrowed to one integration. */\n async listBindings({\n orgId,\n integrationId,\n }: {\n orgId: string;\n integrationId?: string;\n }): Promise<IntakeSourceBinding[]> {\n const rows = await this.#db.findMany<IntakeSourceBindingRow>('intake_source_bindings', {\n org_id: orgId,\n ...(integrationId ? { integration_id: integrationId } : {}),\n });\n return rows.map(toIntakeSourceBinding);\n }\n\n /** Source ids bound to one Factory project. Empty means \"nothing bound\". */\n async listBoundSourceIds({\n orgId,\n integrationId,\n factoryProjectId,\n }: {\n orgId: string;\n integrationId: string;\n factoryProjectId: string;\n }): Promise<string[]> {\n const rows = await this.#db.findMany<IntakeSourceBindingRow>('intake_source_bindings', {\n org_id: orgId,\n integration_id: integrationId,\n factory_project_id: factoryProjectId,\n });\n return rows.map(row => row.source_id);\n }\n\n /** The binding for one source, or `null` when it is unbound. */\n async getBinding({\n orgId,\n integrationId,\n sourceId,\n }: {\n orgId: string;\n integrationId: string;\n sourceId: string;\n }): Promise<IntakeSourceBinding | null> {\n const row = await this.#db.findOne<IntakeSourceBindingRow>('intake_source_bindings', {\n org_id: orgId,\n integration_id: integrationId,\n source_id: sourceId,\n });\n return row ? toIntakeSourceBinding(row) : null;\n }\n\n async clearBinding({\n orgId,\n integrationId,\n sourceId,\n }: {\n orgId: string;\n integrationId: string;\n sourceId: string;\n }): Promise<IntakeSourceBinding | null> {\n const where = { org_id: orgId, integration_id: integrationId, source_id: sourceId };\n return this.storage.withTransaction(\n async ops => {\n const row = await ops.findOne<IntakeSourceBindingRow>('intake_source_bindings', where);\n if (!row) return null;\n await ops.deleteMany('intake_source_bindings', where);\n return toIntakeSourceBinding(row);\n },\n { isolationLevel: 'serializable' },\n );\n }\n\n async setBinding({\n orgId,\n integrationId,\n sourceId,\n factoryProjectId,\n board = null,\n userId,\n }: {\n orgId: string;\n integrationId: string;\n sourceId: string;\n factoryProjectId: string;\n board?: string | null;\n userId?: string;\n }): Promise<void> {\n const where = { org_id: orgId, integration_id: integrationId, source_id: sourceId };\n const now = new Date();\n const patch = { factory_project_id: factoryProjectId, board, updated_at: now };\n const updated = await this.#db.updateMany('intake_source_bindings', where, patch);\n if (updated > 0) return;\n try {\n await this.#db.insertOne('intake_source_bindings', {\n ...where,\n factory_project_id: factoryProjectId,\n board,\n created_by_user_id: userId ?? null,\n created_at: now,\n updated_at: now,\n });\n } catch (error) {\n if (!(error instanceof UniqueViolationError)) throw error;\n await this.#db.updateMany('intake_source_bindings', where, patch);\n }\n }\n /** Atomically replace legacy provider source ids in both selection and routing state. */\n async migrateSourceIds({\n orgId,\n integrationId,\n migrations,\n }: {\n orgId: string;\n integrationId: string;\n migrations: Array<{ from: string; to: string }>;\n }): Promise<{ migrations: Array<{ from: string; to: string }>; conflicts: Array<{ from: string; to: string }> }> {\n if (migrations.length === 0) return { migrations: [], conflicts: [] };\n return this.storage.withTransaction(\n async ops => {\n const bindings = await ops.findMany<IntakeSourceBindingRow>('intake_source_bindings', {\n org_id: orgId,\n integration_id: integrationId,\n });\n const bindingBySource = new Map(bindings.map(binding => [binding.source_id, binding] as const));\n const grouped = new Map<string, Array<{ from: string; to: string }>>();\n for (const migration of migrations) {\n grouped.set(migration.to, [...(grouped.get(migration.to) ?? []), migration]);\n }\n const conflicts: Array<{ from: string; to: string }> = [];\n for (const [to, group] of grouped) {\n const candidates = [bindingBySource.get(to), ...group.map(({ from }) => bindingBySource.get(from))].filter(\n (binding): binding is IntakeSourceBindingRow => binding !== undefined,\n );\n const destinations = new Set(\n candidates.map(binding => `${binding.factory_project_id}\\0${binding.board ?? ''}`),\n );\n if (destinations.size > 1) conflicts.push(...group);\n }\n if (conflicts.length > 0) return { migrations: [], conflicts };\n\n const now = new Date();\n for (const [to, group] of grouped) {\n const canonical = bindingBySource.get(to);\n const legacy = group.flatMap(({ from }) => {\n const binding = bindingBySource.get(from);\n return binding ? [{ from, binding }] : [];\n });\n if (!canonical && legacy.length > 0) {\n const [first, ...duplicates] = legacy;\n await ops.updateMany(\n 'intake_source_bindings',\n { org_id: orgId, integration_id: integrationId, source_id: first!.from },\n { source_id: to, updated_at: now },\n );\n for (const duplicate of duplicates) {\n await ops.deleteMany('intake_source_bindings', {\n org_id: orgId,\n integration_id: integrationId,\n source_id: duplicate.from,\n });\n }\n } else if (canonical) {\n for (const entry of legacy) {\n await ops.deleteMany('intake_source_bindings', {\n org_id: orgId,\n integration_id: integrationId,\n source_id: entry.from,\n });\n }\n }\n }\n\n const configRow = await ops.findOne<{ config: IntakeConfig }>('intake_org_settings', { org_id: orgId });\n if (configRow) {\n const replacements = new Map(migrations.map(({ from, to }) => [from, to] as const));\n const selected = configRow.config[integrationId]?.sourceIds ?? [];\n const nextSelected = [...new Set(selected.map(sourceId => replacements.get(sourceId) ?? sourceId))];\n if (\n nextSelected.length !== selected.length ||\n nextSelected.some((sourceId, index) => sourceId !== selected[index])\n ) {\n const config = structuredClone(configRow.config);\n config[integrationId] = {\n ...(config[integrationId] ?? { enabled: true }),\n sourceIds: nextSelected.length > 0 ? nextSelected : null,\n };\n await ops.updateMany('intake_org_settings', { org_id: orgId }, { config, updated_at: now });\n }\n }\n return { migrations, conflicts: [] };\n },\n { isolationLevel: 'serializable' },\n );\n }\n\n /** Label routes in the org, optionally narrowed to one project and/or integration. Sorted by label. */\n async listLabelRoutes({\n orgId,\n factoryProjectId,\n integrationId,\n }: {\n orgId: string;\n factoryProjectId?: string;\n integrationId?: string;\n }): Promise<IntakeLabelRoute[]> {\n const rows = await this.#db.findMany<IntakeLabelRouteRow>('intake_label_routes', {\n org_id: orgId,\n ...(factoryProjectId ? { factory_project_id: factoryProjectId } : {}),\n ...(integrationId ? { integration_id: integrationId } : {}),\n });\n return rows.map(toIntakeLabelRoute).sort((a, b) => a.label.localeCompare(b.label));\n }\n\n async setLabelRoute({\n orgId,\n factoryProjectId,\n integrationId,\n label,\n board,\n userId,\n }: {\n orgId: string;\n factoryProjectId: string;\n integrationId: string;\n label: string;\n board: string;\n userId?: string;\n }): Promise<void> {\n const where = {\n org_id: orgId,\n factory_project_id: factoryProjectId,\n integration_id: integrationId,\n label: normalizeIntakeLabel(label),\n };\n const now = new Date();\n const patch = { board, updated_at: now };\n const updated = await this.#db.updateMany('intake_label_routes', where, patch);\n if (updated > 0) return;\n try {\n await this.#db.insertOne('intake_label_routes', {\n ...where,\n board,\n created_by_user_id: userId ?? null,\n created_at: now,\n updated_at: now,\n });\n } catch (error) {\n if (!(error instanceof UniqueViolationError)) throw error;\n await this.#db.updateMany('intake_label_routes', where, patch);\n }\n }\n\n /** Remove a label route; returns the route that was removed, or `null` when none existed. */\n async clearLabelRoute({\n orgId,\n factoryProjectId,\n integrationId,\n label,\n }: {\n orgId: string;\n factoryProjectId: string;\n integrationId: string;\n label: string;\n }): Promise<IntakeLabelRoute | null> {\n const where = {\n org_id: orgId,\n factory_project_id: factoryProjectId,\n integration_id: integrationId,\n label: normalizeIntakeLabel(label),\n };\n return this.storage.withTransaction(\n async ops => {\n const row = await ops.findOne<IntakeLabelRouteRow>('intake_label_routes', where);\n if (!row) return null;\n await ops.deleteMany('intake_label_routes', where);\n return toIntakeLabelRoute(row);\n },\n { isolationLevel: 'serializable' },\n );\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"base.js","names":[],"sources":["../../../../src/storage/domains/intake/base.ts"],"sourcesContent":["/**\n * Org-wide intake selections for every configured intake integration.\n *\n * Integration ids are dynamic. Each integration contributes provider-neutral\n * sources through `FactoryIntegration.intake`; this domain only persists which\n * source ids the organization selected. The selection feeds shared boards, so\n * every member reads the same one.\n */\n\nimport { FactoryStorageDomain, UniqueViolationError } from '@mastra/core/storage';\nimport type { CollectionSchema, FactoryStorageOps } from '@mastra/core/storage';\n\nexport class FactoryProjectNotFoundError extends Error {\n constructor() {\n super('Factory project not found for this organization.');\n this.name = 'FactoryProjectNotFoundError';\n }\n}\n\nasync function assertFactoryProject(ops: FactoryStorageOps, orgId: string, factoryProjectId: string): Promise<void> {\n if (!(await ops.findOne('factory_projects', { org_id: orgId, id: factoryProjectId }))) {\n throw new FactoryProjectNotFoundError();\n }\n}\n\nexport interface IntakeSelection {\n enabled: boolean;\n /** Provider-owned source ids; `null` means nothing is selected. */\n sourceIds: string[] | null;\n repositoryByLinearProject?: Record<string, string>;\n}\n\nexport type IntakeConfig = Record<string, IntakeSelection>;\n\nexport const DEFAULT_INTAKE_CONFIG: IntakeConfig = {};\n\nexport const INTAKE_ORG_SETTINGS_SCHEMA: CollectionSchema = {\n name: 'intake_org_settings',\n columns: {\n id: { type: 'uuid-pk' },\n org_id: { type: 'text' },\n config: { type: 'json' },\n created_at: { type: 'timestamp' },\n updated_at: { type: 'timestamp' },\n },\n uniqueIndexes: [{ name: 'intake_org_settings_org_unique', columns: ['org_id'] }],\n};\n\n/** Collection evolution is additive, so the per-member unique index cannot be narrowed: rows stay and are folded at boot. */\nexport const LEGACY_INTAKE_USER_SETTINGS_SCHEMA: CollectionSchema = {\n name: 'intake_settings',\n columns: {\n id: { type: 'uuid-pk' },\n org_id: { type: 'text' },\n user_id: { type: 'text' },\n config: { type: 'json' },\n created_at: { type: 'timestamp' },\n updated_at: { type: 'timestamp' },\n },\n uniqueIndexes: [{ name: 'intake_settings_org_user_unique', columns: ['org_id', 'user_id'] }],\n};\n\n/** What the org effectively saw while selections were personal: a member's sources fed the shared board only while their switch was on. */\nexport function mergeIntakeSelections(configs: readonly IntakeConfig[]): IntakeConfig {\n const merged: IntakeConfig = Object.create(null);\n for (const config of configs) {\n for (const [integrationId, selection] of Object.entries(config)) {\n const current = merged[integrationId] ?? { enabled: false, sourceIds: null };\n const syncing = selection.enabled ? (selection.sourceIds ?? []) : [];\n const sourceIds = [...new Set([...(current.sourceIds ?? []), ...syncing])];\n merged[integrationId] = {\n enabled: current.enabled || selection.enabled,\n sourceIds: sourceIds.length ? sourceIds : null,\n };\n }\n }\n return merged;\n}\n\n/**\n * Binds an intake source to the Factory project its items belong to.\n *\n * Intake selections are org-wide, so they cannot say *where* an\n * ingested item should land. GitHub items are naturally scoped by their linked\n * repository; providers without that link (Linear) need this explicit binding\n * so viewing one project's board cannot materialize another project's items.\n *\n * Frozen: its unique index allows one binding per source, and indexes cannot be\n * narrowed, so bindings moved to {@link INTAKE_SOURCE_SUBSCRIPTIONS_SCHEMA}. Rows\n * are copied once at boot and never written again; the collection stays\n * registered so an earlier build that is rolled back to still finds them.\n */\nexport const INTAKE_SOURCE_BINDINGS_SCHEMA: CollectionSchema = {\n name: 'intake_source_bindings',\n columns: {\n id: { type: 'uuid-pk' },\n org_id: { type: 'text' },\n integration_id: { type: 'text' },\n source_id: { type: 'text' },\n factory_project_id: { type: 'text' },\n /** Installed board the source feeds; `null` keeps the legacy source-type routing (issues → work, PRs → review). */\n board: { type: 'text', nullable: true },\n created_by_user_id: { type: 'text', nullable: true },\n created_at: { type: 'timestamp' },\n updated_at: { type: 'timestamp' },\n },\n indexes: [{ name: 'intake_source_bindings_project_idx', columns: ['org_id', 'factory_project_id'] }],\n uniqueIndexes: [\n {\n name: 'intake_source_bindings_org_integration_source_unique',\n columns: ['org_id', 'integration_id', 'source_id'],\n },\n ],\n};\n\n/**\n * Binds an intake source to a Factory project, one row per (source, Factory\n * project). Most integrations keep one binding per source and rebinding moves\n * it; integrations with `Intake.bindingLabelFilters` let several Factory\n * projects share a source, each taking the issues carrying its own labels.\n */\nexport const INTAKE_SOURCE_SUBSCRIPTIONS_SCHEMA: CollectionSchema = {\n name: 'intake_source_subscriptions',\n columns: {\n id: { type: 'uuid-pk' },\n org_id: { type: 'text' },\n integration_id: { type: 'text' },\n source_id: { type: 'text' },\n factory_project_id: { type: 'text' },\n /** Installed board the source feeds; `null` keeps the legacy source-type routing (issues → work, PRs → review). */\n board: { type: 'text', nullable: true },\n /** Labels whose issues this binding takes, as the user typed them; `null` takes every unclaimed issue. */\n labels: { type: 'json', nullable: true },\n created_by_user_id: { type: 'text', nullable: true },\n created_at: { type: 'timestamp' },\n updated_at: { type: 'timestamp' },\n },\n indexes: [{ name: 'intake_source_subscriptions_project_idx', columns: ['org_id', 'factory_project_id'] }],\n uniqueIndexes: [\n {\n name: 'intake_source_subscriptions_org_integration_source_project_unique',\n columns: ['org_id', 'integration_id', 'source_id', 'factory_project_id'],\n },\n ],\n};\n\n/** One-time data migrations that have run, so none re-runs over rows users have changed since. */\nexport const INTAKE_DATA_MIGRATIONS_SCHEMA: CollectionSchema = {\n name: 'intake_data_migrations',\n columns: {\n name: { type: 'text', primaryKey: true },\n completed_at: { type: 'timestamp' },\n },\n};\n\nconst COPY_LEGACY_BINDINGS_MIGRATION = 'copy_intake_source_bindings_to_subscriptions';\n\nexport interface IntakeSourceBinding {\n integrationId: string;\n sourceId: string;\n factoryProjectId: string;\n /** Installed board the source feeds, or `null` for legacy source-type routing. */\n board: string | null;\n /** Labels whose issues this binding takes; `null` takes every issue no other binding's labels claim. */\n labels: string[] | null;\n createdAt: Date;\n}\n\n/** Another Factory project's binding that already takes some of the requested issues. */\nexport interface IntakeBindingConflict {\n factoryProjectId: string;\n /** The overlapping labels, or `null` when both bindings take all unclaimed issues. */\n labels: string[] | null;\n}\n\n/** Bindings on one source in precedence order: the oldest binding wins an issue two of them match. */\nfunction inPrecedenceOrder<T extends Pick<IntakeSourceBinding, 'createdAt' | 'factoryProjectId'>>(\n bindings: readonly T[],\n): T[] {\n return [...bindings].sort(\n (left, right) =>\n left.createdAt.getTime() - right.createdAt.getTime() ||\n left.factoryProjectId.localeCompare(right.factoryProjectId),\n );\n}\n\n/**\n * The slice of a shared source's issues `binding` takes, given every binding\n * on that source (`siblings` may include `binding` itself). Label bindings beat\n * the all-issues binding, and an issue carrying labels of two label bindings\n * goes to the older one.\n */\nexport function bindingLabelSlice(\n binding: Pick<IntakeSourceBinding, 'factoryProjectId' | 'labels' | 'createdAt'>,\n siblings: readonly Pick<IntakeSourceBinding, 'factoryProjectId' | 'labels' | 'createdAt'>[],\n): { include: string[] | null; exclude: string[] } {\n const labelled = inPrecedenceOrder(\n siblings.filter(sibling => sibling.labels !== null && sibling.factoryProjectId !== binding.factoryProjectId),\n );\n if (binding.labels === null) {\n return { include: null, exclude: labelled.flatMap(sibling => sibling.labels ?? []) };\n }\n const older = labelled.filter(sibling => inPrecedenceOrder([sibling, binding])[0] === sibling);\n return { include: binding.labels, exclude: older.flatMap(sibling => sibling.labels ?? []) };\n}\n\n/** Whether an issue carrying `labels` falls in `binding`'s slice of its source. */\nexport function bindingTakesIssue(\n binding: Pick<IntakeSourceBinding, 'factoryProjectId' | 'labels' | 'createdAt'>,\n siblings: readonly Pick<IntakeSourceBinding, 'factoryProjectId' | 'labels' | 'createdAt'>[],\n labels: readonly string[],\n): boolean {\n const carried = new Set(labels.map(normalizeIntakeLabel));\n const { include, exclude } = bindingLabelSlice(binding, siblings);\n if (exclude.some(label => carried.has(normalizeIntakeLabel(label)))) return false;\n return include === null || include.some(label => carried.has(normalizeIntakeLabel(label)));\n}\n\n/** Trim and de-duplicate case-insensitively, keeping the first spelling the user typed. */\nexport function normalizeBindingLabels(labels: readonly string[]): string[] {\n const seen = new Set<string>();\n const kept: string[] = [];\n for (const label of labels) {\n const trimmed = label.trim();\n const key = normalizeIntakeLabel(trimmed);\n if (!key || seen.has(key)) continue;\n seen.add(key);\n kept.push(trimmed);\n }\n return kept;\n}\n\n/**\n * Routes items carrying a label to an installed board, per Factory project.\n *\n * Repository-scoped providers (GitHub) already know which project an item\n * belongs to, but one repository can feed several boards: an issue labelled\n * `release` belongs on a release board while the rest stay on Work. Labels\n * without a route fall back to the provider's built-in board.\n */\nexport const INTAKE_LABEL_ROUTES_SCHEMA: CollectionSchema = {\n name: 'intake_label_routes',\n columns: {\n id: { type: 'uuid-pk' },\n org_id: { type: 'text' },\n factory_project_id: { type: 'text' },\n integration_id: { type: 'text' },\n /** Label as the provider reports it; matched case-insensitively. */\n label: { type: 'text' },\n board: { type: 'text' },\n created_by_user_id: { type: 'text', nullable: true },\n created_at: { type: 'timestamp' },\n updated_at: { type: 'timestamp' },\n },\n indexes: [{ name: 'intake_label_routes_project_idx', columns: ['org_id', 'factory_project_id'] }],\n uniqueIndexes: [\n {\n name: 'intake_label_routes_org_project_integration_label_unique',\n columns: ['org_id', 'factory_project_id', 'integration_id', 'label'],\n },\n ],\n};\n\nexport interface IntakeLabelRoute {\n factoryProjectId: string;\n integrationId: string;\n label: string;\n board: string;\n}\n\ntype IntakeLabelRouteRow = {\n factory_project_id: string;\n integration_id: string;\n label: string;\n board: string;\n};\nfunction toIntakeLabelRoute(row: IntakeLabelRouteRow): IntakeLabelRoute {\n return {\n factoryProjectId: row.factory_project_id,\n integrationId: row.integration_id,\n label: row.label,\n board: row.board,\n };\n}\n\n/** Labels are matched the way GitHub treats them: case-insensitively. */\nexport function normalizeIntakeLabel(label: string): string {\n return label.trim().toLowerCase();\n}\n\n/**\n * The board a labelled item should land on under `routes`, or `undefined` when\n * none of its labels is routed. Ties resolve to the first route in `routes`\n * order, so callers should pass a deterministically ordered list.\n */\nexport function resolveIntakeLabelRoute(\n routes: readonly IntakeLabelRoute[],\n labels: readonly string[] | undefined,\n): IntakeLabelRoute | undefined {\n if (!labels?.length) return undefined;\n const normalized = new Set(labels.map(normalizeIntakeLabel));\n return routes.find(route => normalized.has(normalizeIntakeLabel(route.label)));\n}\n\ntype IntakeSourceBindingRow = {\n org_id: string;\n integration_id: string;\n source_id: string;\n factory_project_id: string;\n board?: string | null;\n labels?: string[] | null;\n created_by_user_id?: string | null;\n created_at: Date;\n};\nfunction toIntakeSourceBinding(row: IntakeSourceBindingRow): IntakeSourceBinding {\n return {\n integrationId: row.integration_id,\n sourceId: row.source_id,\n factoryProjectId: row.factory_project_id,\n board: row.board ?? null,\n labels: row.labels ?? null,\n createdAt: row.created_at,\n };\n}\n\n/** Overlap between a requested binding and the other Factory projects' bindings on the same source. */\nfunction bindingConflicts(\n factoryProjectId: string,\n labels: string[] | null,\n rows: readonly IntakeSourceBindingRow[],\n): IntakeBindingConflict[] {\n const requested = new Set(labels?.map(normalizeIntakeLabel));\n return rows.flatMap((row): IntakeBindingConflict[] => {\n if (row.factory_project_id === factoryProjectId) return [];\n const theirs = row.labels ?? null;\n if (labels === null || theirs === null) {\n return labels === null && theirs === null ? [{ factoryProjectId: row.factory_project_id, labels: null }] : [];\n }\n const overlap = theirs.filter(label => requested.has(normalizeIntakeLabel(label)));\n return overlap.length > 0 ? [{ factoryProjectId: row.factory_project_id, labels: overlap }] : [];\n });\n}\n\n/** Update one (source, Factory project) binding in place, keeping its age, or insert it. */\nasync function upsertBinding(\n ops: FactoryStorageOps,\n key: { org_id: string; integration_id: string; source_id: string; factory_project_id: string },\n patch: { board: string | null; labels: string[] | null },\n userId: string | undefined,\n): Promise<void> {\n const now = new Date();\n const updated = await ops.updateMany('intake_source_subscriptions', key, { ...patch, updated_at: now });\n if (updated > 0) return;\n await ops.insertOne('intake_source_subscriptions', {\n ...key,\n ...patch,\n created_by_user_id: userId ?? null,\n created_at: now,\n updated_at: now,\n });\n}\n\nexport class IntakeStorage extends FactoryStorageDomain {\n constructor() {\n super('intake');\n }\n\n async init(): Promise<void> {\n await this.ensureCollections([\n INTAKE_ORG_SETTINGS_SCHEMA,\n LEGACY_INTAKE_USER_SETTINGS_SCHEMA,\n INTAKE_SOURCE_BINDINGS_SCHEMA,\n INTAKE_SOURCE_SUBSCRIPTIONS_SCHEMA,\n INTAKE_DATA_MIGRATIONS_SCHEMA,\n INTAKE_LABEL_ROUTES_SCHEMA,\n ]);\n await this.#foldLegacyUserSelections();\n await this.#copyLegacyBindings();\n }\n\n /**\n * Copy `intake_source_bindings` into `intake_source_subscriptions` as\n * all-issues bindings, once. The marker and the copy commit together, so a\n * boot racing another cannot re-copy a binding removed after the first\n * boot finished.\n */\n async #copyLegacyBindings(): Promise<void> {\n try {\n await this.storage.withTransaction(\n async ops => {\n if (await ops.findOne('intake_data_migrations', { name: COPY_LEGACY_BINDINGS_MIGRATION })) return;\n const legacyRows = await ops.findMany<IntakeSourceBindingRow & { updated_at: Date }>(\n 'intake_source_bindings',\n {},\n );\n for (const row of legacyRows) {\n const key = {\n org_id: row.org_id,\n integration_id: row.integration_id,\n source_id: row.source_id,\n factory_project_id: row.factory_project_id,\n };\n if (await ops.findOne('intake_source_subscriptions', key)) continue;\n await ops.insertOne('intake_source_subscriptions', {\n ...key,\n board: row.board ?? null,\n labels: null,\n created_by_user_id: row.created_by_user_id ?? null,\n created_at: row.created_at,\n updated_at: row.updated_at,\n });\n }\n await ops.insertOne('intake_data_migrations', {\n name: COPY_LEGACY_BINDINGS_MIGRATION,\n completed_at: new Date(),\n });\n },\n { isolationLevel: 'serializable' },\n );\n } catch (error) {\n // Postgres can report the boot that lost the race as a unique violation instead of a serialization failure.\n if (!(error instanceof UniqueViolationError)) throw error;\n }\n }\n\n async #foldLegacyUserSelections(): Promise<void> {\n const legacyRows = await this.ops.findMany<{ org_id: string; config: IntakeConfig }>('intake_settings', {});\n const byOrg = new Map<string, IntakeConfig[]>();\n for (const row of legacyRows) {\n byOrg.set(row.org_id, [...(byOrg.get(row.org_id) ?? []), row.config]);\n }\n for (const [orgId, configs] of byOrg) {\n const folded = await this.ops.findOne('intake_org_settings', { org_id: orgId });\n if (folded) continue;\n await this.#insertConfigIfAbsent(orgId, mergeIntakeSelections(configs));\n }\n }\n\n async dangerouslyClearAll(): Promise<void> {\n await this.ops.deleteMany('intake_org_settings', {});\n await this.ops.deleteMany('intake_settings', {});\n await this.ops.deleteMany('intake_source_bindings', {});\n await this.ops.deleteMany('intake_source_subscriptions', {});\n await this.ops.deleteMany('intake_data_migrations', {});\n await this.ops.deleteMany('intake_label_routes', {});\n }\n\n get #db(): FactoryStorageOps {\n return this.ops;\n }\n\n async getConfig({\n orgId,\n integrationIds,\n }: {\n orgId: string;\n /**\n * When provided, the result contains exactly these integration ids, with\n * unset integrations defaulting to `{ enabled: true, sourceIds: null }`.\n */\n integrationIds?: string[];\n }): Promise<IntakeConfig> {\n const row = await this.#db.findOne<{ config: IntakeConfig }>('intake_org_settings', { org_id: orgId });\n const saved = structuredClone(row?.config ?? DEFAULT_INTAKE_CONFIG);\n if (!integrationIds) return saved;\n return Object.fromEntries(\n integrationIds.map(integrationId => [integrationId, saved[integrationId] ?? { enabled: true, sourceIds: null }]),\n );\n }\n\n async saveConfig({ orgId, config }: { orgId: string; config: IntakeConfig }): Promise<void> {\n const now = new Date();\n const where = { org_id: orgId };\n const updated = await this.#db.updateMany('intake_org_settings', where, { config, updated_at: now });\n if (updated > 0) return;\n if (await this.#insertConfigIfAbsent(orgId, config)) return;\n await this.#db.updateMany('intake_org_settings', where, { config, updated_at: now });\n }\n\n /** Overlay `config` onto the stored selections; integrations it omits keep their stored value. */\n async mergeConfig({ orgId, config }: { orgId: string; config: IntakeConfig }): Promise<IntakeConfig> {\n const merged: IntakeConfig = Object.assign(Object.create(null), await this.getConfig({ orgId }), config);\n await this.saveConfig({ orgId, config: merged });\n return merged;\n }\n\n async #insertConfigIfAbsent(orgId: string, config: IntakeConfig): Promise<boolean> {\n const now = new Date();\n try {\n await this.#db.insertOne('intake_org_settings', { org_id: orgId, config, created_at: now, updated_at: now });\n return true;\n } catch (error) {\n if (error instanceof UniqueViolationError) return false;\n throw error;\n }\n }\n\n /** Every source binding in the org, optionally narrowed to one integration, oldest first. */\n async listBindings({\n orgId,\n integrationId,\n }: {\n orgId: string;\n integrationId?: string;\n }): Promise<IntakeSourceBinding[]> {\n const rows = await this.#db.findMany<IntakeSourceBindingRow>('intake_source_subscriptions', {\n org_id: orgId,\n ...(integrationId ? { integration_id: integrationId } : {}),\n });\n return inPrecedenceOrder(rows.map(toIntakeSourceBinding));\n }\n\n /** Source ids bound to one Factory project. Empty means \"nothing bound\". */\n async listBoundSourceIds({\n orgId,\n integrationId,\n factoryProjectId,\n }: {\n orgId: string;\n integrationId: string;\n factoryProjectId: string;\n }): Promise<string[]> {\n const rows = await this.#db.findMany<IntakeSourceBindingRow>('intake_source_subscriptions', {\n org_id: orgId,\n integration_id: integrationId,\n factory_project_id: factoryProjectId,\n });\n return rows.map(row => row.source_id);\n }\n\n /**\n * One Factory project's binding of a source, or `null` when it has none.\n * Without `factoryProjectId`, the source's oldest binding — for integrations\n * that keep one binding per source.\n */\n async getBinding({\n orgId,\n integrationId,\n sourceId,\n factoryProjectId,\n }: {\n orgId: string;\n integrationId: string;\n sourceId: string;\n factoryProjectId?: string;\n }): Promise<IntakeSourceBinding | null> {\n const rows = await this.#db.findMany<IntakeSourceBindingRow>('intake_source_subscriptions', {\n org_id: orgId,\n integration_id: integrationId,\n source_id: sourceId,\n ...(factoryProjectId ? { factory_project_id: factoryProjectId } : {}),\n });\n return inPrecedenceOrder(rows.map(toIntakeSourceBinding))[0] ?? null;\n }\n\n /**\n * Remove one Factory project's binding of a source, or — without\n * `factoryProjectId` — every binding of it. Returns the (oldest) removed\n * binding, or `null` when there was none.\n */\n async clearBinding({\n orgId,\n integrationId,\n sourceId,\n factoryProjectId,\n }: {\n orgId: string;\n integrationId: string;\n sourceId: string;\n factoryProjectId?: string;\n }): Promise<IntakeSourceBinding | null> {\n const where = {\n org_id: orgId,\n integration_id: integrationId,\n source_id: sourceId,\n ...(factoryProjectId ? { factory_project_id: factoryProjectId } : {}),\n };\n return this.storage.withTransaction(\n async ops => {\n const rows = await ops.findMany<IntakeSourceBindingRow>('intake_source_subscriptions', where);\n if (rows.length === 0) return null;\n await ops.deleteMany('intake_source_subscriptions', where);\n return inPrecedenceOrder(rows.map(toIntakeSourceBinding))[0]!;\n },\n { isolationLevel: 'serializable' },\n );\n }\n\n /**\n * Release every source a Factory project was bound to, so the others can take\n * them. Pass the transaction deleting the project, so no binding lands in between.\n */\n async clearProjectBindings(\n { orgId, factoryProjectId }: { orgId: string; factoryProjectId: string },\n ops: FactoryStorageOps = this.#db,\n ): Promise<void> {\n await ops.deleteMany('intake_source_subscriptions', { org_id: orgId, factory_project_id: factoryProjectId });\n }\n\n /**\n * Bind a source to one Factory project, moving it away from any other: the\n * source keeps a single binding, which takes all of its issues.\n */\n async setBinding({\n orgId,\n integrationId,\n sourceId,\n factoryProjectId,\n board = null,\n userId,\n }: {\n orgId: string;\n integrationId: string;\n sourceId: string;\n factoryProjectId: string;\n board?: string | null;\n userId?: string;\n }): Promise<void> {\n const source = { org_id: orgId, integration_id: integrationId, source_id: sourceId };\n await this.storage.withTransaction(\n async ops => {\n await assertFactoryProject(ops, orgId, factoryProjectId);\n const rows = await ops.findMany<IntakeSourceBindingRow>('intake_source_subscriptions', source);\n for (const row of rows) {\n if (row.factory_project_id === factoryProjectId) continue;\n await ops.deleteMany('intake_source_subscriptions', {\n ...source,\n factory_project_id: row.factory_project_id,\n });\n }\n await upsertBinding(ops, { ...source, factory_project_id: factoryProjectId }, { board, labels: null }, userId);\n },\n { isolationLevel: 'serializable' },\n );\n }\n\n /**\n * Bind a source to one Factory project alongside other Factory projects'\n * bindings of it, taking the issues carrying one of `labels` — or, with\n * `labels: null`, every issue no label binding claims. Rejected with the\n * overlapping bindings when another Factory project already takes some of\n * those labels, or already takes all unclaimed issues.\n */\n async setSharedBinding({\n orgId,\n integrationId,\n sourceId,\n factoryProjectId,\n board = null,\n labels,\n userId,\n }: {\n orgId: string;\n integrationId: string;\n sourceId: string;\n factoryProjectId: string;\n board?: string | null;\n labels: string[] | null;\n userId?: string;\n }): Promise<{ conflicts: IntakeBindingConflict[] }> {\n const source = { org_id: orgId, integration_id: integrationId, source_id: sourceId };\n const kept = labels === null ? null : normalizeBindingLabels(labels);\n return this.storage.withTransaction(\n async ops => {\n await assertFactoryProject(ops, orgId, factoryProjectId);\n const rows = await ops.findMany<IntakeSourceBindingRow>('intake_source_subscriptions', source);\n const conflicts = bindingConflicts(factoryProjectId, kept, rows);\n if (conflicts.length > 0) return { conflicts };\n await upsertBinding(ops, { ...source, factory_project_id: factoryProjectId }, { board, labels: kept }, userId);\n return { conflicts: [] };\n },\n { isolationLevel: 'serializable' },\n );\n }\n\n /** Atomically replace legacy provider source ids in both selection and routing state. */\n async migrateSourceIds({\n orgId,\n integrationId,\n migrations,\n }: {\n orgId: string;\n integrationId: string;\n migrations: Array<{ from: string; to: string }>;\n }): Promise<{ migrations: Array<{ from: string; to: string }>; conflicts: Array<{ from: string; to: string }> }> {\n if (migrations.length === 0) return { migrations: [], conflicts: [] };\n return this.storage.withTransaction(\n async ops => {\n const bindings = await ops.findMany<IntakeSourceBindingRow>('intake_source_subscriptions', {\n org_id: orgId,\n integration_id: integrationId,\n });\n const bindingsBySource = new Map<string, IntakeSourceBindingRow[]>();\n for (const binding of bindings) {\n bindingsBySource.set(binding.source_id, [...(bindingsBySource.get(binding.source_id) ?? []), binding]);\n }\n // Where a source's issues land: every Factory project, board and label slice bound to it.\n const destination = (rows: IntakeSourceBindingRow[]) =>\n rows\n .map(row => `${row.factory_project_id}\\0${row.board ?? ''}\\0${JSON.stringify(row.labels ?? null)}`)\n .sort()\n .join('\\n');\n const grouped = new Map<string, Array<{ from: string; to: string }>>();\n for (const migration of migrations) {\n grouped.set(migration.to, [...(grouped.get(migration.to) ?? []), migration]);\n }\n const conflicts: Array<{ from: string; to: string }> = [];\n for (const [to, group] of grouped) {\n const candidates = [bindingsBySource.get(to), ...group.map(({ from }) => bindingsBySource.get(from))].filter(\n (rows): rows is IntakeSourceBindingRow[] => rows !== undefined,\n );\n if (new Set(candidates.map(destination)).size > 1) conflicts.push(...group);\n }\n if (conflicts.length > 0) return { migrations: [], conflicts };\n\n const now = new Date();\n for (const [to, group] of grouped) {\n const legacy = group.filter(({ from }) => bindingsBySource.has(from));\n const scope = { org_id: orgId, integration_id: integrationId };\n if (!bindingsBySource.has(to) && legacy.length > 0) {\n const [first, ...duplicates] = legacy;\n await ops.updateMany(\n 'intake_source_subscriptions',\n { ...scope, source_id: first!.from },\n { source_id: to, updated_at: now },\n );\n for (const duplicate of duplicates) {\n await ops.deleteMany('intake_source_subscriptions', { ...scope, source_id: duplicate.from });\n }\n } else {\n for (const entry of legacy) {\n await ops.deleteMany('intake_source_subscriptions', { ...scope, source_id: entry.from });\n }\n }\n }\n\n const configRow = await ops.findOne<{ config: IntakeConfig }>('intake_org_settings', { org_id: orgId });\n if (configRow) {\n const replacements = new Map(migrations.map(({ from, to }) => [from, to] as const));\n const selected = configRow.config[integrationId]?.sourceIds ?? [];\n const nextSelected = [...new Set(selected.map(sourceId => replacements.get(sourceId) ?? sourceId))];\n if (\n nextSelected.length !== selected.length ||\n nextSelected.some((sourceId, index) => sourceId !== selected[index])\n ) {\n const config = structuredClone(configRow.config);\n config[integrationId] = {\n ...(config[integrationId] ?? { enabled: true }),\n sourceIds: nextSelected.length > 0 ? nextSelected : null,\n };\n await ops.updateMany('intake_org_settings', { org_id: orgId }, { config, updated_at: now });\n }\n }\n return { migrations, conflicts: [] };\n },\n { isolationLevel: 'serializable' },\n );\n }\n\n /** Label routes in the org, optionally narrowed to one project and/or integration. Sorted by label. */\n async listLabelRoutes({\n orgId,\n factoryProjectId,\n integrationId,\n }: {\n orgId: string;\n factoryProjectId?: string;\n integrationId?: string;\n }): Promise<IntakeLabelRoute[]> {\n const rows = await this.#db.findMany<IntakeLabelRouteRow>('intake_label_routes', {\n org_id: orgId,\n ...(factoryProjectId ? { factory_project_id: factoryProjectId } : {}),\n ...(integrationId ? { integration_id: integrationId } : {}),\n });\n return rows.map(toIntakeLabelRoute).sort((a, b) => a.label.localeCompare(b.label));\n }\n\n async setLabelRoute({\n orgId,\n factoryProjectId,\n integrationId,\n label,\n board,\n userId,\n }: {\n orgId: string;\n factoryProjectId: string;\n integrationId: string;\n label: string;\n board: string;\n userId?: string;\n }): Promise<void> {\n const where = {\n org_id: orgId,\n factory_project_id: factoryProjectId,\n integration_id: integrationId,\n label: normalizeIntakeLabel(label),\n };\n const now = new Date();\n const patch = { board, updated_at: now };\n const updated = await this.#db.updateMany('intake_label_routes', where, patch);\n if (updated > 0) return;\n try {\n await this.#db.insertOne('intake_label_routes', {\n ...where,\n board,\n created_by_user_id: userId ?? null,\n created_at: now,\n updated_at: now,\n });\n } catch (error) {\n if (!(error instanceof UniqueViolationError)) throw error;\n await this.#db.updateMany('intake_label_routes', where, patch);\n }\n }\n\n /** Remove a label route; returns the route that was removed, or `null` when none existed. */\n async clearLabelRoute({\n orgId,\n factoryProjectId,\n integrationId,\n label,\n }: {\n orgId: string;\n factoryProjectId: string;\n integrationId: string;\n label: string;\n }): Promise<IntakeLabelRoute | null> {\n const where = {\n org_id: orgId,\n factory_project_id: factoryProjectId,\n integration_id: integrationId,\n label: normalizeIntakeLabel(label),\n };\n return this.storage.withTransaction(\n async ops => {\n const row = await ops.findOne<IntakeLabelRouteRow>('intake_label_routes', where);\n if (!row) return null;\n await ops.deleteMany('intake_label_routes', where);\n return toIntakeLabelRoute(row);\n },\n { isolationLevel: 'serializable' },\n );\n }\n}\n"],"mappings":"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"}