@mastra/factory 0.26.0-alpha.2 → 0.26.0-alpha.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/README.md +74 -7
- package/dist/boards/define-board.d.ts +19 -0
- package/dist/boards/define-board.d.ts.map +1 -1
- package/dist/boards/define-board.js +41 -9
- package/dist/boards/define-board.js.map +1 -1
- package/dist/boards/project-boards.d.ts +78 -0
- package/dist/boards/project-boards.d.ts.map +1 -0
- package/dist/boards/project-boards.js +194 -0
- package/dist/boards/project-boards.js.map +1 -0
- package/dist/boards/review.js +2 -2
- package/dist/boards/review.js.map +1 -1
- package/dist/boards/stored-board.d.ts +101 -0
- package/dist/boards/stored-board.d.ts.map +1 -0
- package/dist/boards/stored-board.js +139 -0
- package/dist/boards/stored-board.js.map +1 -0
- package/dist/boards/work.d.ts.map +1 -1
- package/dist/boards/work.js +6 -3
- package/dist/boards/work.js.map +1 -1
- package/dist/capabilities/intake.d.ts +6 -0
- package/dist/capabilities/intake.d.ts.map +1 -1
- package/dist/factory.d.ts +15 -2
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +49 -10
- package/dist/factory.js.map +1 -1
- package/dist/integrations/jira/binding-scope.d.ts +30 -0
- package/dist/integrations/jira/binding-scope.d.ts.map +1 -0
- package/dist/integrations/jira/binding-scope.js +50 -0
- package/dist/integrations/jira/binding-scope.js.map +1 -0
- package/dist/integrations/jira/claim.d.ts +12 -0
- package/dist/integrations/jira/claim.d.ts.map +1 -1
- package/dist/integrations/jira/claim.js +24 -1
- package/dist/integrations/jira/claim.js.map +1 -1
- package/dist/integrations/jira/intake-jql.d.ts +32 -0
- package/dist/integrations/jira/intake-jql.d.ts.map +1 -0
- package/dist/integrations/jira/intake-jql.js +57 -0
- package/dist/integrations/jira/intake-jql.js.map +1 -0
- package/dist/integrations/jira/integration.d.ts +4 -3
- package/dist/integrations/jira/integration.d.ts.map +1 -1
- package/dist/integrations/jira/integration.js +14 -21
- package/dist/integrations/jira/integration.js.map +1 -1
- package/dist/integrations/jira/routes.d.ts +3 -0
- package/dist/integrations/jira/routes.d.ts.map +1 -1
- package/dist/integrations/jira/routes.js +16 -29
- package/dist/integrations/jira/routes.js.map +1 -1
- package/dist/integrations/jira/rules.d.ts.map +1 -1
- package/dist/integrations/jira/rules.js +8 -13
- package/dist/integrations/jira/rules.js.map +1 -1
- package/dist/integrations/platform/jira/integration.d.ts +7 -1
- package/dist/integrations/platform/jira/integration.d.ts.map +1 -1
- package/dist/integrations/platform/jira/integration.js +26 -24
- package/dist/integrations/platform/jira/integration.js.map +1 -1
- package/dist/integrations/platform/jira/routes.d.ts +3 -0
- package/dist/integrations/platform/jira/routes.d.ts.map +1 -1
- package/dist/integrations/platform/jira/routes.js +17 -28
- package/dist/integrations/platform/jira/routes.js.map +1 -1
- package/dist/routes/attention-providers.d.ts +2 -1
- package/dist/routes/attention-providers.d.ts.map +1 -1
- package/dist/routes/attention-providers.js +3 -2
- package/dist/routes/attention-providers.js.map +1 -1
- package/dist/routes/attention.d.ts +2 -1
- package/dist/routes/attention.d.ts.map +1 -1
- package/dist/routes/attention.js.map +1 -1
- package/dist/routes/boards.d.ts +30 -0
- package/dist/routes/boards.d.ts.map +1 -0
- package/dist/routes/boards.js +272 -0
- package/dist/routes/boards.js.map +1 -0
- package/dist/routes/contracts.d.ts +249 -0
- package/dist/routes/contracts.d.ts.map +1 -1
- package/dist/routes/contracts.js +107 -35
- package/dist/routes/contracts.js.map +1 -1
- package/dist/routes/intake.d.ts +15 -10
- package/dist/routes/intake.d.ts.map +1 -1
- package/dist/routes/intake.js +109 -22
- package/dist/routes/intake.js.map +1 -1
- package/dist/routes/projects.d.ts +3 -0
- package/dist/routes/projects.d.ts.map +1 -1
- package/dist/routes/projects.js +10 -1
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/surface.d.ts +10 -3
- package/dist/routes/surface.d.ts.map +1 -1
- package/dist/routes/surface.js +12 -1
- package/dist/routes/surface.js.map +1 -1
- package/dist/routes/tenant-credentials.d.ts +9 -0
- package/dist/routes/tenant-credentials.d.ts.map +1 -1
- package/dist/routes/tenant-credentials.js +24 -2
- package/dist/routes/tenant-credentials.js.map +1 -1
- package/dist/routes/work-items.d.ts +3 -1
- package/dist/routes/work-items.d.ts.map +1 -1
- package/dist/routes/work-items.js +64 -42
- package/dist/routes/work-items.js.map +1 -1
- package/dist/rules/dispatcher.d.ts +7 -1
- package/dist/rules/dispatcher.d.ts.map +1 -1
- package/dist/rules/dispatcher.js +70 -34
- package/dist/rules/dispatcher.js.map +1 -1
- package/dist/rules/processor.d.ts +3 -1
- package/dist/rules/processor.d.ts.map +1 -1
- package/dist/rules/processor.js +11 -6
- package/dist/rules/processor.js.map +1 -1
- package/dist/rules/tools.d.ts +2 -1
- package/dist/rules/tools.d.ts.map +1 -1
- package/dist/rules/tools.js +2 -1
- package/dist/rules/tools.js.map +1 -1
- package/dist/rules/transition-service.d.ts +3 -1
- package/dist/rules/transition-service.d.ts.map +1 -1
- package/dist/rules/transition-service.js +12 -12
- package/dist/rules/transition-service.js.map +1 -1
- package/dist/storage/domains/audit/actions.d.ts +1 -0
- package/dist/storage/domains/audit/actions.d.ts.map +1 -1
- package/dist/storage/domains/audit/actions.js +6 -1
- package/dist/storage/domains/audit/actions.js.map +1 -1
- package/dist/storage/domains/boards/base.d.ts +110 -0
- package/dist/storage/domains/boards/base.d.ts.map +1 -0
- package/dist/storage/domains/boards/base.js +292 -0
- package/dist/storage/domains/boards/base.js.map +1 -0
- package/dist/storage/domains/boards/schemas.d.ts +11 -0
- package/dist/storage/domains/boards/schemas.d.ts.map +1 -0
- package/dist/storage/domains/boards/schemas.js +106 -0
- package/dist/storage/domains/boards/schemas.js.map +1 -0
- package/dist/storage/domains/credentials/base.d.ts +4 -1
- package/dist/storage/domains/credentials/base.d.ts.map +1 -1
- package/dist/storage/domains/credentials/base.js +5 -2
- package/dist/storage/domains/credentials/base.js.map +1 -1
- package/dist/storage/domains/intake/base.d.ts +86 -5
- package/dist/storage/domains/intake/base.d.ts.map +1 -1
- package/dist/storage/domains/intake/base.js +298 -59
- package/dist/storage/domains/intake/base.js.map +1 -1
- package/dist/storage/domains/projects/base.d.ts +2 -1
- package/dist/storage/domains/projects/base.d.ts.map +1 -1
- package/dist/storage/domains/projects/base.js +15 -10
- package/dist/storage/domains/projects/base.js.map +1 -1
- package/dist/storage/domains/work-items/base.d.ts +14 -0
- package/dist/storage/domains/work-items/base.d.ts.map +1 -1
- package/dist/storage/domains/work-items/base.js +39 -11
- package/dist/storage/domains/work-items/base.js.map +1 -1
- package/dist/tools/read-tools.d.ts +3 -1
- package/dist/tools/read-tools.d.ts.map +1 -1
- package/dist/tools/read-tools.js +3 -2
- package/dist/tools/read-tools.js.map +1 -1
- package/dist/tools/session-tools.d.ts +3 -1
- package/dist/tools/session-tools.d.ts.map +1 -1
- package/dist/tools/session-tools.js +2 -1
- package/dist/tools/session-tools.js.map +1 -1
- package/dist/tools/sessions-state-processor.d.ts +3 -1
- package/dist/tools/sessions-state-processor.d.ts.map +1 -1
- package/dist/tools/sessions-state-processor.js +2 -1
- package/dist/tools/sessions-state-processor.js.map +1 -1
- package/dist/tools/work-item-tools.d.ts +3 -1
- package/dist/tools/work-item-tools.d.ts.map +1 -1
- package/dist/tools/work-item-tools.js +3 -2
- package/dist/tools/work-item-tools.js.map +1 -1
- package/dist/workspace.js +1 -1
- package/factory-skills/factory-plan/SKILL.md +1 -1
- package/factory-skills/factory-triage/SKILL.md +18 -7
- package/package.json +9 -9
|
@@ -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("
|
|
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("
|
|
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
|
-
/**
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
462
|
+
source_id: sourceId,
|
|
463
|
+
...factoryProjectId ? { factory_project_id: factoryProjectId } : {}
|
|
464
|
+
})).map(toIntakeSourceBinding))[0] ?? null;
|
|
269
465
|
}
|
|
270
|
-
|
|
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
|
|
278
|
-
if (
|
|
279
|
-
await ops.deleteMany("
|
|
280
|
-
return toIntakeSourceBinding
|
|
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
|
|
500
|
+
const source = {
|
|
285
501
|
org_id: orgId,
|
|
286
502
|
integration_id: integrationId,
|
|
287
503
|
source_id: sourceId
|
|
288
504
|
};
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
await
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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("
|
|
560
|
+
const bindings = await ops.findMany("intake_source_subscriptions", {
|
|
318
561
|
org_id: orgId,
|
|
319
562
|
integration_id: integrationId
|
|
320
563
|
});
|
|
321
|
-
const
|
|
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 = [
|
|
327
|
-
if (new Set(candidates.map(
|
|
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
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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("
|
|
346
|
-
|
|
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("
|
|
354
|
-
|
|
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
|
|
359
|
-
|
|
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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"}
|