History

Add @convex-dev/migrations dependency and refactor organization handling

- Introduced the `@convex-dev/migrations` package to manage database migrations. - Updated organization and workspace creation logic to support anonymous organizations and workspaces. - Refactored session context to include new handling for anonymous mode. - Enhanced UI components to reflect changes in organization and workspace naming conventions. - Improved type definitions and API interactions for better clarity and functionality.

Rhys Sullivan committed 9 months ago

Commit ab83e641 parent ad7cb939 files · +347 −70Browse files

Unified · Split

9 files
  1. bun.lock +3 −0
  2. executor/apps/web/src/components/app-shell.tsx +19 −18
  3. executor/apps/web/src/lib/session-context.tsx +58 −45
  4. executor/convex/_generated/api.d.ts +160 −0
  5. executor/convex/convex.config.ts +2 −0
  6. executor/convex/database.ts +6 −4
  7. executor/convex/migrations.ts +95 −0
  8. executor/convex/schema.ts +3 −3
  9. package.json +1 −0
bun.lock+3−0
@@ -6,6 +6,7 @@ · 5 unmodified lines
66 "name": "proto-monorepo",
77 "dependencies": {
88 "@apidevtools/swagger-parser": "^12.1.0",
9+ "@convex-dev/migrations": "^0.3.1",
910 "@convex-dev/stripe": "^0.1.3",
1011 "@convex-dev/workos-authkit": "^0.1.6",
1112 "@executor/contracts": "workspace:*",
@@ -284,6 +285,8 @@ · 272 unmodified lines
284285
285286 "@borewit/text-codec": ["@borewit/text-codec@0.2.1", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="],
286287
288+ "@convex-dev/migrations": ["@convex-dev/migrations@0.3.1", "", { "peerDependencies": { "convex": "^1.24.8" } }, "sha512-dGiN82vWMN415/AGMcVooDOJk8iTousZahBBs27uA03pL74Uo3ksucUW3CiCfPx75rv+x9UnenEakimQYYNIrA=="],
289+
287290 "@convex-dev/stripe": ["@convex-dev/stripe@0.1.3", "", { "dependencies": { "stripe": "^20.0.0" }, "peerDependencies": { "convex": "^1.29.3", "react": "^18.3.1 || ^19.0.0" } }, "sha512-yL0ygrWrjto9f7rprCb4KYp+p3Oka7KG5e3HBoJ9qSzbncpavZI+JTkGZnLCYZjPOo2ByCrBJvtGAxfP169Nzw=="],
288291
289292 "@convex-dev/workos": ["@convex-dev/workos@0.0.1", "", { "dependencies": { "@workos-inc/authkit-react": "^0.11.0", "tsdown": "^0.12.7", "vitest": "^3.1.4" }, "peerDependencies": { "convex": "^1.25.4", "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0" } }, "sha512-8gZOgmcTitcKXwagdU69XC4Va6wMPFIhSqSOEaXmFXMEPtkMgxPW1dhJzrmm9UQ4iRgZsckjd2O5aQjUH7kHGQ=="],
executor/apps/web/src/components/app-shell.tsx+19−18
@@ -104,12 +104,13 @@ function WorkspaceSelector({ inHeader = false }: { inHeader?: boolean }) { · 103 unmodified lines
104104 const activeWorkspace = context
105105 ? workspaces.find((workspace) => workspace.id === context.workspaceId)
106106 : null;
107+ const supportsOrganizationManagement = mode !== "guest";
107108 const activeOrganizationLabel = activeWorkspace?.organizationName ?? "Organization";
108− const activeWorkspaceLabel = activeWorkspace?.name ?? (mode === "guest" ? "Guest Workspace" : "Select workspace");
109+ const activeWorkspaceLabel = activeWorkspace?.name ?? (mode === "guest" ? "Anonymous Workspace" : "Select workspace");
109110 const activeWorkspaceInitial = (activeWorkspaceLabel[0] ?? "W").toUpperCase();
110111
111112 const workspaceGroups = useMemo(() => {
112− if (mode !== "workos") {
113+ if (!supportsOrganizationManagement) {
113114 return [];
114115 }
115116
@@ -129,7 +130,7 @@ function WorkspaceSelector({ inHeader = false }: { inHeader?: boolean }) { · 13 unmodified lines
129130 groups.push({
130131 key,
131132 organizationId: workspace.organizationId,
132− organizationName: workspace.organizationName ?? "Organization",
133+ organizationName: workspace.organizationName,
133134 workspaces: [workspace],
134135 });
135136 continue;
@@ -139,7 +140,7 @@ function WorkspaceSelector({ inHeader = false }: { inHeader?: boolean }) { · 3 unmodified lines
139140 }
140141
141142 return groups;
142− }, [mode, workspaces]);
143+ }, [supportsOrganizationManagement, workspaces]);
143144
144145 const openCreateWorkspaceForOrganization = (
145146 organizationId: (typeof workspaces)[number]["organizationId"],
@@ -225,7 +226,7 @@ function WorkspaceSelector({ inHeader = false }: { inHeader?: boolean }) { · 79 unmodified lines
225226 </span>
226227 )}
227228 <span className="min-w-0">
228− {mode === "workos" ? (
229+ {supportsOrganizationManagement ? (
229230 <>
230231 <span className="truncate block text-[10px] text-muted-foreground">{activeOrganizationLabel}</span>
231232 <span className="truncate block">{activeWorkspaceLabel}</span>
@@ -239,7 +240,7 @@ function WorkspaceSelector({ inHeader = false }: { inHeader?: boolean }) { · 7 unmodified lines
239240 </Button>
240241 </DropdownMenuTrigger>
241242 <DropdownMenuContent align="end" className="w-72">
242− {mode === "workos"
243+ {supportsOrganizationManagement
243244 ? (
244245 <>
245246 {workspaceGroups.map((group, groupIndex) => {
@@ -319,7 +320,7 @@ function WorkspaceSelector({ inHeader = false }: { inHeader?: boolean }) { · 73 unmodified lines
319320 Guest workspace
320321 </DropdownMenuItem>
321322 )}
322− {mode === "workos" ? (
323+ {supportsOrganizationManagement ? (
323324 <>
324325 <DropdownMenuSeparator />
325326 <DropdownMenuItem onSelect={openCreateOrganization} className="text-xs">
@@ -668,8 +669,8 @@ function NoOrganizationModal({ enabled }: { enabled: boolean }) { · 342 unmodified lines
668669 organizationsLoading,
669670 context,
670671 isSignedInToWorkos,
671− createAnonymousWorkspace,
672− creatingAnonymousWorkspace,
672+ createAnonymousOrganization,
673+ creatingAnonymousOrganization,
673674 } = useSession();
674675 const [error, setError] = useState<string | null>(null);
675676
@@ -680,12 +681,12 @@ function NoOrganizationModal({ enabled }: { enabled: boolean }) { · 4 unmodified lines
680681 && !isSignedInToWorkos
681682 && organizations.length === 0;
682683
683− const handleCreateAnonymousWorkspace = async () => {
684+ const handleCreateAnonymousOrganization = async () => {
684685 setError(null);
685686 try {
686− await createAnonymousWorkspace();
687+ await createAnonymousOrganization();
687688 } catch (cause) {
688− const message = cause instanceof Error ? cause.message : "Failed to create anonymous workspace";
689+ const message = cause instanceof Error ? cause.message : "Failed to create anonymous organization";
689690 setError(message);
690691 }
691692 };
@@ -700,12 +701,12 @@ function NoOrganizationModal({ enabled }: { enabled: boolean }) { · 8 unmodified lines
700701 <DialogHeader>
701702 <DialogTitle>Choose how to continue</DialogTitle>
702703 <DialogDescription>
703− Sign in to access your organizations, or create an anonymous workspace to continue as a guest.
704+ Sign in to access your organizations, or create an anonymous organization with a default workspace.
704705 </DialogDescription>
705706 </DialogHeader>
706707 <div className="space-y-2">
707708 {workosEnabled ? (
708− <Button asChild className="w-full" disabled={creatingAnonymousWorkspace}>
709+ <Button asChild className="w-full" disabled={creatingAnonymousOrganization}>
709710 <Link to="/sign-in" reloadDocument className="gap-2">
710711 <LogIn className="h-4 w-4" />
711712 Sign in
@@ -715,13 +716,13 @@ function NoOrganizationModal({ enabled }: { enabled: boolean }) { · 3 unmodified lines
715716 <Button
716717 variant="outline"
717718 className="w-full"
718− onClick={handleCreateAnonymousWorkspace}
719− disabled={creatingAnonymousWorkspace || !anonymousDemoEnabled}
719+ onClick={handleCreateAnonymousOrganization}
720+ disabled={creatingAnonymousOrganization || !anonymousDemoEnabled}
720721 >
721− {creatingAnonymousWorkspace ? "Creating anonymous workspace..." : "Create anonymous workspace"}
722+ {creatingAnonymousOrganization ? "Creating anonymous organization..." : "Create anonymous organization"}
722723 </Button>
723724 {!anonymousDemoEnabled ? (
724− <p className="text-xs text-muted-foreground">Anonymous workspace creation is disabled.</p>
725+ <p className="text-xs text-muted-foreground">Anonymous organization creation is disabled.</p>
725726 ) : null}
726727 {error ? <p className="text-xs text-destructive">{error}</p> : null}
727728 </div>
executor/apps/web/src/lib/session-context.tsx+58−45
@@ -29,7 +29,7 @@ interface SessionState { · 28 unmodified lines
2929 workspaceRestrictions: boolean;
3030 };
3131 } | null;
32− mode: "guest" | "workos";
32+ mode: "guest" | "workos" | "anonymous";
3333 organizations: Array<{
3434 id: Id<"organizations">;
3535 name: string;
@@ -43,8 +43,8 @@ interface SessionState { · 7 unmodified lines
4343 docId: Id<"workspaces"> | null;
4444 name: string;
4545 organizationId: Id<"organizations"> | null;
46− organizationName: string | null;
47− organizationSlug: string | null;
46+ organizationName: string;
47+ organizationSlug: string;
4848 iconUrl?: string | null;
4949 }>;
5050 switchWorkspace: (workspaceId: Id<"workspaces">) => void;
@@ -54,8 +54,8 @@ interface SessionState { · 3 unmodified lines
5454 iconFile?: File | null,
5555 organizationId?: Id<"organizations">,
5656 ) => Promise<void>;
57− creatingAnonymousWorkspace: boolean;
58− createAnonymousWorkspace: () => Promise<void>;
57+ creatingAnonymousOrganization: boolean;
58+ createAnonymousOrganization: () => Promise<void>;
5959 isSignedInToWorkos: boolean;
6060 workosProfile: {
6161 name: string;
@@ -76,9 +76,9 @@ interface WorkosAccount { · 14 unmodified lines
7676
7777 interface WorkspaceListItem {
7878 id: Id<"workspaces">;
79− organizationId: Id<"organizations"> | null;
80− organizationName: string | null;
81− organizationSlug: string | null;
79+ organizationId: Id<"organizations">;
80+ organizationName: string;
81+ organizationSlug: string;
8282 name: string;
8383 slug: string;
8484 iconUrl?: string | null;
@@ -97,8 +97,8 @@ const SessionContext = createContext<SessionState>({ · 12 unmodified lines
9797 switchWorkspace: () => {},
9898 creatingWorkspace: false,
9999 createWorkspace: async () => {},
100− creatingAnonymousWorkspace: false,
101− createAnonymousWorkspace: async () => {},
100+ creatingAnonymousOrganization: false,
101+ createAnonymousOrganization: async () => {},
102102 isSignedInToWorkos: false,
103103 workosProfile: null,
104104 resetWorkspace: async () => {},
@@ -147,7 +147,7 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 42 unmodified lines
147147 const createWorkspaceMutation = useMutation(convexApi.workspaces.create);
148148 const generateWorkspaceIconUploadUrl = useMutation(convexApi.workspaces.generateWorkspaceIconUploadUrl);
149149 const [creatingWorkspace, setCreatingWorkspace] = useState(false);
150− const [creatingAnonymousWorkspace, setCreatingAnonymousWorkspace] = useState(false);
150+ const [creatingAnonymousOrganization, setCreatingAnonymousOrganization] = useState(false);
151151 const [manualGuestContext, setManualGuestContext] = useState<AnonymousContext | null>(null);
152152
153153 const bootstrapSessionQuery = useTanstackQuery({
@@ -173,15 +173,15 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 19 unmodified lines
173173
174174 const account = useConvexQuery(
175175 convexApi.app.getCurrentAccount,
176− workosEnabled ? { sessionId: storedSessionId ?? undefined } : "skip",
176+ { sessionId: storedSessionId ?? undefined },
177177 ) as WorkosAccount | null | undefined;
178178 const workspaces = useConvexQuery(
179179 convexApi.workspaces.list,
180− workosEnabled ? { sessionId: storedSessionId ?? undefined } : "skip",
180+ { sessionId: storedSessionId ?? undefined },
181181 ) as WorkspaceListItem[] | undefined;
182182 const organizations = useConvexQuery(
183183 convexApi.organizations.listMine,
184− workosEnabled ? { sessionId: storedSessionId ?? undefined } : "skip",
184+ { sessionId: storedSessionId ?? undefined },
185185 );
186186
187187 const resolvedActiveWorkspaceId = useMemo(() => {
@@ -193,7 +193,7 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 5 unmodified lines
193193 return activeWorkspaceId;
194194 }
195195
196− const accountId = account?.provider === "workos" ? account._id : null;
196+ const accountId = account?._id ?? null;
197197 const accountStoredWorkspace = accountId ? readWorkspaceByAccount()[accountId] : null;
198198 if (accountStoredWorkspace && workspaces.some((workspace) => workspace.id === accountStoredWorkspace)) {
199199 return accountStoredWorkspace;
@@ -219,13 +219,13 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 19 unmodified lines
219219 setRuntimeError(null);
220220 }, []);
221221
222− const createAnonymousWorkspace = useCallback(async () => {
222+ const createAnonymousOrganization = useCallback(async () => {
223223 if (!anonymousDemoEnabled) {
224− throw new Error("Anonymous workspace creation is disabled");
224+ throw new Error("Anonymous organization creation is disabled");
225225 }
226226
227227 setRuntimeError(null);
228− setCreatingAnonymousWorkspace(true);
228+ setCreatingAnonymousOrganization(true);
229229 try {
230230 const context = await bootstrapAnonymousSession({});
231231 localStorage.setItem(SESSION_KEY, context.sessionId);
@@ -234,11 +234,11 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 2 unmodified lines
234234 setActiveWorkspaceId(context.workspaceId as Id<"workspaces">);
235235 setManualGuestContext(context);
236236 } catch (cause) {
237− const message = cause instanceof Error ? cause.message : "Failed to create anonymous workspace";
237+ const message = cause instanceof Error ? cause.message : "Failed to create anonymous organization";
238238 setRuntimeError(message);
239239 throw cause;
240240 } finally {
241− setCreatingAnonymousWorkspace(false);
241+ setCreatingAnonymousOrganization(false);
242242 }
243243 }, [bootstrapAnonymousSession]);
244244
@@ -246,7 +246,7 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 1 unmodified line
246246 setActiveWorkspaceId(workspaceId);
247247 localStorage.setItem(ACTIVE_WORKSPACE_KEY, workspaceId);
248248
249− if (account?.provider === "workos") {
249+ if (account) {
250250 const accountId = account._id;
251251 const byAccount = readWorkspaceByAccount();
252252 writeWorkspaceByAccount({
@@ -314,8 +314,8 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 61 unmodified lines
314314 switchWorkspace,
315315 ]);
316316
317− const workosContext = useMemo<AnonymousContext | null>(() => {
318− if (!workosEnabled || !account || account.provider !== "workos" || !workspaces || workspaces.length === 0) {
317+ const accountWorkspaceContext = useMemo<AnonymousContext | null>(() => {
318+ if (!account || !workspaces || workspaces.length === 0) {
319319 return null;
320320 }
321321
@@ -327,17 +327,26 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 5 unmodified lines
327327 return null;
328328 }
329329
330+ const sessionId = account.provider === "workos"
331+ ? `workos_${account._id}`
332+ : (storedSessionId ?? guestContext?.sessionId ?? null);
333+ if (!sessionId) {
334+ return null;
335+ }
336+
337+ const actorId = account.provider === "workos" ? account._id : account.providerAccountId;
338+
330339 return {
331− sessionId: `workos_${account._id}`,
340+ sessionId,
332341 workspaceId: activeWorkspace.id,
333− actorId: account._id,
342+ actorId,
334343 clientId: "web",
335344 accountId: account._id,
336345 userId: account._id,
337346 createdAt: Date.now(),
338347 lastSeenAt: Date.now(),
339348 };
340− }, [account, resolvedActiveWorkspaceId, workspaces]);
349+ }, [account, guestContext?.sessionId, resolvedActiveWorkspaceId, storedSessionId, workspaces]);
341350
342351 // When WorkOS is enabled, don't fall back to guest context while WorkOS
343352 // auth/account bootstrapping is still in flight. Otherwise workspace-bound
@@ -348,9 +357,11 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 4 unmodified lines
348357 || account === undefined
349358 || (account?.provider === "workos" && bootstrapWorkosAccountQuery.isFetching)
350359 );
351− const mode: "guest" | "workos" = workosContext ? "workos" : "guest";
360+ const mode: "guest" | "workos" | "anonymous" = accountWorkspaceContext
361+ ? (account?.provider === "workos" ? "workos" : "anonymous")
362+ : "guest";
352363 const shouldBlockGuestFallback = workosEnabled && account?.provider === "workos";
353− const context = workosContext ?? ((workosStillLoading || shouldBlockGuestFallback) ? null : guestContext);
364+ const context = accountWorkspaceContext ?? ((workosStillLoading || shouldBlockGuestFallback) ? null : guestContext);
354365
355366 const bootstrapSessionError =
356367 storedSessionId && bootstrapSessionQuery.error instanceof Error
@@ -367,22 +378,24 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 10 unmodified lines
367378 const error = runtimeError ?? bootstrapSessionError ?? bootstrapWorkosError;
368379
369380 const effectiveLoading = !context && !error && (
370− creatingAnonymousWorkspace
381+ creatingAnonymousOrganization
371382 || (storedSessionId !== null && bootstrapSessionQuery.isLoading)
372383 || workosStillLoading
373384 || bootstrapWorkosAccountQuery.isFetching
374385 );
375386 const workspaceOptions = useMemo(() => {
376− if (mode === "workos" && workspaces) {
377− return workspaces.map((workspace): SessionState["workspaces"][number] => ({
378− id: workspace.id,
379− docId: workspace.id,
380− name: workspace.name,
381− organizationId: workspace.organizationId ?? null,
382− organizationName: workspace.organizationName,
383− organizationSlug: workspace.organizationSlug,
384− iconUrl: workspace.iconUrl,
385− }));
387+ if (mode !== "guest" && workspaces) {
388+ return workspaces.map((workspace): SessionState["workspaces"][number] => {
389+ return {
390+ id: workspace.id,
391+ docId: workspace.id,
392+ name: workspace.name,
393+ organizationId: workspace.organizationId,
394+ organizationName: workspace.organizationName,
395+ organizationSlug: workspace.organizationSlug,
396+ iconUrl: workspace.iconUrl,
397+ };
398+ });
386399 }
387400
388401 if (guestContext) {
@@ -390,10 +403,10 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 1 unmodified line
390403 {
391404 id: guestContext.workspaceId as Id<"workspaces">,
392405 docId: null,
393− name: "Guest Workspace",
406+ name: "Anonymous Workspace",
394407 organizationId: null,
395− organizationName: null,
396− organizationSlug: null,
408+ organizationName: "Anonymous Organization",
409+ organizationSlug: "anonymous-organization",
397410 },
398411 ];
399412 }
@@ -410,13 +423,13 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 10 unmodified lines
410423 clientConfig: clientConfig ?? null,
411424 mode,
412425 organizations: organizations ?? [],
413− organizationsLoading: workosEnabled ? organizations === undefined : false,
426+ organizationsLoading: organizations === undefined,
414427 workspaces: workspaceOptions,
415428 switchWorkspace,
416429 creatingWorkspace,
417430 createWorkspace,
418− creatingAnonymousWorkspace,
419− createAnonymousWorkspace,
431+ creatingAnonymousOrganization,
432+ createAnonymousOrganization,
420433 isSignedInToWorkos: Boolean(account && account.provider === "workos"),
421434 workosProfile:
422435 account && account.provider === "workos"
executor/convex/_generated/api.d.ts+160−0
@@ -813,6 +813,80 @@ export declare const internal: { · 812 unmodified lines
813813 any
814814 >;
815815 };
816+ migrations: {
817+ deleteAnonymousSessionsMissingAccountId: FunctionReference<
818+ "mutation",
819+ "internal",
820+ {
821+ batchSize?: number;
822+ cursor?: string | null;
823+ dryRun?: boolean;
824+ fn?: string;
825+ next?: Array<string>;
826+ },
827+ any
828+ >;
829+ deleteInvitesMissingProvider: FunctionReference<
830+ "mutation",
831+ "internal",
832+ {
833+ batchSize?: number;
834+ cursor?: string | null;
835+ dryRun?: boolean;
836+ fn?: string;
837+ next?: Array<string>;
838+ },
839+ any
840+ >;
841+ deleteSourceCredentialsMissingProvider: FunctionReference<
842+ "mutation",
843+ "internal",
844+ {
845+ batchSize?: number;
846+ cursor?: string | null;
847+ dryRun?: boolean;
848+ fn?: string;
849+ next?: Array<string>;
850+ },
851+ any
852+ >;
853+ renameLegacyAnonymousOrganizations: FunctionReference<
854+ "mutation",
855+ "internal",
856+ {
857+ batchSize?: number;
858+ cursor?: string | null;
859+ dryRun?: boolean;
860+ fn?: string;
861+ next?: Array<string>;
862+ },
863+ any
864+ >;
865+ renameLegacyAnonymousWorkspaces: FunctionReference<
866+ "mutation",
867+ "internal",
868+ {
869+ batchSize?: number;
870+ cursor?: string | null;
871+ dryRun?: boolean;
872+ fn?: string;
873+ next?: Array<string>;
874+ },
875+ any
876+ >;
877+ run: FunctionReference<
878+ "mutation",
879+ "internal",
880+ {
881+ batchSize?: number;
882+ cursor?: string | null;
883+ dryRun?: boolean;
884+ fn?: string;
885+ next?: Array<string>;
886+ },
887+ any
888+ >;
889+ };
816890 openApiSpecCache: {
817891 getEntry: FunctionReference<
818892 "query",
@@ -1270,4 +1344,90 @@ export declare const components: { · 451 unmodified lines
12701344 >;
12711345 };
12721346 };
1347+ migrations: {
1348+ lib: {
1349+ cancel: FunctionReference<
1350+ "mutation",
1351+ "internal",
1352+ { name: string },
1353+ {
1354+ batchSize?: number;
1355+ cursor?: string | null;
1356+ error?: string;
1357+ isDone: boolean;
1358+ latestEnd?: number;
1359+ latestStart: number;
1360+ name: string;
1361+ next?: Array<string>;
1362+ processed: number;
1363+ state: "inProgress" | "success" | "failed" | "canceled" | "unknown";
1364+ }
1365+ >;
1366+ cancelAll: FunctionReference<
1367+ "mutation",
1368+ "internal",
1369+ { sinceTs?: number },
1370+ Array<{
1371+ batchSize?: number;
1372+ cursor?: string | null;
1373+ error?: string;
1374+ isDone: boolean;
1375+ latestEnd?: number;
1376+ latestStart: number;
1377+ name: string;
1378+ next?: Array<string>;
1379+ processed: number;
1380+ state: "inProgress" | "success" | "failed" | "canceled" | "unknown";
1381+ }>
1382+ >;
1383+ clearAll: FunctionReference<
1384+ "mutation",
1385+ "internal",
1386+ { before?: number },
1387+ null
1388+ >;
1389+ getStatus: FunctionReference<
1390+ "query",
1391+ "internal",
1392+ { limit?: number; names?: Array<string> },
1393+ Array<{
1394+ batchSize?: number;
1395+ cursor?: string | null;
1396+ error?: string;
1397+ isDone: boolean;
1398+ latestEnd?: number;
1399+ latestStart: number;
1400+ name: string;
1401+ next?: Array<string>;
1402+ processed: number;
1403+ state: "inProgress" | "success" | "failed" | "canceled" | "unknown";
1404+ }>
1405+ >;
1406+ migrate: FunctionReference<
1407+ "mutation",
1408+ "internal",
1409+ {
1410+ batchSize?: number;
1411+ cursor?: string | null;
1412+ dryRun: boolean;
1413+ fnHandle: string;
1414+ name: string;
1415+ next?: Array<{ fnHandle: string; name: string }>;
1416+ oneBatchOnly?: boolean;
1417+ },
1418+ {
1419+ batchSize?: number;
1420+ cursor?: string | null;
1421+ error?: string;
1422+ isDone: boolean;
1423+ latestEnd?: number;
1424+ latestStart: number;
1425+ name: string;
1426+ next?: Array<string>;
1427+ processed: number;
1428+ state: "inProgress" | "success" | "failed" | "canceled" | "unknown";
1429+ }
1430+ >;
1431+ };
1432+ };
12731433 };
executor/convex/convex.config.ts+2−0
@@ -1,3 +1,4 @@
1+import migrations from "@convex-dev/migrations/convex.config.js";
12 import stripe from "@convex-dev/stripe/convex.config.js";
23 import workOSAuthKit from "@convex-dev/workos-authkit/convex.config";
34 import { defineApp } from "convex/server";
@@ -6,5 +7,6 @@ const app = defineApp(); · 2 unmodified lines
67
78 app.use(workOSAuthKit);
89 app.use(stripe);
10+app.use(migrations);
911
1012 export default app;
executor/convex/database.ts+6−4
@@ -195,6 +195,8 @@ async function ensureAnonymousIdentity( · 194 unmodified lines
195195 timestamp: number;
196196 },
197197 ) {
198+ const anonymousOrganizationName = "Anonymous Organization";
199+ const anonymousWorkspaceName = "Anonymous Workspace";
198200 const now = params.timestamp;
199201
200202 let account = await ctx.db
@@ -226,10 +228,10 @@ async function ensureAnonymousIdentity( · 25 unmodified lines
226228 let organizationId: Doc<"organizations">["_id"];
227229
228230 if (!workspace) {
229− const organizationSlug = await ensureUniqueOrganizationSlug(ctx, "Guest Workspace");
231+ const organizationSlug = await ensureUniqueOrganizationSlug(ctx, anonymousOrganizationName);
230232 organizationId = await ctx.db.insert("organizations", {
231233 slug: organizationSlug,
232− name: "Guest Workspace",
234+ name: anonymousOrganizationName,
233235 status: "active",
234236 createdByAccountId: account._id,
235237 createdAt: now,
@@ -238,8 +240,8 @@ async function ensureAnonymousIdentity( · 2 unmodified lines
238240
239241 const workspaceId = await ctx.db.insert("workspaces", {
240242 organizationId,
241− slug: `guest-${crypto.randomUUID().slice(0, 8)}`,
242− name: "Guest Workspace",
243+ slug: `anonymous-${crypto.randomUUID().slice(0, 8)}`,
244+ name: anonymousWorkspaceName,
243245 plan: "free",
244246 createdByAccountId: account._id,
245247 createdAt: now,
executor/convex/migrations.ts+95−0
@@ -0,0 +1,95 @@
1+import { Migrations } from "@convex-dev/migrations";
2+import type { Id } from "./_generated/dataModel";
3+import type { DataModel } from "./_generated/dataModel";
4+import { components } from "./_generated/api";
5+
6+const migrations = new Migrations<DataModel>(components.migrations);
7+
8+const LEGACY_GUEST_NAME = "Guest Workspace";
9+const ANONYMOUS_ORGANIZATION_NAME = "Anonymous Organization";
10+const ANONYMOUS_WORKSPACE_NAME = "Anonymous Workspace";
11+
12+async function isAnonymousCreator(
13+ ctx: { db: { get: (id: Id<"accounts">) => Promise<{ provider: string } | null> } },
14+ accountId: Id<"accounts"> | undefined,
15+): Promise<boolean> {
16+ if (!accountId) {
17+ return false;
18+ }
19+
20+ const account = await ctx.db.get(accountId);
21+ return account?.provider === "anonymous";
22+}
23+
24+export const renameLegacyAnonymousOrganizations = migrations.define({
25+ table: "organizations",
26+ migrateOne: async (ctx, organization) => {
27+ if (organization.name !== LEGACY_GUEST_NAME) {
28+ return;
29+ }
30+
31+ const anonymousCreator = await isAnonymousCreator(ctx, organization.createdByAccountId);
32+ if (!anonymousCreator) {
33+ return;
34+ }
35+
36+ return {
37+ name: ANONYMOUS_ORGANIZATION_NAME,
38+ updatedAt: Date.now(),
39+ };
40+ },
41+});
42+
43+export const renameLegacyAnonymousWorkspaces = migrations.define({
44+ table: "workspaces",
45+ migrateOne: async (ctx, workspace) => {
46+ if (workspace.name !== LEGACY_GUEST_NAME) {
47+ return;
48+ }
49+
50+ const anonymousCreator = await isAnonymousCreator(ctx, workspace.createdByAccountId);
51+ if (!anonymousCreator) {
52+ return;
53+ }
54+
55+ return {
56+ name: ANONYMOUS_WORKSPACE_NAME,
57+ updatedAt: Date.now(),
58+ };
59+ },
60+});
61+
62+export const deleteAnonymousSessionsMissingAccountId = migrations.define({
63+ table: "anonymousSessions",
64+ migrateOne: async (ctx, session) => {
65+ if (session.accountId) {
66+ return;
67+ }
68+
69+ await ctx.db.delete(session._id);
70+ },
71+});
72+
73+export const deleteSourceCredentialsMissingProvider = migrations.define({
74+ table: "sourceCredentials",
75+ migrateOne: async (ctx, credential) => {
76+ if (credential.provider) {
77+ return;
78+ }
79+
80+ await ctx.db.delete(credential._id);
81+ },
82+});
83+
84+export const deleteInvitesMissingProvider = migrations.define({
85+ table: "invites",
86+ migrateOne: async (ctx, invite) => {
87+ if (invite.provider) {
88+ return;
89+ }
90+
91+ await ctx.db.delete(invite._id);
92+ },
93+});
94+
95+export const run = migrations.runner();
executor/convex/schema.ts+3−3
@@ -130,7 +130,7 @@ export default defineSchema({ · 129 unmodified lines
130130 email: v.string(),
131131 role: orgRole,
132132 status: inviteStatus,
133− provider: v.optional(inviteProvider),
133+ provider: inviteProvider,
134134 providerInviteId: v.optional(v.string()),
135135 invitedByAccountId: v.id("accounts"),
136136 expiresAt: v.number(),
@@ -263,7 +263,7 @@ export default defineSchema({ · 126 unmodified lines
263263 sourceKey: v.string(),
264264 scope: credentialScope,
265265 actorId: v.string(),
266− provider: v.optional(credentialProvider),
266+ provider: credentialProvider,
267267 secretJson: v.any(),
268268 createdAt: v.number(),
269269 updatedAt: v.number(),
@@ -333,7 +333,7 @@ export default defineSchema({ · 63 unmodified lines
333333 workspaceId: v.id("workspaces"),
334334 actorId: v.string(),
335335 clientId: v.string(),
336− accountId: v.optional(v.id("accounts")),
336+ accountId: v.id("accounts"),
337337 userId: v.optional(v.id("workspaceMembers")),
338338 createdAt: v.number(),
339339 lastSeenAt: v.number(),
package.json+1−0
@@ -29,6 +29,7 @@ · 28 unmodified lines
2929 },
3030 "dependencies": {
3131 "@apidevtools/swagger-parser": "^12.1.0",
32+ "@convex-dev/migrations": "^0.3.1",
3233 "@convex-dev/stripe": "^0.1.3",
3334 "@convex-dev/workos-authkit": "^0.1.6",
3435 "@executor/contracts": "workspace:*",