Skip to content

Commit 10b51fe

Browse files
authored
Merge pull request #2647 from trycompai/fix/onboarding-transaction-perf
perf(onboarding): replace sequential update loops with bulk SQL
2 parents 0a77112 + 0c5d332 commit 10b51fe

1 file changed

Lines changed: 121 additions & 114 deletions

File tree

apps/app/src/actions/organization/lib/initialize-organization.ts

Lines changed: 121 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -216,61 +216,61 @@ export const _upsertOrgFrameworkStructureCore = async ({
216216
);
217217

218218
if (policyTemplatesForCreation.length > 0) {
219+
// Pre-generate Policy and PolicyVersion IDs in a single round-trip so we can
220+
// skip the post-insert findMany lookups and the per-policy update loop.
221+
// Policy.currentVersionId -> PolicyVersion.id and PolicyVersion.policyId ->
222+
// Policy.id form an FK cycle, so we insert Policy first (currentVersionId null),
223+
// insert PolicyVersion, then set currentVersionId in one bulk UPDATE.
224+
const idPairs = await tx.$queryRaw<
225+
Array<{ policy_id: string; version_id: string }>
226+
>`
227+
SELECT
228+
generate_prefixed_cuid('pol'::text) AS policy_id,
229+
generate_prefixed_cuid('pv'::text) AS version_id
230+
FROM generate_series(1, ${policyTemplatesForCreation.length}::int)
231+
`;
232+
const preparedPolicies = policyTemplatesForCreation.map((template, i) => ({
233+
template,
234+
policyId: idPairs[i].policy_id,
235+
versionId: idPairs[i].version_id,
236+
contentArray: extractTipTapContentArray(template.content),
237+
}));
238+
219239
await tx.policy.createMany({
220-
data: policyTemplatesForCreation.map((policyTemplate) => {
221-
const templateContent = policyTemplate.content;
222-
const contentArray = extractTipTapContentArray(templateContent);
223-
return {
224-
name: policyTemplate.name,
225-
description: policyTemplate.description,
226-
department: policyTemplate.department,
227-
frequency: policyTemplate.frequency,
228-
content: { set: contentArray },
229-
organizationId: organizationId,
230-
policyTemplateId: policyTemplate.id,
231-
};
232-
}),
240+
data: preparedPolicies.map(({ template, policyId, contentArray }) => ({
241+
id: policyId,
242+
name: template.name,
243+
description: template.description,
244+
department: template.department,
245+
frequency: template.frequency,
246+
content: { set: contentArray },
247+
organizationId: organizationId,
248+
policyTemplateId: template.id,
249+
})),
233250
});
234251

235-
// Fetch newly created policies to create versions for them
236-
const newlyCreatedPolicies = await tx.policy.findMany({
237-
where: {
238-
organizationId: organizationId,
239-
policyTemplateId: {
240-
in: policyTemplatesForCreation.map((t) => t.id),
241-
},
242-
},
243-
select: { id: true, policyTemplateId: true, content: true },
252+
await tx.policyVersion.createMany({
253+
data: preparedPolicies.map(({ policyId, versionId, contentArray }) => ({
254+
id: versionId,
255+
policyId,
256+
version: 1,
257+
content: { set: contentArray },
258+
changelog: 'Initial version from template',
259+
})),
244260
});
245261

246-
// Create version 1 for each newly created policy
247-
if (newlyCreatedPolicies.length > 0) {
248-
await tx.policyVersion.createMany({
249-
data: newlyCreatedPolicies.map((policy) => ({
250-
policyId: policy.id,
251-
version: 1,
252-
content: { set: policy.content as Prisma.InputJsonValue[] },
253-
changelog: 'Initial version from template',
254-
})),
255-
});
256-
257-
// Fetch the created versions to update policies with currentVersionId
258-
const createdVersions = await tx.policyVersion.findMany({
259-
where: {
260-
policyId: { in: newlyCreatedPolicies.map((p) => p.id) },
261-
version: 1,
262-
},
263-
select: { id: true, policyId: true },
264-
});
265-
266-
// Update each policy with its currentVersionId
267-
for (const version of createdVersions) {
268-
await tx.policy.update({
269-
where: { id: version.policyId },
270-
data: { currentVersionId: version.id },
271-
});
272-
}
273-
}
262+
const currentVersionValues = Prisma.join(
263+
preparedPolicies.map(
264+
({ policyId, versionId }) =>
265+
Prisma.sql`(${policyId}::text, ${versionId}::text)`,
266+
),
267+
);
268+
await tx.$executeRaw`
269+
UPDATE "Policy"
270+
SET "currentVersionId" = v.version_id
271+
FROM (VALUES ${currentVersionValues}) AS v(policy_id, version_id)
272+
WHERE "Policy".id = v.policy_id
273+
`;
274274
}
275275

276276
/**
@@ -350,6 +350,8 @@ export const _upsertOrgFrameworkStructureCore = async ({
350350
);
351351

352352
const requirementMapEntriesToCreate: Prisma.RequirementMapCreateManyInput[] = [];
353+
const controlToPolicyPairs: Array<{ controlId: string; policyId: string }> = [];
354+
const controlToTaskPairs: Array<{ controlId: string; taskId: string }> = [];
353355

354356
for (const controlTemplateRelation of groupedControlTemplateRelations) {
355357
const newControlId = controlTemplateIdToInstanceIdMap.get(
@@ -363,81 +365,86 @@ export const _upsertOrgFrameworkStructureCore = async ({
363365
continue;
364366
}
365367

366-
const updateData: Prisma.ControlUpdateInput = {};
367-
let needsUpdate = false;
368-
369368
// --- Process Requirements for RequirementMap ---
370-
if (controlTemplateRelation.requirementTemplateIds.length > 0) {
371-
for (const reqTemplateId of controlTemplateRelation.requirementTemplateIds) {
372-
let frameworkEditorFrameworkIdForReq: string | undefined;
373-
for (const fw of frameworkEditorFrameworks) {
374-
if (fw.requirements.some((r) => r.id === reqTemplateId)) {
375-
frameworkEditorFrameworkIdForReq = fw.id;
376-
break;
377-
}
378-
}
379-
const frameworkInstanceId = frameworkEditorFrameworkIdForReq
380-
? editorFrameworkIdToInstanceIdMap.get(frameworkEditorFrameworkIdForReq)
381-
: undefined;
382-
383-
if (frameworkInstanceId) {
384-
requirementMapEntriesToCreate.push({
385-
controlId: newControlId,
386-
requirementId: reqTemplateId,
387-
frameworkInstanceId: frameworkInstanceId,
388-
});
389-
} else {
390-
console.warn(
391-
`UpsertOrgFrameworkStructureCore: Could not find FrameworkInstanceId for editor requirement ID ${reqTemplateId}. Cannot create RequirementMap for Control ${newControlId}.`,
392-
);
369+
for (const reqTemplateId of controlTemplateRelation.requirementTemplateIds) {
370+
let frameworkEditorFrameworkIdForReq: string | undefined;
371+
for (const fw of frameworkEditorFrameworks) {
372+
if (fw.requirements.some((r) => r.id === reqTemplateId)) {
373+
frameworkEditorFrameworkIdForReq = fw.id;
374+
break;
393375
}
394376
}
377+
const frameworkInstanceId = frameworkEditorFrameworkIdForReq
378+
? editorFrameworkIdToInstanceIdMap.get(frameworkEditorFrameworkIdForReq)
379+
: undefined;
380+
381+
if (frameworkInstanceId) {
382+
requirementMapEntriesToCreate.push({
383+
controlId: newControlId,
384+
requirementId: reqTemplateId,
385+
frameworkInstanceId: frameworkInstanceId,
386+
});
387+
} else {
388+
console.warn(
389+
`UpsertOrgFrameworkStructureCore: Could not find FrameworkInstanceId for editor requirement ID ${reqTemplateId}. Cannot create RequirementMap for Control ${newControlId}.`,
390+
);
391+
}
395392
}
396393

397-
// --- Connect Policies ---
398-
if (controlTemplateRelation.policyTemplateIds.length > 0) {
399-
const policiesToConnect = [];
400-
for (const policyTemplateId of controlTemplateRelation.policyTemplateIds) {
401-
const newPolicyId = policyTemplateIdToInstanceIdMap.get(policyTemplateId);
402-
if (newPolicyId) {
403-
policiesToConnect.push({ id: newPolicyId });
404-
} else {
405-
console.warn(
406-
`UpsertOrgFrameworkStructureCore: Policy instance not found for template ID ${policyTemplateId}. Cannot connect to Control ${newControlId}.`,
407-
);
408-
}
409-
}
410-
if (policiesToConnect.length > 0) {
411-
updateData.policies = { connect: policiesToConnect };
412-
needsUpdate = true;
394+
// --- Collect Control <-> Policy pairs ---
395+
for (const policyTemplateId of controlTemplateRelation.policyTemplateIds) {
396+
const newPolicyId = policyTemplateIdToInstanceIdMap.get(policyTemplateId);
397+
if (newPolicyId) {
398+
controlToPolicyPairs.push({ controlId: newControlId, policyId: newPolicyId });
399+
} else {
400+
console.warn(
401+
`UpsertOrgFrameworkStructureCore: Policy instance not found for template ID ${policyTemplateId}. Cannot connect to Control ${newControlId}.`,
402+
);
413403
}
414404
}
415405

416-
// --- Connect Tasks ---
417-
if (controlTemplateRelation.taskTemplateIds.length > 0) {
418-
const tasksToConnect = [];
419-
for (const taskTemplateId of controlTemplateRelation.taskTemplateIds) {
420-
const newTaskId = taskTemplateIdToInstanceIdMap.get(taskTemplateId);
421-
if (newTaskId) {
422-
tasksToConnect.push({ id: newTaskId });
423-
} else {
424-
console.warn(
425-
`UpsertOrgFrameworkStructureCore: Task instance not found for template ID ${taskTemplateId}. Cannot connect to Control ${newControlId}.`,
426-
);
427-
}
428-
}
429-
if (tasksToConnect.length > 0) {
430-
updateData.tasks = { connect: tasksToConnect };
431-
needsUpdate = true;
406+
// --- Collect Control <-> Task pairs ---
407+
for (const taskTemplateId of controlTemplateRelation.taskTemplateIds) {
408+
const newTaskId = taskTemplateIdToInstanceIdMap.get(taskTemplateId);
409+
if (newTaskId) {
410+
controlToTaskPairs.push({ controlId: newControlId, taskId: newTaskId });
411+
} else {
412+
console.warn(
413+
`UpsertOrgFrameworkStructureCore: Task instance not found for template ID ${taskTemplateId}. Cannot connect to Control ${newControlId}.`,
414+
);
432415
}
433416
}
417+
}
434418

435-
if (needsUpdate) {
436-
await tx.control.update({
437-
where: { id: newControlId },
438-
data: updateData,
439-
});
440-
}
419+
// Bulk-insert into the implicit M2M join tables instead of N `control.update({ connect })`
420+
// calls. ON CONFLICT DO NOTHING preserves the idempotency the connect loop provided for
421+
// re-runs where some links already exist (e.g., adding a framework to an existing org).
422+
if (controlToPolicyPairs.length > 0) {
423+
const rows = Prisma.join(
424+
controlToPolicyPairs.map(
425+
({ controlId, policyId }) =>
426+
Prisma.sql`(${controlId}::text, ${policyId}::text)`,
427+
),
428+
);
429+
await tx.$executeRaw`
430+
INSERT INTO "_ControlToPolicy" ("A", "B")
431+
VALUES ${rows}
432+
ON CONFLICT ("A", "B") DO NOTHING
433+
`;
434+
}
435+
436+
if (controlToTaskPairs.length > 0) {
437+
const rows = Prisma.join(
438+
controlToTaskPairs.map(
439+
({ controlId, taskId }) =>
440+
Prisma.sql`(${controlId}::text, ${taskId}::text)`,
441+
),
442+
);
443+
await tx.$executeRaw`
444+
INSERT INTO "_ControlToTask" ("A", "B")
445+
VALUES ${rows}
446+
ON CONFLICT ("A", "B") DO NOTHING
447+
`;
441448
}
442449

443450
// --- Create RequirementMap entries ---

0 commit comments

Comments
 (0)