Skip to content

Commit 082501f

Browse files
Marfuenclaude
andauthored
fix(onboarding): add initialize-organization trigger task and recover… (#2512)
* fix(onboarding): add initialize-organization trigger task and recovery guard If createOrganizationMinimal partially fails (org created but initializeOrganization doesn't run), the org ends up with no framework instances, controls, policies, or tasks. completeOnboarding now detects this and runs initializeOrganization as recovery before triggering the onboard job. A standalone Trigger.dev task allows manual re-runs from the dashboard for orgs already in this broken state. Also saves raw framework IDs to context for reliable recovery lookups and upserts the onboarding record in case that was also missing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(onboarding): extract resolveFrameworkIds to shared helper Deduplicates the resolveFrameworkIds logic that was copied in both complete-onboarding.ts and initialize-organization.ts. Now lives in actions/organization/lib/resolve-framework-ids.ts alongside initialize-organization.ts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0ec7eed commit 082501f

File tree

4 files changed

+172
-5
lines changed

4 files changed

+172
-5
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { db } from '@db/server';
2+
3+
/**
4+
* Resolves framework IDs for an organization by:
5+
* 1. Checking for a raw frameworkIds context entry (JSON array, saved by newer code)
6+
* 2. Falling back to reverse-looking framework names from the onboarding context
7+
*/
8+
export async function resolveFrameworkIds(organizationId: string): Promise<string[]> {
9+
// Try the raw IDs context entry first (saved by newer createOrganizationMinimal)
10+
const rawIdsContext = await db.context.findFirst({
11+
where: {
12+
organizationId,
13+
question: 'frameworkIds',
14+
tags: { has: 'onboarding' },
15+
},
16+
});
17+
18+
if (rawIdsContext?.answer) {
19+
try {
20+
const ids = JSON.parse(rawIdsContext.answer);
21+
if (Array.isArray(ids) && ids.length > 0) {
22+
return ids;
23+
}
24+
} catch {
25+
// Fall through to name-based lookup
26+
}
27+
}
28+
29+
// Fall back to reverse-looking from framework names
30+
const frameworkContext = await db.context.findFirst({
31+
where: {
32+
organizationId,
33+
question: 'Which compliance frameworks do you need?',
34+
tags: { has: 'onboarding' },
35+
},
36+
});
37+
38+
if (!frameworkContext?.answer) {
39+
return [];
40+
}
41+
42+
const frameworkNames = frameworkContext.answer.split(',').map((name) => name.trim());
43+
44+
const frameworks = await db.frameworkEditorFramework.findMany({
45+
where: {
46+
name: { in: frameworkNames, mode: 'insensitive' },
47+
},
48+
select: { id: true },
49+
});
50+
51+
return frameworks.map((f) => f.id);
52+
}

apps/app/src/app/(app)/onboarding/actions/complete-onboarding.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
'use server';
22

3+
import { initializeOrganization } from '@/actions/organization/lib/initialize-organization';
4+
import { resolveFrameworkIds } from '@/actions/organization/lib/resolve-framework-ids';
35
import { authActionClientWithoutOrg } from '@/actions/safe-action';
46
import { steps } from '@/app/(app)/setup/lib/constants';
57
import { createFleetLabelForOrg } from '@/trigger/tasks/device/create-fleet-label-for-org';
@@ -155,6 +157,43 @@ export const completeOnboarding = authActionClientWithoutOrg
155157
data: { onboardingCompleted: true },
156158
});
157159

160+
// Ensure framework structure exists before triggering the onboard job.
161+
// If createOrganizationMinimal partially failed (org created but
162+
// initializeOrganization didn't run), recover by initializing now.
163+
const existingFrameworks = await db.frameworkInstance.findFirst({
164+
where: { organizationId: parsedInput.organizationId },
165+
});
166+
167+
if (!existingFrameworks) {
168+
console.warn(
169+
`[complete-onboarding] No framework instances found for org ${parsedInput.organizationId}, running initializeOrganization as recovery`,
170+
);
171+
172+
const frameworkIds = await resolveFrameworkIds(parsedInput.organizationId);
173+
174+
if (frameworkIds.length > 0) {
175+
await initializeOrganization({
176+
frameworkIds,
177+
organizationId: parsedInput.organizationId,
178+
});
179+
} else {
180+
console.error(
181+
`[complete-onboarding] Could not resolve framework IDs for org ${parsedInput.organizationId}`,
182+
);
183+
}
184+
}
185+
186+
// Ensure onboarding record exists (may be missing if createOrganizationMinimal
187+
// failed before creating it).
188+
await db.onboarding.upsert({
189+
where: { organizationId: parsedInput.organizationId },
190+
create: {
191+
organizationId: parsedInput.organizationId,
192+
triggerJobCompleted: false,
193+
},
194+
update: {},
195+
});
196+
158197
// Now trigger the jobs that were skipped during minimal creation
159198
const handle = await tasks.trigger<typeof onboardOrganizationTask>('onboard-organization', {
160199
organizationId: parsedInput.organizationId,
@@ -208,3 +247,4 @@ export const completeOnboarding = authActionClientWithoutOrg
208247
};
209248
}
210249
});
250+

