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
6 "name": "proto-monorepo",6 "name": "proto-monorepo",
7 "dependencies": {7 "dependencies": {
8 "@apidevtools/swagger-parser": "^12.1.0",8 "@apidevtools/swagger-parser": "^12.1.0",
9 "@convex-dev/migrations": "^0.3.1",
9 "@convex-dev/stripe": "^0.1.3",10 "@convex-dev/stripe": "^0.1.3",
10 "@convex-dev/workos-authkit": "^0.1.6",11 "@convex-dev/workos-authkit": "^0.1.6",
11 "@executor/contracts": "workspace:*",12 "@executor/contracts": "workspace:*",
@@ -284,6 +285,8 @@ · 272 unmodified lines
284 285
285 "@borewit/text-codec": ["@borewit/text-codec@0.2.1", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="],286 "@borewit/text-codec": ["@borewit/text-codec@0.2.1", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="],
286 287
288 "@convex-dev/migrations": ["@convex-dev/migrations@0.3.1", "", { "peerDependencies": { "convex": "^1.24.8" } }, "sha512-dGiN82vWMN415/AGMcVooDOJk8iTousZahBBs27uA03pL74Uo3ksucUW3CiCfPx75rv+x9UnenEakimQYYNIrA=="],
289
287 "@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=="],290 "@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=="],
288 291
289 "@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=="],292 "@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
104 const activeWorkspace = context104 const activeWorkspace = context
105 ? workspaces.find((workspace) => workspace.id === context.workspaceId)105 ? workspaces.find((workspace) => workspace.id === context.workspaceId)
106 : null;106 : null;
107 const supportsOrganizationManagement = mode !== "guest";
107 const activeOrganizationLabel = activeWorkspace?.organizationName ?? "Organization";108 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");
109 const activeWorkspaceInitial = (activeWorkspaceLabel[0] ?? "W").toUpperCase();110 const activeWorkspaceInitial = (activeWorkspaceLabel[0] ?? "W").toUpperCase();
110 111
111 const workspaceGroups = useMemo(() => {112 const workspaceGroups = useMemo(() => {
112 if (mode !== "workos") {113 if (!supportsOrganizationManagement) {
113 return [];114 return [];
114 }115 }
115 116
@@ -129,7 +130,7 @@ function WorkspaceSelector({ inHeader = false }: { inHeader?: boolean }) { · 13 unmodified lines
129 groups.push({130 groups.push({
130 key,131 key,
131 organizationId: workspace.organizationId,132 organizationId: workspace.organizationId,
132 organizationName: workspace.organizationName ?? "Organization",133 organizationName: workspace.organizationName,
133 workspaces: [workspace],134 workspaces: [workspace],
134 });135 });
135 continue;136 continue;
@@ -139,7 +140,7 @@ function WorkspaceSelector({ inHeader = false }: { inHeader?: boolean }) { · 3 unmodified lines
139 }140 }
140 141
141 return groups;142 return groups;
142 }, [mode, workspaces]);143 }, [supportsOrganizationManagement, workspaces]);
143 144
144 const openCreateWorkspaceForOrganization = (145 const openCreateWorkspaceForOrganization = (
145 organizationId: (typeof workspaces)[number]["organizationId"],146 organizationId: (typeof workspaces)[number]["organizationId"],
@@ -225,7 +226,7 @@ function WorkspaceSelector({ inHeader = false }: { inHeader?: boolean }) { · 79 unmodified lines
225 </span>226 </span>
226 )}227 )}
227 <span className="min-w-0">228 <span className="min-w-0">
228 {mode === "workos" ? (229 {supportsOrganizationManagement ? (
229 <>230 <>
230 <span className="truncate block text-[10px] text-muted-foreground">{activeOrganizationLabel}</span>231 <span className="truncate block text-[10px] text-muted-foreground">{activeOrganizationLabel}</span>
231 <span className="truncate block">{activeWorkspaceLabel}</span>232 <span className="truncate block">{activeWorkspaceLabel}</span>
@@ -239,7 +240,7 @@ function WorkspaceSelector({ inHeader = false }: { inHeader?: boolean }) { · 7 unmodified lines
239 </Button>240 </Button>
240 </DropdownMenuTrigger>241 </DropdownMenuTrigger>
241 <DropdownMenuContent align="end" className="w-72">242 <DropdownMenuContent align="end" className="w-72">
242 {mode === "workos"243 {supportsOrganizationManagement
243 ? (244 ? (
244 <>245 <>
245 {workspaceGroups.map((group, groupIndex) => {246 {workspaceGroups.map((group, groupIndex) => {
@@ -319,7 +320,7 @@ function WorkspaceSelector({ inHeader = false }: { inHeader?: boolean }) { · 73 unmodified lines
319 Guest workspace320 Guest workspace
320 </DropdownMenuItem>321 </DropdownMenuItem>
321 )}322 )}
322 {mode === "workos" ? (323 {supportsOrganizationManagement ? (
323 <>324 <>
324 <DropdownMenuSeparator />325 <DropdownMenuSeparator />
325 <DropdownMenuItem onSelect={openCreateOrganization} className="text-xs">326 <DropdownMenuItem onSelect={openCreateOrganization} className="text-xs">
@@ -668,8 +669,8 @@ function NoOrganizationModal({ enabled }: { enabled: boolean }) { · 342 unmodified lines
668 organizationsLoading,669 organizationsLoading,
669 context,670 context,
670 isSignedInToWorkos,671 isSignedInToWorkos,
671 createAnonymousWorkspace,672 createAnonymousOrganization,
672 creatingAnonymousWorkspace,673 creatingAnonymousOrganization,
673 } = useSession();674 } = useSession();
674 const [error, setError] = useState<string | null>(null);675 const [error, setError] = useState<string | null>(null);
675 676
@@ -680,12 +681,12 @@ function NoOrganizationModal({ enabled }: { enabled: boolean }) { · 4 unmodified lines
680 && !isSignedInToWorkos681 && !isSignedInToWorkos
681 && organizations.length === 0;682 && organizations.length === 0;
682 683
683 const handleCreateAnonymousWorkspace = async () => {684 const handleCreateAnonymousOrganization = async () => {
684 setError(null);685 setError(null);
685 try {686 try {
686 await createAnonymousWorkspace();687 await createAnonymousOrganization();
687 } catch (cause) {688 } 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";
689 setError(message);690 setError(message);
690 }691 }
691 };692 };
@@ -700,12 +701,12 @@ function NoOrganizationModal({ enabled }: { enabled: boolean }) { · 8 unmodified lines
700 <DialogHeader>701 <DialogHeader>
701 <DialogTitle>Choose how to continue</DialogTitle>702 <DialogTitle>Choose how to continue</DialogTitle>
702 <DialogDescription>703 <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.
704 </DialogDescription>705 </DialogDescription>
705 </DialogHeader>706 </DialogHeader>
706 <div className="space-y-2">707 <div className="space-y-2">
707 {workosEnabled ? (708 {workosEnabled ? (
708 <Button asChild className="w-full" disabled={creatingAnonymousWorkspace}>709 <Button asChild className="w-full" disabled={creatingAnonymousOrganization}>
709 <Link to="/sign-in" reloadDocument className="gap-2">710 <Link to="/sign-in" reloadDocument className="gap-2">
710 <LogIn className="h-4 w-4" />711 <LogIn className="h-4 w-4" />
711 Sign in712 Sign in
@@ -715,13 +716,13 @@ function NoOrganizationModal({ enabled }: { enabled: boolean }) { · 3 unmodified lines
715 <Button716 <Button
716 variant="outline"717 variant="outline"
717 className="w-full"718 className="w-full"
718 onClick={handleCreateAnonymousWorkspace}719 onClick={handleCreateAnonymousOrganization}
719 disabled={creatingAnonymousWorkspace || !anonymousDemoEnabled}720 disabled={creatingAnonymousOrganization || !anonymousDemoEnabled}
720 >721 >
721 {creatingAnonymousWorkspace ? "Creating anonymous workspace..." : "Create anonymous workspace"}722 {creatingAnonymousOrganization ? "Creating anonymous organization..." : "Create anonymous organization"}
722 </Button>723 </Button>
723 {!anonymousDemoEnabled ? (724 {!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>
725 ) : null}726 ) : null}
726 {error ? <p className="text-xs text-destructive">{error}</p> : null}727 {error ? <p className="text-xs text-destructive">{error}</p> : null}
727 </div>728 </div>
executor/apps/web/src/lib/session-context.tsx+58−45
@@ -29,7 +29,7 @@ interface SessionState { · 28 unmodified lines
29 workspaceRestrictions: boolean;29 workspaceRestrictions: boolean;
30 };30 };
31 } | null;31 } | null;
32 mode: "guest" | "workos";32 mode: "guest" | "workos" | "anonymous";
33 organizations: Array<{33 organizations: Array<{
34 id: Id<"organizations">;34 id: Id<"organizations">;
35 name: string;35 name: string;
@@ -43,8 +43,8 @@ interface SessionState { · 7 unmodified lines
43 docId: Id<"workspaces"> | null;43 docId: Id<"workspaces"> | null;
44 name: string;44 name: string;
45 organizationId: Id<"organizations"> | null;45 organizationId: Id<"organizations"> | null;
46 organizationName: string | null;46 organizationName: string;
47 organizationSlug: string | null;47 organizationSlug: string;
48 iconUrl?: string | null;48 iconUrl?: string | null;
49 }>;49 }>;
50 switchWorkspace: (workspaceId: Id<"workspaces">) => void;50 switchWorkspace: (workspaceId: Id<"workspaces">) => void;
@@ -54,8 +54,8 @@ interface SessionState { · 3 unmodified lines
54 iconFile?: File | null,54 iconFile?: File | null,
55 organizationId?: Id<"organizations">,55 organizationId?: Id<"organizations">,
56 ) => Promise<void>;56 ) => Promise<void>;
57 creatingAnonymousWorkspace: boolean;57 creatingAnonymousOrganization: boolean;
58 createAnonymousWorkspace: () => Promise<void>;58 createAnonymousOrganization: () => Promise<void>;
59 isSignedInToWorkos: boolean;59 isSignedInToWorkos: boolean;
60 workosProfile: {60 workosProfile: {
61 name: string;61 name: string;
@@ -76,9 +76,9 @@ interface WorkosAccount { · 14 unmodified lines
76 76
77interface WorkspaceListItem {77interface WorkspaceListItem {
78 id: Id<"workspaces">;78 id: Id<"workspaces">;
79 organizationId: Id<"organizations"> | null;79 organizationId: Id<"organizations">;
80 organizationName: string | null;80 organizationName: string;
81 organizationSlug: string | null;81 organizationSlug: string;
82 name: string;82 name: string;
83 slug: string;83 slug: string;
84 iconUrl?: string | null;84 iconUrl?: string | null;
@@ -97,8 +97,8 @@ const SessionContext = createContext<SessionState>({ · 12 unmodified lines
97 switchWorkspace: () => {},97 switchWorkspace: () => {},
98 creatingWorkspace: false,98 creatingWorkspace: false,
99 createWorkspace: async () => {},99 createWorkspace: async () => {},
100 creatingAnonymousWorkspace: false,100 creatingAnonymousOrganization: false,
101 createAnonymousWorkspace: async () => {},101 createAnonymousOrganization: async () => {},
102 isSignedInToWorkos: false,102 isSignedInToWorkos: false,
103 workosProfile: null,103 workosProfile: null,
104 resetWorkspace: async () => {},104 resetWorkspace: async () => {},
@@ -147,7 +147,7 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 42 unmodified lines
147 const createWorkspaceMutation = useMutation(convexApi.workspaces.create);147 const createWorkspaceMutation = useMutation(convexApi.workspaces.create);
148 const generateWorkspaceIconUploadUrl = useMutation(convexApi.workspaces.generateWorkspaceIconUploadUrl);148 const generateWorkspaceIconUploadUrl = useMutation(convexApi.workspaces.generateWorkspaceIconUploadUrl);
149 const [creatingWorkspace, setCreatingWorkspace] = useState(false);149 const [creatingWorkspace, setCreatingWorkspace] = useState(false);
150 const [creatingAnonymousWorkspace, setCreatingAnonymousWorkspace] = useState(false);150 const [creatingAnonymousOrganization, setCreatingAnonymousOrganization] = useState(false);
151 const [manualGuestContext, setManualGuestContext] = useState<AnonymousContext | null>(null);151 const [manualGuestContext, setManualGuestContext] = useState<AnonymousContext | null>(null);
152 152
153 const bootstrapSessionQuery = useTanstackQuery({153 const bootstrapSessionQuery = useTanstackQuery({
@@ -173,15 +173,15 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 19 unmodified lines
173 173
174 const account = useConvexQuery(174 const account = useConvexQuery(
175 convexApi.app.getCurrentAccount,175 convexApi.app.getCurrentAccount,
176 workosEnabled ? { sessionId: storedSessionId ?? undefined } : "skip",176 { sessionId: storedSessionId ?? undefined },
177 ) as WorkosAccount | null | undefined;177 ) as WorkosAccount | null | undefined;
178 const workspaces = useConvexQuery(178 const workspaces = useConvexQuery(
179 convexApi.workspaces.list,179 convexApi.workspaces.list,
180 workosEnabled ? { sessionId: storedSessionId ?? undefined } : "skip",180 { sessionId: storedSessionId ?? undefined },
181 ) as WorkspaceListItem[] | undefined;181 ) as WorkspaceListItem[] | undefined;
182 const organizations = useConvexQuery(182 const organizations = useConvexQuery(
183 convexApi.organizations.listMine,183 convexApi.organizations.listMine,
184 workosEnabled ? { sessionId: storedSessionId ?? undefined } : "skip",184 { sessionId: storedSessionId ?? undefined },
185 );185 );
186 186
187 const resolvedActiveWorkspaceId = useMemo(() => {187 const resolvedActiveWorkspaceId = useMemo(() => {
@@ -193,7 +193,7 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 5 unmodified lines
193 return activeWorkspaceId;193 return activeWorkspaceId;
194 }194 }
195 195
196 const accountId = account?.provider === "workos" ? account._id : null;196 const accountId = account?._id ?? null;
197 const accountStoredWorkspace = accountId ? readWorkspaceByAccount()[accountId] : null;197 const accountStoredWorkspace = accountId ? readWorkspaceByAccount()[accountId] : null;
198 if (accountStoredWorkspace && workspaces.some((workspace) => workspace.id === accountStoredWorkspace)) {198 if (accountStoredWorkspace && workspaces.some((workspace) => workspace.id === accountStoredWorkspace)) {
199 return accountStoredWorkspace;199 return accountStoredWorkspace;
@@ -219,13 +219,13 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 19 unmodified lines
219 setRuntimeError(null);219 setRuntimeError(null);
220 }, []);220 }, []);
221 221
222 const createAnonymousWorkspace = useCallback(async () => {222 const createAnonymousOrganization = useCallback(async () => {
223 if (!anonymousDemoEnabled) {223 if (!anonymousDemoEnabled) {
224 throw new Error("Anonymous workspace creation is disabled");224 throw new Error("Anonymous organization creation is disabled");
225 }225 }
226 226
227 setRuntimeError(null);227 setRuntimeError(null);
228 setCreatingAnonymousWorkspace(true);228 setCreatingAnonymousOrganization(true);
229 try {229 try {
230 const context = await bootstrapAnonymousSession({});230 const context = await bootstrapAnonymousSession({});
231 localStorage.setItem(SESSION_KEY, context.sessionId);231 localStorage.setItem(SESSION_KEY, context.sessionId);
@@ -234,11 +234,11 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 2 unmodified lines
234 setActiveWorkspaceId(context.workspaceId as Id<"workspaces">);234 setActiveWorkspaceId(context.workspaceId as Id<"workspaces">);
235 setManualGuestContext(context);235 setManualGuestContext(context);
236 } catch (cause) {236 } 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";
238 setRuntimeError(message);238 setRuntimeError(message);
239 throw cause;239 throw cause;
240 } finally {240 } finally {
241 setCreatingAnonymousWorkspace(false);241 setCreatingAnonymousOrganization(false);
242 }242 }
243 }, [bootstrapAnonymousSession]);243 }, [bootstrapAnonymousSession]);
244 244
@@ -246,7 +246,7 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 1 unmodified line
246 setActiveWorkspaceId(workspaceId);246 setActiveWorkspaceId(workspaceId);
247 localStorage.setItem(ACTIVE_WORKSPACE_KEY, workspaceId);247 localStorage.setItem(ACTIVE_WORKSPACE_KEY, workspaceId);
248 248
249 if (account?.provider === "workos") {249 if (account) {
250 const accountId = account._id;250 const accountId = account._id;
251 const byAccount = readWorkspaceByAccount();251 const byAccount = readWorkspaceByAccount();
252 writeWorkspaceByAccount({252 writeWorkspaceByAccount({
@@ -314,8 +314,8 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 61 unmodified lines
314 switchWorkspace,314 switchWorkspace,
315 ]);315 ]);
316 316
317 const workosContext = useMemo<AnonymousContext | null>(() => {317 const accountWorkspaceContext = useMemo<AnonymousContext | null>(() => {
318 if (!workosEnabled || !account || account.provider !== "workos" || !workspaces || workspaces.length === 0) {318 if (!account || !workspaces || workspaces.length === 0) {
319 return null;319 return null;
320 }320 }
321 321
@@ -327,17 +327,26 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 5 unmodified lines
327 return null;327 return null;
328 }328 }
329 329
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
330 return {339 return {
331 sessionId: `workos_${account._id}`,340 sessionId,
332 workspaceId: activeWorkspace.id,341 workspaceId: activeWorkspace.id,
333 actorId: account._id,342 actorId,
334 clientId: "web",343 clientId: "web",
335 accountId: account._id,344 accountId: account._id,
336 userId: account._id,345 userId: account._id,
337 createdAt: Date.now(),346 createdAt: Date.now(),
338 lastSeenAt: Date.now(),347 lastSeenAt: Date.now(),
339 };348 };
340 }, [account, resolvedActiveWorkspaceId, workspaces]);349 }, [account, guestContext?.sessionId, resolvedActiveWorkspaceId, storedSessionId, workspaces]);
341 350
342 // When WorkOS is enabled, don't fall back to guest context while WorkOS351 // When WorkOS is enabled, don't fall back to guest context while WorkOS
343 // auth/account bootstrapping is still in flight. Otherwise workspace-bound352 // auth/account bootstrapping is still in flight. Otherwise workspace-bound
@@ -348,9 +357,11 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 4 unmodified lines
348 || account === undefined357 || account === undefined
349 || (account?.provider === "workos" && bootstrapWorkosAccountQuery.isFetching)358 || (account?.provider === "workos" && bootstrapWorkosAccountQuery.isFetching)
350 );359 );
351 const mode: "guest" | "workos" = workosContext ? "workos" : "guest";360 const mode: "guest" | "workos" | "anonymous" = accountWorkspaceContext
361 ? (account?.provider === "workos" ? "workos" : "anonymous")
362 : "guest";
352 const shouldBlockGuestFallback = workosEnabled && account?.provider === "workos";363 const shouldBlockGuestFallback = workosEnabled && account?.provider === "workos";
353 const context = workosContext ?? ((workosStillLoading || shouldBlockGuestFallback) ? null : guestContext);364 const context = accountWorkspaceContext ?? ((workosStillLoading || shouldBlockGuestFallback) ? null : guestContext);
354 365
355 const bootstrapSessionError =366 const bootstrapSessionError =
356 storedSessionId && bootstrapSessionQuery.error instanceof Error367 storedSessionId && bootstrapSessionQuery.error instanceof Error
@@ -367,22 +378,24 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 10 unmodified lines
367 const error = runtimeError ?? bootstrapSessionError ?? bootstrapWorkosError;378 const error = runtimeError ?? bootstrapSessionError ?? bootstrapWorkosError;
368 379
369 const effectiveLoading = !context && !error && (380 const effectiveLoading = !context && !error && (
370 creatingAnonymousWorkspace381 creatingAnonymousOrganization
371 || (storedSessionId !== null && bootstrapSessionQuery.isLoading)382 || (storedSessionId !== null && bootstrapSessionQuery.isLoading)
372 || workosStillLoading383 || workosStillLoading
373 || bootstrapWorkosAccountQuery.isFetching384 || bootstrapWorkosAccountQuery.isFetching
374 );385 );
375 const workspaceOptions = useMemo(() => {386 const workspaceOptions = useMemo(() => {
376 if (mode === "workos" && workspaces) {387 if (mode !== "guest" && workspaces) {
377 return workspaces.map((workspace): SessionState["workspaces"][number] => ({388 return workspaces.map((workspace): SessionState["workspaces"][number] => {
378 id: workspace.id,389 return {
379 docId: workspace.id,390 id: workspace.id,
380 name: workspace.name,391 docId: workspace.id,
381 organizationId: workspace.organizationId ?? null,392 name: workspace.name,
382 organizationName: workspace.organizationName,393 organizationId: workspace.organizationId,
383 organizationSlug: workspace.organizationSlug,394 organizationName: workspace.organizationName,
384 iconUrl: workspace.iconUrl,395 organizationSlug: workspace.organizationSlug,
385 }));396 iconUrl: workspace.iconUrl,
397 };
398 });
386 }399 }
387 400
388 if (guestContext) {401 if (guestContext) {
@@ -390,10 +403,10 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 1 unmodified line
390 {403 {
391 id: guestContext.workspaceId as Id<"workspaces">,404 id: guestContext.workspaceId as Id<"workspaces">,
392 docId: null,405 docId: null,
393 name: "Guest Workspace",406 name: "Anonymous Workspace",
394 organizationId: null,407 organizationId: null,
395 organizationName: null,408 organizationName: "Anonymous Organization",
396 organizationSlug: null,409 organizationSlug: "anonymous-organization",
397 },410 },
398 ];411 ];
399 }412 }
@@ -410,13 +423,13 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 10 unmodified lines
410 clientConfig: clientConfig ?? null,423 clientConfig: clientConfig ?? null,
411 mode,424 mode,
412 organizations: organizations ?? [],425 organizations: organizations ?? [],
413 organizationsLoading: workosEnabled ? organizations === undefined : false,426 organizationsLoading: organizations === undefined,
414 workspaces: workspaceOptions,427 workspaces: workspaceOptions,
415 switchWorkspace,428 switchWorkspace,
416 creatingWorkspace,429 creatingWorkspace,
417 createWorkspace,430 createWorkspace,
418 creatingAnonymousWorkspace,431 creatingAnonymousOrganization,
419 createAnonymousWorkspace,432 createAnonymousOrganization,
420 isSignedInToWorkos: Boolean(account && account.provider === "workos"),433 isSignedInToWorkos: Boolean(account && account.provider === "workos"),
421 workosProfile:434 workosProfile:
422 account && account.provider === "workos"435 account && account.provider === "workos"
executor/convex/_generated/api.d.ts+160−0
@@ -813,6 +813,80 @@ export declare const internal: { · 812 unmodified lines
813 any813 any
814 >;814 >;
815 };815 };
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 };
816 openApiSpecCache: {890 openApiSpecCache: {
817 getEntry: FunctionReference<891 getEntry: FunctionReference<
818 "query",892 "query",
@@ -1270,4 +1344,90 @@ export declare const components: { · 451 unmodified lines
1270 >;1344 >;
1271 };1345 };
1272 };1346 };
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 };
1273};1433};
executor/convex/convex.config.ts+2−0
@@ -1,3 +1,4 @@
1import migrations from "@convex-dev/migrations/convex.config.js";
1import stripe from "@convex-dev/stripe/convex.config.js";2import stripe from "@convex-dev/stripe/convex.config.js";
2import workOSAuthKit from "@convex-dev/workos-authkit/convex.config";3import workOSAuthKit from "@convex-dev/workos-authkit/convex.config";
3import { defineApp } from "convex/server";4import { defineApp } from "convex/server";
@@ -6,5 +7,6 @@ const app = defineApp(); · 2 unmodified lines
6 7
7app.use(workOSAuthKit);8app.use(workOSAuthKit);
8app.use(stripe);9app.use(stripe);
10app.use(migrations);
9 11
10export default app;12export default app;
executor/convex/database.ts+6−4
@@ -195,6 +195,8 @@ async function ensureAnonymousIdentity( · 194 unmodified lines
195 timestamp: number;195 timestamp: number;
196 },196 },
197) {197) {
198 const anonymousOrganizationName = "Anonymous Organization";
199 const anonymousWorkspaceName = "Anonymous Workspace";
198 const now = params.timestamp;200 const now = params.timestamp;
199 201
200 let account = await ctx.db202 let account = await ctx.db
@@ -226,10 +228,10 @@ async function ensureAnonymousIdentity( · 25 unmodified lines
226 let organizationId: Doc<"organizations">["_id"];228 let organizationId: Doc<"organizations">["_id"];
227 229
228 if (!workspace) {230 if (!workspace) {
229 const organizationSlug = await ensureUniqueOrganizationSlug(ctx, "Guest Workspace");231 const organizationSlug = await ensureUniqueOrganizationSlug(ctx, anonymousOrganizationName);
230 organizationId = await ctx.db.insert("organizations", {232 organizationId = await ctx.db.insert("organizations", {
231 slug: organizationSlug,233 slug: organizationSlug,
232 name: "Guest Workspace",234 name: anonymousOrganizationName,
233 status: "active",235 status: "active",
234 createdByAccountId: account._id,236 createdByAccountId: account._id,
235 createdAt: now,237 createdAt: now,
@@ -238,8 +240,8 @@ async function ensureAnonymousIdentity( · 2 unmodified lines
238 240
239 const workspaceId = await ctx.db.insert("workspaces", {241 const workspaceId = await ctx.db.insert("workspaces", {
240 organizationId,242 organizationId,
241 slug: `guest-${crypto.randomUUID().slice(0, 8)}`,243 slug: `anonymous-${crypto.randomUUID().slice(0, 8)}`,
242 name: "Guest Workspace",244 name: anonymousWorkspaceName,
243 plan: "free",245 plan: "free",
244 createdByAccountId: account._id,246 createdByAccountId: account._id,
245 createdAt: now,247 createdAt: now,
executor/convex/migrations.ts+95−0
@@ -0,0 +1,95 @@
1import { Migrations } from "@convex-dev/migrations";
2import type { Id } from "./_generated/dataModel";
3import type { DataModel } from "./_generated/dataModel";
4import { components } from "./_generated/api";
5
6const migrations = new Migrations<DataModel>(components.migrations);
7
8const LEGACY_GUEST_NAME = "Guest Workspace";
9const ANONYMOUS_ORGANIZATION_NAME = "Anonymous Organization";
10const ANONYMOUS_WORKSPACE_NAME = "Anonymous Workspace";
11
12async 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
24export 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
43export 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
62export 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
73export 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
84export 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
95export const run = migrations.runner();
executor/convex/schema.ts+3−3
@@ -130,7 +130,7 @@ export default defineSchema({ · 129 unmodified lines
130 email: v.string(),130 email: v.string(),
131 role: orgRole,131 role: orgRole,
132 status: inviteStatus,132 status: inviteStatus,
133 provider: v.optional(inviteProvider),133 provider: inviteProvider,
134 providerInviteId: v.optional(v.string()),134 providerInviteId: v.optional(v.string()),
135 invitedByAccountId: v.id("accounts"),135 invitedByAccountId: v.id("accounts"),
136 expiresAt: v.number(),136 expiresAt: v.number(),
@@ -263,7 +263,7 @@ export default defineSchema({ · 126 unmodified lines
263 sourceKey: v.string(),263 sourceKey: v.string(),
264 scope: credentialScope,264 scope: credentialScope,
265 actorId: v.string(),265 actorId: v.string(),
266 provider: v.optional(credentialProvider),266 provider: credentialProvider,
267 secretJson: v.any(),267 secretJson: v.any(),
268 createdAt: v.number(),268 createdAt: v.number(),
269 updatedAt: v.number(),269 updatedAt: v.number(),
@@ -333,7 +333,7 @@ export default defineSchema({ · 63 unmodified lines
333 workspaceId: v.id("workspaces"),333 workspaceId: v.id("workspaces"),
334 actorId: v.string(),334 actorId: v.string(),
335 clientId: v.string(),335 clientId: v.string(),
336 accountId: v.optional(v.id("accounts")),336 accountId: v.id("accounts"),
337 userId: v.optional(v.id("workspaceMembers")),337 userId: v.optional(v.id("workspaceMembers")),
338 createdAt: v.number(),338 createdAt: v.number(),
339 lastSeenAt: v.number(),339 lastSeenAt: v.number(),
package.json+1−0
@@ -29,6 +29,7 @@ · 28 unmodified lines
29 },29 },
30 "dependencies": {30 "dependencies": {
31 "@apidevtools/swagger-parser": "^12.1.0",31 "@apidevtools/swagger-parser": "^12.1.0",
32 "@convex-dev/migrations": "^0.3.1",
32 "@convex-dev/stripe": "^0.1.3",33 "@convex-dev/stripe": "^0.1.3",
33 "@convex-dev/workos-authkit": "^0.1.6",34 "@convex-dev/workos-authkit": "^0.1.6",
34 "@executor/contracts": "workspace:*",35 "@executor/contracts": "workspace:*",