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