apps/app/src/app/(app)/setup/actions/create-organization-minimal.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -134,12 +134,21 @@ export const createOrganizationMinimal = authActionClientWithoutOrg
134134
role: 'owner',
135135
},
136136
},
137-
// Only save the context for frameworkIds (we need this for later)
137+
// Save framework context: display names for AI prompts + raw IDs for recovery
138138
context: {
139-
create: {
140-
question: 'Which compliance frameworks do you need?',
141-
answer: frameworkNames || parsedInput.frameworkIds.join(', '),
142-
tags: ['onboarding'],
139+
createMany: {
140+
data: [
141+
{
142+
question: 'Which compliance frameworks do you need?',
143+
answer: frameworkNames || parsedInput.frameworkIds.join(', '),
144+
tags: ['onboarding'],
145+
},
146+
{
147+
question: 'frameworkIds',
148+
answer: JSON.stringify(parsedInput.frameworkIds),
149+
tags: ['onboarding'],
150+
},
151+
],
143152
},
144153
},
145154
},
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { initializeOrganization } from '@/actions/organization/lib/initialize-organization';
2+
import { resolveFrameworkIds } from '@/actions/organization/lib/resolve-framework-ids';
3+
import { db } from '@db/server';
4+
import { logger, queue, tags, task } from '@trigger.dev/sdk';
5+
6+
const initOrgQueue = queue({
7+
name: 'initialize-organization',
8+
concurrencyLimit: 10,
9+
});
10+
11+
/**
12+
* Standalone Trigger.dev task for initializing an organization's framework
13+
* structure (framework instances, controls, policies, tasks, requirement maps).
14+
*
15+
* Use cases:
16+
* - Manual re-run from the Trigger.dev dashboard for orgs stuck in a partial state
17+
* - Automatic recovery when `completeOnboarding` detects missing framework instances
18+
*
19+
* Accepts optional `frameworkIds`. When omitted, resolves them by reverse-looking
20+
* framework names stored in the organization's onboarding context.
21+
*/
22+
export const initializeOrganizationTask = task({
23+
id: 'initialize-organization',
24+
queue: initOrgQueue,
25+
retry: {
26+
maxAttempts: 3,
27+
},
28+
run: async (payload: { organizationId: string; frameworkIds?: string[] }) => {
29+
const { organizationId } = payload;
30+
await tags.add([`org:${organizationId}`]);
31+
32+
logger.info(`Initializing organization ${organizationId}`);
33+
34+
// Check if already initialized
35+
const existingFrameworks = await db.frameworkInstance.findFirst({
36+
where: { organizationId },
37+
});
38+
39+
if (existingFrameworks) {
40+
logger.info(
41+
`Organization ${organizationId} already has framework instances, skipping initialization`,
42+
);
43+
return { skipped: true, reason: 'already_initialized' };
44+
}
45+
46+
// Resolve framework IDs
47+
const frameworkIds = payload.frameworkIds ?? (await resolveFrameworkIds(organizationId));
48+
49+
if (frameworkIds.length === 0) {
50+
logger.error(`No framework IDs found for organization ${organizationId}`);
51+
throw new Error(
52+
`Cannot initialize organization ${organizationId}: no framework IDs found in context`,
53+
);
54+
}
55+
56+
logger.info(
57+
`Initializing organization ${organizationId} with frameworks: ${frameworkIds.join(', ')}`,
58+
);
59+
60+
await initializeOrganization({ frameworkIds, organizationId });
61+
62+
logger.info(`Successfully initialized organization ${organizationId}`);
63+
return { skipped: false, frameworkIds };
64+
},
65+
});
66+

0 commit comments

Comments
 (0)