History

Unify workspace membership model and tighten core types

Rhys Sullivan committed 9 months ago

Commit d03cf181 parent f0d800111 files · +125 −141Browse files

Unified · Split

11 files
  1. executor/apps/web/src/components/members-view.tsx +17 −16
  2. executor/apps/web/src/lib/session-context.tsx +25 −25
  3. executor/convex/auth.ts +16 −17
  4. executor/convex/database.ts +12 −13
  5. executor/convex/invites.ts +12 −11
  6. executor/convex/lib/identity.ts +0 −8
  7. executor/convex/organizationMembers.ts +8 −1
  8. executor/convex/organizations.ts +5 −7
  9. executor/convex/schema.ts +30 −40
  10. executor/convex/workspaces.ts +0 −2
  11. executor/packages/contracts/src/index.ts +0 −1
executor/apps/web/src/components/members-view.tsx+17−16
@@ -9,6 +9,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; · 8 unmodified lines
99 import { Input } from "@/components/ui/input";
1010 import { useSession } from "@/lib/session-context";
1111 import { convexApi } from "@/lib/convex-api";
12+import type { Id } from "../../../../convex/_generated/dataModel";
1213
1314 type Role = "owner" | "admin" | "member" | "billing_admin";
1415
@@ -52,14 +53,14 @@ export function MembersView() { · 37 unmodified lines
5253 const [inviteRole, setInviteRole] = useState<Role>("member");
5354 const [inviteState, setInviteState] = useState<"idle" | "sending" | "sent" | "failed">("idle");
5455 const [inviteMessage, setInviteMessage] = useState<string | null>(null);
55− const [busyMemberAccountId, setBusyMemberAccountId] = useState<string | null>(null);
56− const [busyInviteId, setBusyInviteId] = useState<string | null>(null);
56+ const [busyMemberAccountId, setBusyMemberAccountId] = useState<Id<"accounts"> | null>(null);
57+ const [busyInviteId, setBusyInviteId] = useState<Id<"invites"> | null>(null);
5758
5859 const memberItems = members?.items ?? [];
5960 const inviteItems = listInvites?.items ?? [];
6061 const pendingInviteItems = inviteItems.filter((invite) => invite.status === "pending" || invite.status === "failed");
6162 const actorMembership = memberItems.find((member) =>
62− context?.accountId ? String(member.accountId) === context.accountId : false,
63+ context?.accountId ? member.accountId === context.accountId : false,
6364 );
6465 const actorRole = actorMembership?.role ?? null;
6566 const canManageMembers = actorRole === "owner" || actorRole === "admin";
@@ -276,20 +277,20 @@ export function MembersView() { · 210 unmodified lines
276277 <p className="text-xs text-muted-foreground">Role: {invite.role}</p>
277278 <p className="text-xs text-muted-foreground">Status: {invite.status}</p>
278279 </div>
279− <Button
280− variant="outline"
281− size="sm"
282− className="h-8 text-xs"
283− disabled={!canManageMembers || busyInviteId === String(invite.id)}
284− onClick={async () => {
285− if (!typedOrganizationId) {
286− return;
287− }
280+ <Button
281+ variant="outline"
282+ size="sm"
283+ className="h-8 text-xs"
284+ disabled={!canManageMembers || busyInviteId === invite.id}
285+ onClick={async () => {
286+ if (!typedOrganizationId) {
287+ return;
288+ }
288289
289− setBusyInviteId(String(invite.id));
290− try {
291− await revokeInvite({
292− organizationId: typedOrganizationId,
290+ setBusyInviteId(invite.id);
291+ try {
292+ await revokeInvite({
293+ organizationId: typedOrganizationId,
293294 inviteId: invite.id,
294295 sessionId: context?.sessionId ?? undefined,
295296 });
executor/apps/web/src/lib/session-context.tsx+25−25
@@ -30,20 +30,20 @@ interface SessionState { · 29 unmodified lines
3030 } | null;
3131 mode: "guest" | "workos";
3232 organizations: Array<{
33− id: string;
33+ id: Id<"organizations">;
3434 name: string;
3535 slug: string;
3636 status: string;
3737 role: string;
3838 }>;
3939 workspaces: Array<{
40− id: string;
40+ id: Id<"workspaces">;
4141 docId: Id<"workspaces"> | null;
4242 name: string;
4343 organizationId: Id<"organizations"> | null;
4444 iconUrl?: string | null;
4545 }>;
46− switchWorkspace: (workspaceId: string) => void;
46+ switchWorkspace: (workspaceId: Id<"workspaces">) => void;
4747 creatingWorkspace: boolean;
4848 createWorkspace: (name: string, iconFile?: File | null) => Promise<void>;
4949 isSignedInToWorkos: boolean;
@@ -77,16 +77,16 @@ const ACTIVE_WORKSPACE_BY_ACCOUNT_KEY = "executor_active_workspace_by_account"; · 27 unmodified lines
7777
7878 function readWorkspaceByAccount() {
7979 const raw = localStorage.getItem(ACTIVE_WORKSPACE_BY_ACCOUNT_KEY);
80− if (!raw) return {} as Record<string, string>;
80+ if (!raw) return {} as Record<string, Id<"workspaces">>;
8181 try {
82− const parsed = JSON.parse(raw) as Record<string, string>;
82+ const parsed = JSON.parse(raw) as Record<string, Id<"workspaces">>;
8383 return parsed ?? {};
8484 } catch {
8585 return {};
8686 }
8787 }
8888
89−function writeWorkspaceByAccount(value: Record<string, string>) {
89+function writeWorkspaceByAccount(value: Record<string, Id<"workspaces">>) {
9090 localStorage.setItem(ACTIVE_WORKSPACE_BY_ACCOUNT_KEY, JSON.stringify(value));
9191 }
9292
@@ -98,11 +98,12 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 5 unmodified lines
9898 }
9999 return localStorage.getItem(SESSION_KEY);
100100 });
101− const [activeWorkspaceId, setActiveWorkspaceId] = useState<string | null>(() => {
101+ const [activeWorkspaceId, setActiveWorkspaceId] = useState<Id<"workspaces"> | null>(() => {
102102 if (typeof window === "undefined") {
103103 return null;
104104 }
105− return localStorage.getItem(ACTIVE_WORKSPACE_KEY);
105+ const stored = localStorage.getItem(ACTIVE_WORKSPACE_KEY);
106+ return stored as Id<"workspaces"> | null;
106107 });
107108 const [runtimeError, setRuntimeError] = useState<string | null>(null);
108109
@@ -148,17 +149,17 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 39 unmodified lines
148149 return activeWorkspaceId;
149150 }
150151
151− if (activeWorkspaceId && workspaces.some((workspace) => workspace.runtimeWorkspaceId === activeWorkspaceId)) {
152+ if (activeWorkspaceId && workspaces.some((workspace) => workspace._id === activeWorkspaceId)) {
152153 return activeWorkspaceId;
153154 }
154155
155− const accountId = account?.provider === "workos" ? String(account._id) : null;
156+ const accountId = account?.provider === "workos" ? account._id : null;
156157 const accountStoredWorkspace = accountId ? readWorkspaceByAccount()[accountId] : null;
157− if (accountStoredWorkspace && workspaces.some((workspace) => workspace.runtimeWorkspaceId === accountStoredWorkspace)) {
158+ if (accountStoredWorkspace && workspaces.some((workspace) => workspace._id === accountStoredWorkspace)) {
158159 return accountStoredWorkspace;
159160 }
160161
161− return workspaces[0]?.runtimeWorkspaceId ?? null;
162+ return workspaces[0]?._id ?? null;
162163 }, [workspaces, activeWorkspaceId, account]);
163164
164165 const bootstrapWorkosAccountQuery = useTanstackQuery({
@@ -177,12 +178,12 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 12 unmodified lines
177178 setRuntimeError(null);
178179 }, []);
179180
180− const switchWorkspace = useCallback((workspaceId: string) => {
181+ const switchWorkspace = useCallback((workspaceId: Id<"workspaces">) => {
181182 setActiveWorkspaceId(workspaceId);
182183 localStorage.setItem(ACTIVE_WORKSPACE_KEY, workspaceId);
183184
184185 if (account?.provider === "workos") {
185− const accountId = String(account._id);
186+ const accountId = account._id;
186187 const byAccount = readWorkspaceByAccount();
187188 writeWorkspaceByAccount({
188189 ...byAccount,
@@ -227,8 +228,8 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 38 unmodified lines
227228 sessionId: storedSessionId ?? undefined,
228229 });
229230
230− if (created?.runtimeWorkspaceId) {
231− switchWorkspace(created.runtimeWorkspaceId);
231+ if (created?._id) {
232+ switchWorkspace(created._id);
232233 }
233234 } catch (cause) {
234235 const message = cause instanceof Error ? cause.message : "Failed to create workspace";
@@ -250,7 +251,7 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 15 unmodified lines
250251 }
251252
252253 const activeWorkspace =
253− workspaces.find((workspace) => workspace.runtimeWorkspaceId === resolvedActiveWorkspaceId)
254+ workspaces.find((workspace) => workspace._id === resolvedActiveWorkspaceId)
254255 ?? workspaces[0]
255256 ?? null;
256257 if (!activeWorkspace) {
@@ -258,13 +259,12 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 1 unmodified line
258259 }
259260
260261 return {
261− sessionId: `workos_${String(account._id)}`,
262− workspaceId: activeWorkspace.runtimeWorkspaceId,
263− actorId: String(activeWorkspace.userId),
262+ sessionId: `workos_${account._id}`,
263+ workspaceId: activeWorkspace._id,
264+ actorId: activeWorkspace.userId,
264265 clientId: "web",
265− accountId: String(account._id),
266− workspaceDocId: String(activeWorkspace._id),
267− userId: String(activeWorkspace.userId),
266+ accountId: account._id,
267+ userId: activeWorkspace.userId,
268268 createdAt: Date.now(),
269269 lastSeenAt: Date.now(),
270270 };
@@ -301,7 +301,7 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 30 unmodified lines
301301 const workspaceOptions = useMemo(() => {
302302 if (mode === "workos" && workspaces) {
303303 return workspaces.map((workspace): SessionState["workspaces"][number] => ({
304− id: workspace.runtimeWorkspaceId,
304+ id: workspace._id,
305305 docId: workspace._id,
306306 name: workspace.name,
307307 organizationId: workspace.organizationId ?? null,
@@ -312,7 +312,7 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 4 unmodified lines
312312 if (guestContext) {
313313 return [
314314 {
315− id: guestContext.workspaceId,
315+ id: guestContext.workspaceId as Id<"workspaces">,
316316 docId: null,
317317 name: "Guest Workspace",
318318 organizationId: null,
executor/convex/auth.ts+16−17
@@ -9,6 +9,8 @@ import { ensureUniqueSlug } from "./lib/slug"; · 8 unmodified lines
99 type DbCtx = Pick<MutationCtx, "db">;
1010 type RunQueryCtx = Pick<MutationCtx, "runQuery">;
1111 type WorkosEventCtx = Pick<MutationCtx, "db" | "runQuery">;
12+type OrganizationRole = "owner" | "admin" | "member" | "billing_admin";
13+type OrganizationMemberStatus = "active" | "pending" | "removed";
1214
1315 const workosEnabled = Boolean(
1416 process.env.WORKOS_CLIENT_ID && process.env.WORKOS_API_KEY && process.env.WORKOS_WEBHOOK_SECRET,
@@ -81,8 +83,8 @@ async function upsertOrganizationMembership( · 66 unmodified lines
8183 args: {
8284 organizationId: Id<"organizations">;
8385 accountId: Id<"accounts">;
84− role: string;
85− status: string;
86+ role: OrganizationRole;
87+ status: OrganizationMemberStatus;
8688 billable: boolean;
8789 invitedByAccountId?: Id<"accounts">;
8890 now: number;
@@ -153,7 +155,7 @@ async function ensurePersonalWorkspace( · 64 unmodified lines
153155 opts: { email: string; firstName?: string; workosUserId: string; now: number; workspaceName?: string },
154156 ) {
155157 const memberships = await ctx.db
156− .query("users")
158+ .query("workspaceMembers")
157159 .withIndex("by_account", (q) => q.eq("accountId", accountId))
158160 .collect();
159161
@@ -204,7 +206,7 @@ async function ensurePersonalWorkspace( · 44 unmodified lines
204206 now: opts.now,
205207 });
206208
207− const userId = await ctx.db.insert("users", {
209+ const userId = await ctx.db.insert("workspaceMembers", {
208210 workspaceId,
209211 accountId,
210212 role: "owner",
@@ -305,7 +307,7 @@ const workosEventHandlers: Record<string, (ctx: WorkosEventCtx, event: any) => P · 94 unmodified lines
305307 if (!account) return;
306308
307309 const memberships = await ctx.db
308− .query("users")
310+ .query("workspaceMembers")
309311 .withIndex("by_account", (q) => q.eq("accountId", account._id))
310312 .collect();
311313 for (const membership of memberships) {
@@ -394,7 +396,7 @@ const workosEventHandlers: Record<string, (ctx: WorkosEventCtx, event: any) => P · 82 unmodified lines
394396 if (!workspace) return;
395397
396398 const members = await ctx.db
397− .query("users")
399+ .query("workspaceMembers")
398400 .withIndex("by_workspace", (q) => q.eq("workspaceId", workspace._id))
399401 .collect();
400402 for (const member of members) {
@@ -428,7 +430,7 @@ const workosEventHandlers: Record<string, (ctx: WorkosEventCtx, event: any) => P · 27 unmodified lines
428430 const organizationId = workspace.organizationId;
429431
430432 const existing = await ctx.db
431− .query("users")
433+ .query("workspaceMembers")
432434 .withIndex("by_workspace_account", (q) => q.eq("workspaceId", workspace._id).eq("accountId", account._id))
433435 .unique();
434436
@@ -463,7 +465,7 @@ const workosEventHandlers: Record<string, (ctx: WorkosEventCtx, event: any) => P · 28 unmodified lines
463465 return;
464466 }
465467
466− await ctx.db.insert("users", {
468+ await ctx.db.insert("workspaceMembers", {
467469 workspaceId: workspace._id,
468470 accountId: account._id,
469471 workosOrgMembershipId: event.data.id,
@@ -487,7 +489,7 @@ const workosEventHandlers: Record<string, (ctx: WorkosEventCtx, event: any) => P · 17 unmodified lines
487489 };
488490
489491 let membership = await ctx.db
490− .query("users")
492+ .query("workspaceMembers")
491493 .withIndex("by_workos_membership_id", (q) => q.eq("workosOrgMembershipId", data.id))
492494 .unique();
493495
@@ -506,7 +508,7 @@ const workosEventHandlers: Record<string, (ctx: WorkosEventCtx, event: any) => P · 12 unmodified lines
506508 const workspaceId = workspace._id;
507509 const accountId = account._id;
508510 membership = await ctx.db
509− .query("users")
511+ .query("workspaceMembers")
510512 .withIndex("by_workspace_account", (q) => q.eq("workspaceId", workspaceId).eq("accountId", accountId))
511513 .unique();
512514 if (!membership) return;
@@ -551,7 +553,7 @@ const workosEventHandlers: Record<string, (ctx: WorkosEventCtx, event: any) => P · 38 unmodified lines
551553 "organization_membership.deleted": async (ctx, event) => {
552554 const now = Date.now();
553555 const membership = await ctx.db
554− .query("users")
556+ .query("workspaceMembers")
555557 .withIndex("by_workos_membership_id", (q) => q.eq("workosOrgMembershipId", event.data.id))
556558 .unique();
557559 if (!membership) return;
@@ -723,7 +725,7 @@ export const createWorkspace = mutation({ · 165 unmodified lines
723725 now,
724726 });
725727
726− const userId = await ctx.db.insert("users", {
728+ const userId = await ctx.db.insert("workspaceMembers", {
727729 workspaceId,
728730 accountId: account._id,
729731 role: "owner",
@@ -750,7 +752,6 @@ export const createWorkspace = mutation({ · 20 unmodified lines
750752 userId: user._id,
751753 role: user.role,
752754 status: user.status,
753− runtimeWorkspaceId: workspace._id,
754755 };
755756 },
756757 });
@@ -804,7 +805,7 @@ export const getMyWorkspaces = query({ · 47 unmodified lines
804805 if (!account) return [];
805806
806807 const memberships = await ctx.db
807− .query("users")
808+ .query("workspaceMembers")
808809 .withIndex("by_account", (q) => q.eq("accountId", account._id))
809810 .collect();
810811
@@ -823,7 +824,6 @@ export const getMyWorkspaces = query({ · 12 unmodified lines
823824 userId: membership._id,
824825 role: membership.role,
825826 status: membership.status,
826− runtimeWorkspaceId: workspace._id,
827827 };
828828 }),
829829 );
@@ -839,7 +839,7 @@ export const getMyAccountsWithWorkspaces = query({ · 9 unmodified lines
839839 if (!account) return [];
840840
841841 const memberships = await ctx.db
842− .query("users")
842+ .query("workspaceMembers")
843843 .withIndex("by_account", (q) => q.eq("accountId", account._id))
844844 .collect();
845845
@@ -858,7 +858,6 @@ export const getMyAccountsWithWorkspaces = query({ · 12 unmodified lines
858858 userId: membership._id,
859859 role: membership.role,
860860 status: membership.status,
861− runtimeWorkspaceId: workspace._id,
862861 };
863862 }),
864863 );
executor/convex/database.ts+12−13
@@ -5,6 +5,8 @@ import { mutation, query } from "./_generated/server"; · 4 unmodified lines
55 import { ensureUniqueSlug } from "./lib/slug";
66
77 const DEFAULT_TIMEOUT_MS = 15_000;
8+type OrganizationRole = "owner" | "admin" | "member" | "billing_admin";
9+type OrganizationMemberStatus = "active" | "pending" | "removed";
810
911 function normalizeOptional(value?: string): string {
1012 if (typeof value !== "string") {
@@ -45,8 +47,8 @@ async function upsertOrganizationMembership( · 34 unmodified lines
4547 args: {
4648 organizationId: Doc<"organizations">["_id"];
4749 accountId: Doc<"accounts">["_id"];
48− role: string;
49− status: string;
50+ role: OrganizationRole;
51+ status: OrganizationMemberStatus;
5052 billable: boolean;
5153 now: number;
5254 },
@@ -168,7 +170,7 @@ async function ensureAnonymousIdentity( · 115 unmodified lines
168170 ctx: MutationCtx,
169171 params: {
170172 sessionId: string;
171− workspaceDocId?: Doc<"workspaces">["_id"];
173+ workspaceId?: Doc<"workspaces">["_id"];
172174 actorId: string;
173175 timestamp: number;
174176 },
@@ -199,7 +201,7 @@ async function ensureAnonymousIdentity( · 24 unmodified lines
199201 await ctx.db.patch(account._id, { updatedAt: now, lastLoginAt: now });
200202 }
201203
202− let workspace = params.workspaceDocId ? await ctx.db.get(params.workspaceDocId) : null;
204+ let workspace = params.workspaceId ? await ctx.db.get(params.workspaceId) : null;
203205
204206 let organizationId: Doc<"organizations">["_id"];
205207
@@ -241,12 +243,12 @@ async function ensureAnonymousIdentity( · 35 unmodified lines
241243 });
242244
243245 let user = await ctx.db
244− .query("users")
246+ .query("workspaceMembers")
245247 .withIndex("by_workspace_account", (q) => q.eq("workspaceId", workspace._id).eq("accountId", account._id))
246248 .unique();
247249
248250 if (!user) {
249− const userId = await ctx.db.insert("users", {
251+ const userId = await ctx.db.insert("workspaceMembers", {
250252 workspaceId: workspace._id,
251253 accountId: account._id,
252254 role: "owner",
@@ -264,7 +266,7 @@ async function ensureAnonymousIdentity( · 11 unmodified lines
264266
265267 return {
266268 accountId: account._id,
267− workspaceDocId: workspace._id,
269+ workspaceId: workspace._id,
268270 userId: user._id,
269271 };
270272 }
@@ -276,7 +278,6 @@ function mapAnonymousContext(doc: Doc<"anonymousSessions">) { · 5 unmodified lines
276278 actorId: doc.actorId,
277279 clientId: doc.clientId,
278280 accountId: doc.accountId,
279− workspaceDocId: doc.workspaceDocId,
280281 userId: doc.userId,
281282 createdAt: doc.createdAt,
282283 lastSeenAt: doc.lastSeenAt,
@@ -708,15 +709,14 @@ export const bootstrapAnonymousSession = mutation({ · 425 unmodified lines
708709 if (existing) {
709710 const identity = await ensureAnonymousIdentity(ctx, {
710711 sessionId,
711− workspaceDocId: existing.workspaceDocId,
712+ workspaceId: existing.workspaceId,
712713 actorId: existing.actorId,
713714 timestamp: now,
714715 });
715716
716717 await ctx.db.patch(existing._id, {
717− workspaceId: identity.workspaceDocId,
718+ workspaceId: identity.workspaceId,
718719 accountId: identity.accountId,
719− workspaceDocId: identity.workspaceDocId,
720720 userId: identity.userId,
721721 lastSeenAt: now,
722722 });
@@ -744,11 +744,10 @@ export const bootstrapAnonymousSession = mutation({ · 21 unmodified lines
744744
745745 await ctx.db.insert("anonymousSessions", {
746746 sessionId,
747− workspaceId: identity.workspaceDocId,
747+ workspaceId: identity.workspaceId,
748748 actorId,
749749 clientId,
750750 accountId: identity.accountId,
751− workspaceDocId: identity.workspaceDocId,
752751 userId: identity.userId,
753752 createdAt: now,
754753 lastSeenAt: now,
executor/convex/invites.ts+12−11
@@ -7,6 +7,14 @@ import { authedMutation, organizationMutation, organizationQuery } from "./lib/f · 6 unmodified lines
77
88 const workosEnabled = Boolean(process.env.WORKOS_CLIENT_ID && process.env.WORKOS_API_KEY);
99 const workosClient = process.env.WORKOS_API_KEY ? new WorkOS(process.env.WORKOS_API_KEY) : null;
10+type OrganizationRole = "owner" | "admin" | "member" | "billing_admin";
11+
12+const organizationRoleValidator = v.union(
13+ v.literal("owner"),
14+ v.literal("admin"),
15+ v.literal("member"),
16+ v.literal("billing_admin"),
17+);
1018
1119 async function sha256Hex(value: string): Promise<string> {
1220 const bytes = new TextEncoder().encode(value);
@@ -65,7 +73,7 @@ async function revokeWorkosInvitation(invitationId: string): Promise<void> { · 52 unmodified lines
6573 await workos.userManagement.revokeInvitation(invitationId);
6674 }
6775
68−function mapRoleToWorkosRoleSlug(role: string): string | undefined {
76+function mapRoleToWorkosRoleSlug(role: OrganizationRole): string | undefined {
6977 if (role === "admin" || role === "owner") {
7078 return "admin";
7179 }
@@ -103,7 +111,7 @@ export const create = organizationMutation({ · 31 unmodified lines
103111 requireAdmin: true,
104112 args: {
105113 email: v.string(),
106− role: v.string(),
114+ role: organizationRoleValidator,
107115 workspaceId: v.optional(v.id("workspaces")),
108116 expiresInDays: v.optional(v.number()),
109117 },
@@ -123,17 +131,10 @@ export const create = organizationMutation({ · 13 unmodified lines
123131 }
124132 }
125133
126− const inviterWorkosUserId = ctx.account.provider === "workos"
127− ? ctx.account.providerAccountId
128− : (await ctx.db
129− .query("accountIdentities")
130− .withIndex("by_account", (q) => q.eq("accountId", ctx.account._id))
131− .filter((q) => q.eq(q.field("provider"), "workos"))
132− .first())?.providerUserId;
133−
134− if (!inviterWorkosUserId) {
134+ if (ctx.account.provider !== "workos") {
135135 throw new Error("Inviter is not linked to WorkOS");
136136 }
137+ const inviterWorkosUserId = ctx.account.providerAccountId;
137138
138139 const token = `invite_${crypto.randomUUID()}`;
139140 const tokenHash = await sha256Hex(token);
executor/convex/lib/identity.ts+0−8
@@ -25,14 +25,6 @@ export async function resolveAccountForRequest( · 24 unmodified lines
2525 if (fromAccounts) {
2626 return fromAccounts;
2727 }
28−
29− const fromIdentity = await ctx.db
30− .query("accountIdentities")
31− .withIndex("by_provider_user", (q) => q.eq("provider", "workos").eq("providerUserId", identity.subject))
32− .unique();
33− if (fromIdentity?.accountId) {
34− return await ctx.db.get(fromIdentity.accountId);
35− }
3628 }
3729
3830 if (!sessionId) {
executor/convex/organizationMembers.ts+8−1
@@ -3,6 +3,13 @@ import { internal } from "./_generated/api"; · 2 unmodified lines
33 import { getOrganizationMembership } from "./lib/identity";
44 import { organizationMutation, organizationQuery } from "./lib/functionBuilders";
55
6+const organizationRoleValidator = v.union(
7+ v.literal("owner"),
8+ v.literal("admin"),
9+ v.literal("member"),
10+ v.literal("billing_admin"),
11+);
12+
613 export const list = organizationQuery({
714 args: {},
815 handler: async (ctx) => {
@@ -36,7 +43,7 @@ export const list = organizationQuery({ · 27 unmodified lines
3643 export const updateRole = organizationMutation({
3744 args: {
3845 accountId: v.id("accounts"),
39− role: v.string(),
46+ role: organizationRoleValidator,
4047 },
4148 requireAdmin: true,
4249 handler: async (ctx, args) => {
executor/convex/organizations.ts+5−7
@@ -11,7 +11,6 @@ type WorkspaceSummary = { · 10 unmodified lines
1111 name: string;
1212 slug: string;
1313 iconUrl: string | null;
14− runtimeWorkspaceId: string;
1514 };
1615
1716 async function ensureUniqueOrganizationSlug(ctx: Pick<MutationCtx, "db">, baseName: string): Promise<string> {
@@ -36,7 +35,6 @@ async function mapWorkspaceWithIcon( · 18 unmodified lines
3635 name: workspace.name,
3736 slug: workspace.slug,
3837 iconUrl,
39− runtimeWorkspaceId: workspace._id,
4038 };
4139 }
4240
@@ -152,8 +150,8 @@ export const getNavigationState = optionalAccountQuery({ · 109 unmodified lines
152150 .query("anonymousSessions")
153151 .withIndex("by_session_id", (q) => q.eq("sessionId", sessionId))
154152 .unique();
155− if (anonymousSession?.workspaceDocId) {
156− const workspace = await ctx.db.get(anonymousSession.workspaceDocId);
153+ if (anonymousSession?.workspaceId) {
154+ const workspace = await ctx.db.get(anonymousSession.workspaceId);
157155 if (workspace) {
158156 workspaces.push(await mapWorkspaceWithIcon(ctx, workspace));
159157 }
@@ -162,7 +160,7 @@ export const getNavigationState = optionalAccountQuery({ · 2 unmodified lines
162160
163161 return {
164162 currentOrganizationId: workspaces[0]?.organizationId ?? null,
165− currentWorkspaceId: workspaces[0]?.runtimeWorkspaceId ?? null,
163+ currentWorkspaceId: workspaces[0]?.id ?? null,
166164 organizations,
167165 workspaces,
168166 };
@@ -199,12 +197,12 @@ export const getNavigationState = optionalAccountQuery({ · 30 unmodified lines
199197 }
200198
201199 const uniqueWorkspaces = Array.from(
202− new Map(workspaces.map((workspace) => [workspace.runtimeWorkspaceId, workspace])).values(),
200+ new Map(workspaces.map((workspace) => [workspace.id, workspace])).values(),
203201 );
204202
205203 return {
206204 currentOrganizationId: organizations[0]?.id ?? null,
207− currentWorkspaceId: uniqueWorkspaces[0]?.runtimeWorkspaceId ?? null,
205+ currentWorkspaceId: uniqueWorkspaces[0]?.id ?? null,
208206 organizations,
209207 workspaces: uniqueWorkspaces,
210208 };
executor/convex/schema.ts+30−40
@@ -1,16 +1,32 @@
11 import { defineSchema, defineTable } from "convex/server";
22 import { v } from "convex/values";
33
4+const accountProvider = v.union(v.literal("workos"), v.literal("anonymous"));
5+const accountStatus = v.union(v.literal("active"), v.literal("deleted"));
6+const organizationStatus = v.union(v.literal("active"), v.literal("deleted"));
7+const orgRole = v.union(v.literal("owner"), v.literal("admin"), v.literal("member"), v.literal("billing_admin"));
8+const orgMemberStatus = v.union(v.literal("active"), v.literal("pending"), v.literal("removed"));
9+const workspaceMemberRole = v.union(v.literal("owner"), v.literal("admin"), v.literal("member"));
10+const workspaceMemberStatus = v.union(v.literal("active"), v.literal("pending"), v.literal("removed"));
11+const inviteStatus = v.union(
12+ v.literal("pending"),
13+ v.literal("accepted"),
14+ v.literal("expired"),
15+ v.literal("revoked"),
16+ v.literal("failed"),
17+);
18+const inviteProvider = v.literal("workos");
19+
420 export default defineSchema({
521 accounts: defineTable({
6− provider: v.string(),
22+ provider: accountProvider,
723 providerAccountId: v.string(),
824 email: v.string(),
925 name: v.string(),
1026 firstName: v.optional(v.string()),
1127 lastName: v.optional(v.string()),
1228 avatarUrl: v.optional(v.string()),
13− status: v.string(),
29+ status: accountStatus,
1430 createdAt: v.number(),
1531 updatedAt: v.number(),
1632 lastLoginAt: v.optional(v.number()),
@@ -39,7 +55,7 @@ export default defineSchema({ · 22 unmodified lines
3955 workosOrgId: v.optional(v.string()),
4056 slug: v.string(),
4157 name: v.string(),
42− status: v.string(),
58+ status: organizationStatus,
4359 createdByAccountId: v.optional(v.id("accounts")),
4460 createdAt: v.number(),
4561 updatedAt: v.number(),
@@ -51,8 +67,8 @@ export default defineSchema({ · 5 unmodified lines
5167 organizationMembers: defineTable({
5268 organizationId: v.id("organizations"),
5369 accountId: v.id("accounts"),
54− role: v.string(),
55− status: v.string(),
70+ role: orgRole,
71+ status: orgMemberStatus,
5672 billable: v.boolean(),
5773 invitedByAccountId: v.optional(v.id("accounts")),
5874 joinedAt: v.optional(v.number()),
@@ -68,37 +84,26 @@ export default defineSchema({ · 9 unmodified lines
6884 workspaceMembers: defineTable({
6985 workspaceId: v.id("workspaces"),
7086 accountId: v.id("accounts"),
71− role: v.string(),
72− status: v.string(),
87+ workosOrgMembershipId: v.optional(v.string()),
88+ role: workspaceMemberRole,
89+ status: workspaceMemberStatus,
7390 createdAt: v.number(),
7491 updatedAt: v.number(),
7592 })
7693 .index("by_workspace", ["workspaceId"])
7794 .index("by_workspace_account", ["workspaceId", "accountId"])
7895 .index("by_account", ["accountId"])
79− .index("by_workspace_status", ["workspaceId", "status"]),
80−
81− accountIdentities: defineTable({
82− accountId: v.id("accounts"),
83− provider: v.string(),
84− providerUserId: v.string(),
85− providerEmail: v.optional(v.string()),
86− rawClaims: v.optional(v.any()),
87− lastLoginAt: v.optional(v.number()),
88− createdAt: v.number(),
89− updatedAt: v.number(),
90− })
91− .index("by_account", ["accountId"])
92− .index("by_provider_user", ["provider", "providerUserId"]),
96+ .index("by_workspace_status", ["workspaceId", "status"])
97+ .index("by_workos_membership_id", ["workosOrgMembershipId"]),
9398
9499 invites: defineTable({
95100 organizationId: v.id("organizations"),
96101 workspaceId: v.optional(v.id("workspaces")),
97102 email: v.string(),
98− role: v.string(),
99− status: v.string(),
103+ role: orgRole,
104+ status: inviteStatus,
100105 tokenHash: v.string(),
101− provider: v.string(),
106+ provider: inviteProvider,
102107 providerInviteId: v.optional(v.string()),
103108 invitedByAccountId: v.id("accounts"),
104109 expiresAt: v.number(),
@@ -146,20 +151,6 @@ export default defineSchema({ · 41 unmodified lines
146151 updatedAt: v.number(),
147152 }).index("by_org", ["organizationId"]),
148153
149− users: defineTable({
150− workspaceId: v.id("workspaces"),
151− accountId: v.id("accounts"),
152− workosOrgMembershipId: v.optional(v.string()),
153− role: v.string(),
154− status: v.string(),
155− createdAt: v.number(),
156− updatedAt: v.number(),
157− })
158− .index("by_workspace_account", ["workspaceId", "accountId"])
159− .index("by_account", ["accountId"])
160− .index("by_workspace", ["workspaceId"])
161− .index("by_workos_membership_id", ["workosOrgMembershipId"]),
162−
163154 tasks: defineTable({
164155 taskId: v.string(),
165156 code: v.string(),
@@ -277,8 +268,7 @@ export default defineSchema({ · 111 unmodified lines
277268 actorId: v.string(),
278269 clientId: v.string(),
279270 accountId: v.optional(v.id("accounts")),
280− workspaceDocId: v.optional(v.id("workspaces")),
281− userId: v.optional(v.id("users")),
271+ userId: v.optional(v.id("workspaceMembers")),
282272 createdAt: v.number(),
283273 lastSeenAt: v.number(),
284274 })
executor/convex/workspaces.ts+0−2
@@ -11,7 +11,6 @@ type WorkspaceResult = { · 10 unmodified lines
1111 name: string;
1212 slug: string;
1313 iconUrl: string | null;
14− runtimeWorkspaceId: string;
1514 createdAt: number;
1615 };
1716
@@ -52,7 +51,6 @@ async function toWorkspaceResult( · 34 unmodified lines
5251 name: workspace.name,
5352 slug: workspace.slug,
5453 iconUrl,
55− runtimeWorkspaceId: workspace._id,
5654 createdAt: workspace.createdAt,
5755 };
5856 }
executor/packages/contracts/src/index.ts+0−1
@@ -118,7 +118,6 @@ export interface AnonymousContext { · 117 unmodified lines
118118 actorId: string;
119119 clientId: string;
120120 accountId?: string;
121− workspaceDocId?: string;
122121 userId?: string;
123122 createdAt: number;
124123 lastSeenAt: number;