History

Remove unknown context casts from organization and billing handlers

Rhys Sullivan committed 9 months ago

Commit f46bd121 parent b5a42d64 files · +90 −101Browse files

Unified · Split

4 files
  1. executor/packages/database/src/billing/internal-handlers.ts +12 −13
  2. executor/packages/database/src/billing/public-handlers.ts +21 −23
  3. executor/packages/database/src/organizations/handlers.ts +32 −37
  4. executor/packages/database/src/workspaces/handlers.ts +25 −28
executor/packages/database/src/billing/internal-handlers.ts+12−13
@@ -2,6 +2,11 @@ import type { Doc, Id } from "../../convex/_generated/dataModel.d.ts"; · 1 unmodified line
2import type { MutationCtx, QueryCtx } from "../../convex/_generated/server";2import type { MutationCtx, QueryCtx } from "../../convex/_generated/server";
3 3
4type DbCtx = Pick<QueryCtx, "db"> | Pick<MutationCtx, "db">;4type DbCtx = Pick<QueryCtx, "db"> | Pick<MutationCtx, "db">;
5type BillingAccessCtx = QueryCtx & {
6 account: Doc<"accounts">;
7 actorMembership: Doc<"organizationMembers">;
8 organizationId: Id<"organizations">;
9};
5 10
6async function getBillableSeatCount(ctx: DbCtx, organizationId: Id<"organizations">): Promise<number> {11async function getBillableSeatCount(ctx: DbCtx, organizationId: Id<"organizations">): Promise<number> {
7 const members = await ctx.db12 const members = await ctx.db
@@ -23,27 +28,21 @@ async function getSeatState( · 15 unmodified lines
23 .unique();28 .unique();
24}29}
25 30
26export async function getBillingAccessForRequestHandler(ctx: unknown) {31export async function getBillingAccessForRequestHandler(ctx: BillingAccessCtx) {
27 const typedCtx = ctx as QueryCtx & {32 const organization = await ctx.db.get(ctx.organizationId);
28 account: Doc<"accounts">;
29 actorMembership: Doc<"organizationMembers">;
30 organizationId: Id<"organizations">;
31 };
32
33 const organization = await typedCtx.db.get(typedCtx.organizationId);
34 if (!organization) {33 if (!organization) {
35 return null;34 return null;
36 }35 }
37 36
38 const billableMembers = await getBillableSeatCount(typedCtx, typedCtx.organizationId);37 const billableMembers = await getBillableSeatCount(ctx, ctx.organizationId);
39 const customer = await typedCtx.db38 const customer = await ctx.db
40 .query("billingCustomers")39 .query("billingCustomers")
41 .withIndex("by_org", (q) => q.eq("organizationId", typedCtx.organizationId))40 .withIndex("by_org", (q) => q.eq("organizationId", ctx.organizationId))
42 .unique();41 .unique();
43 42
44 return {43 return {
45 role: typedCtx.actorMembership.role,44 role: ctx.actorMembership.role,
46 email: typedCtx.account.email,45 email: ctx.account.email,
47 organizationName: organization.name,46 organizationName: organization.name,
48 billableMembers,47 billableMembers,
49 customerId: customer?.stripeCustomerId ?? null,48 customerId: customer?.stripeCustomerId ?? null,
executor/packages/database/src/billing/public-handlers.ts+21−23
@@ -8,6 +8,14 @@ type Internal = typeof import("../../convex/_generated/api").internal; · 7 unmodified lines
8type Components = typeof import("../../convex/_generated/api").components;8type Components = typeof import("../../convex/_generated/api").components;
9 9
10type DbCtx = Pick<QueryCtx, "db"> | Pick<MutationCtx, "db">;10type DbCtx = Pick<QueryCtx, "db"> | Pick<MutationCtx, "db">;
11type BillingSummaryCtx = QueryCtx & {
12 organizationId: Id<"organizations">;
13 runQuery: QueryCtx["runQuery"];
14};
15type RetrySeatSyncCtx = MutationCtx & {
16 organizationId: Id<"organizations">;
17 scheduler: MutationCtx["scheduler"];
18};
11 19
12function canAccessBilling(role: string): boolean {20function canAccessBilling(role: string): boolean {
13 return isAdminRole(role) || canManageBilling(role);21 return isAdminRole(role) || canManageBilling(role);
@@ -31,29 +39,24 @@ async function getSeatState(ctx: DbCtx, organizationId: Id<"organizations">) { · 17 unmodified lines
31}39}
32 40
33export async function getBillingSummaryHandler(41export async function getBillingSummaryHandler(
34 ctx: unknown,42 ctx: BillingSummaryCtx,
35 components: Components,43 components: Components,
36) {44) {
37 const typedCtx = ctx as QueryCtx & {45 const billableMembers = await getBillableSeatCount(ctx, ctx.organizationId);
38 organizationId: Id<"organizations">;46 const seatState = await getSeatState(ctx, ctx.organizationId);
39 runQuery: QueryCtx["runQuery"];47 const customer = await ctx.db
40 };
41
42 const billableMembers = await getBillableSeatCount(typedCtx, typedCtx.organizationId);
43 const seatState = await getSeatState(typedCtx, typedCtx.organizationId);
44 const customer = await typedCtx.db
45 .query("billingCustomers")48 .query("billingCustomers")
46 .withIndex("by_org", (q) => q.eq("organizationId", typedCtx.organizationId))49 .withIndex("by_org", (q) => q.eq("organizationId", ctx.organizationId))
47 .unique();50 .unique();
48 51
49 const subscription = await typedCtx.runQuery(components.stripe.public.getSubscriptionByOrgId, {52 const subscription = await ctx.runQuery(components.stripe.public.getSubscriptionByOrgId, {
50 orgId: String(typedCtx.organizationId),53 orgId: String(ctx.organizationId),
51 });54 });
52 55
53 const syncStatus = seatState?.syncError ? "error" : seatState?.lastSyncAt ? "ok" : "pending";56 const syncStatus = seatState?.syncError ? "error" : seatState?.lastSyncAt ? "ok" : "pending";
54 57
55 return {58 return {
56 organizationId: String(typedCtx.organizationId),59 organizationId: String(ctx.organizationId),
57 customer: customer60 customer: customer
58 ? {61 ? {
59 stripeCustomerId: customer.stripeCustomerId,62 stripeCustomerId: customer.stripeCustomerId,
@@ -198,20 +201,15 @@ export async function createCustomerPortalHandler( · 138 unmodified lines
198}201}
199 202
200export async function retrySeatSyncHandler(203export async function retrySeatSyncHandler(
201 ctx: unknown,204 ctx: RetrySeatSyncCtx,
202 internal: Internal,205 internal: Internal,
203) {206) {
204 const typedCtx = ctx as MutationCtx & {207 const nextVersion = await ctx.runMutation(internal.billingInternal.bumpSeatSyncVersion, {
205 organizationId: Id<"organizations">;208 organizationId: ctx.organizationId,
206 scheduler: MutationCtx["scheduler"];
207 };
208
209 const nextVersion = await typedCtx.runMutation(internal.billingInternal.bumpSeatSyncVersion, {
210 organizationId: typedCtx.organizationId,
211 });209 });
212 210
213 await safeRunAfter(typedCtx.scheduler, 0, internal.billingSync.syncSeatQuantity, {211 await safeRunAfter(ctx.scheduler, 0, internal.billingSync.syncSeatQuantity, {
214 organizationId: typedCtx.organizationId,212 organizationId: ctx.organizationId,
215 expectedVersion: nextVersion,213 expectedVersion: nextVersion,
216 });214 });
217 215
executor/packages/database/src/organizations/handlers.ts+32−37
@@ -50,17 +50,16 @@ async function mapWorkspaceWithIcon( · 49 unmodified lines
50 };50 };
51}51}
52 52
53export async function createOrganizationHandler(ctx: unknown, args: { name: string }) {53export async function createOrganizationHandler(ctx: AuthedCtx, args: { name: string }) {
54 const typedCtx = ctx as AuthedCtx;54 const account = ctx.account;
55 const account = typedCtx.account;
56 const name = args.name.trim();55 const name = args.name.trim();
57 if (name.length < 2) {56 if (name.length < 2) {
58 throw new Error("Organization name must be at least 2 characters");57 throw new Error("Organization name must be at least 2 characters");
59 }58 }
60 59
61 const now = Date.now();60 const now = Date.now();
62 const slug = await ensureUniqueOrganizationSlug(typedCtx, name);61 const slug = await ensureUniqueOrganizationSlug(ctx, name);
63 const organizationId = await typedCtx.db.insert("organizations", {62 const organizationId = await ctx.db.insert("organizations", {
64 slug,63 slug,
65 name,64 name,
66 status: "active",65 status: "active",
@@ -69,7 +68,7 @@ export async function createOrganizationHandler(ctx: unknown, args: { name: stri · 2 unmodified lines
69 updatedAt: now,68 updatedAt: now,
70 });69 });
71 70
72 await upsertOrganizationMembership(typedCtx, {71 await upsertOrganizationMembership(ctx, {
73 organizationId,72 organizationId,
74 accountId: account._id,73 accountId: account._id,
75 role: "owner",74 role: "owner",
@@ -78,7 +77,7 @@ export async function createOrganizationHandler(ctx: unknown, args: { name: stri · 2 unmodified lines
78 now,77 now,
79 });78 });
80 79
81 const workspaceId = await typedCtx.db.insert("workspaces", {80 const workspaceId = await ctx.db.insert("workspaces", {
82 organizationId,81 organizationId,
83 slug: "default",82 slug: "default",
84 name: "Default Workspace",83 name: "Default Workspace",
@@ -87,15 +86,15 @@ export async function createOrganizationHandler(ctx: unknown, args: { name: stri · 2 unmodified lines
87 updatedAt: now,86 updatedAt: now,
88 });87 });
89 88
90 await seedWorkspaceMembersFromOrganization(typedCtx, {89 await seedWorkspaceMembersFromOrganization(ctx, {
91 organizationId,90 organizationId,
92 workspaceId,91 workspaceId,
93 now,92 now,
94 mapRole: mapOrganizationRoleToWorkspaceRole,93 mapRole: mapOrganizationRoleToWorkspaceRole,
95 });94 });
96 95
97 const organization = await typedCtx.db.get(organizationId);96 const organization = await ctx.db.get(organizationId);
98 const workspace = await typedCtx.db.get(workspaceId);97 const workspace = await ctx.db.get(workspaceId);
99 if (!organization || !workspace) {98 if (!organization || !workspace) {
100 throw new Error("Failed to create organization");99 throw new Error("Failed to create organization");
101 }100 }
@@ -108,18 +107,17 @@ export async function createOrganizationHandler(ctx: unknown, args: { name: stri · 6 unmodified lines
108 status: organization.status,107 status: organization.status,
109 createdAt: organization.createdAt,108 createdAt: organization.createdAt,
110 },109 },
111 workspace: await mapWorkspaceWithIcon(typedCtx, workspace),110 workspace: await mapWorkspaceWithIcon(ctx, workspace),
112 };111 };
113}112}
114 113
115export async function listOrganizationsMineHandler(ctx: unknown) {114export async function listOrganizationsMineHandler(ctx: OptionalAccountCtx) {
116 const typedCtx = ctx as OptionalAccountCtx;115 const account = ctx.account;
117 const account = typedCtx.account;
118 if (!account) {116 if (!account) {
119 return [];117 return [];
120 }118 }
121 119
122 const memberships = await typedCtx.db120 const memberships = await ctx.db
123 .query("organizationMembers")121 .query("organizationMembers")
124 .withIndex("by_account", (q) => q.eq("accountId", account._id))122 .withIndex("by_account", (q) => q.eq("accountId", account._id))
125 .collect();123 .collect();
@@ -128,7 +126,7 @@ export async function listOrganizationsMineHandler(ctx: unknown) { · 2 unmodified lines
128 memberships126 memberships
129 .filter((membership) => membership.status === "active")127 .filter((membership) => membership.status === "active")
130 .map(async (membership) => {128 .map(async (membership) => {
131 const org = await typedCtx.db.get(membership.organizationId);129 const org = await ctx.db.get(membership.organizationId);
132 if (!org) {130 if (!org) {
133 return null;131 return null;
134 }132 }
@@ -146,22 +144,21 @@ export async function listOrganizationsMineHandler(ctx: unknown) { · 11 unmodified lines
146 return organizations.filter((org): org is NonNullable<typeof org> => org !== null);144 return organizations.filter((org): org is NonNullable<typeof org> => org !== null);
147}145}
148 146
149export async function getNavigationStateHandler(ctx: unknown) {147export async function getNavigationStateHandler(ctx: OptionalAccountCtx) {
150 const typedCtx = ctx as OptionalAccountCtx;148 const account = ctx.account;
151 const account = typedCtx.account;
152 const organizations: Array<{ id: Id<"organizations">; name: string; slug: string; status: string; role: string }> = [];149 const organizations: Array<{ id: Id<"organizations">; name: string; slug: string; status: string; role: string }> = [];
153 const workspaces: WorkspaceSummary[] = [];150 const workspaces: WorkspaceSummary[] = [];
154 151
155 if (!account) {152 if (!account) {
156 if (typedCtx.sessionId) {153 if (ctx.sessionId) {
157 const anonymousSession = await typedCtx.db154 const anonymousSession = await ctx.db
158 .query("anonymousSessions")155 .query("anonymousSessions")
159 .withIndex("by_session_id", (q) => q.eq("sessionId", typedCtx.sessionId as string))156 .withIndex("by_session_id", (q) => q.eq("sessionId", ctx.sessionId))
160 .unique();157 .unique();
161 if (anonymousSession?.workspaceId) {158 if (anonymousSession?.workspaceId) {
162 const workspace = await typedCtx.db.get(anonymousSession.workspaceId);159 const workspace = await ctx.db.get(anonymousSession.workspaceId);
163 if (workspace) {160 if (workspace) {
164 workspaces.push(await mapWorkspaceWithIcon(typedCtx, workspace));161 workspaces.push(await mapWorkspaceWithIcon(ctx, workspace));
165 }162 }
166 }163 }
167 }164 }
@@ -174,7 +171,7 @@ export async function getNavigationStateHandler(ctx: unknown) { · 6 unmodified lines
174 };171 };
175 }172 }
176 173
177 const memberships = await typedCtx.db174 const memberships = await ctx.db
178 .query("organizationMembers")175 .query("organizationMembers")
179 .withIndex("by_account", (q) => q.eq("accountId", account._id))176 .withIndex("by_account", (q) => q.eq("accountId", account._id))
180 .collect();177 .collect();
@@ -182,7 +179,7 @@ export async function getNavigationStateHandler(ctx: unknown) { · 1 unmodified line
182 const activeMemberships = memberships.filter((membership) => membership.status === "active");179 const activeMemberships = memberships.filter((membership) => membership.status === "active");
183 180
184 for (const membership of activeMemberships) {181 for (const membership of activeMemberships) {
185 const org = await typedCtx.db.get(membership.organizationId);182 const org = await ctx.db.get(membership.organizationId);
186 if (!org) {183 if (!org) {
187 continue;184 continue;
188 }185 }
@@ -195,12 +192,12 @@ export async function getNavigationStateHandler(ctx: unknown) { · 6 unmodified lines
195 role: membership.role,192 role: membership.role,
196 });193 });
197 194
198 const orgWorkspaces = await typedCtx.db195 const orgWorkspaces = await ctx.db
199 .query("workspaces")196 .query("workspaces")
200 .withIndex("by_organization_created", (q) => q.eq("organizationId", org._id))197 .withIndex("by_organization_created", (q) => q.eq("organizationId", org._id))
201 .collect();198 .collect();
202 for (const workspace of orgWorkspaces) {199 for (const workspace of orgWorkspaces) {
203 workspaces.push(await mapWorkspaceWithIcon(typedCtx, workspace));200 workspaces.push(await mapWorkspaceWithIcon(ctx, workspace));
204 }201 }
205 }202 }
206 203
@@ -217,16 +214,15 @@ export async function getNavigationStateHandler(ctx: unknown) { · 10 unmodified lines
217}214}
218 215
219export async function getOrganizationAccessHandler(216export async function getOrganizationAccessHandler(
220 ctx: unknown,217 ctx: OptionalAccountCtx,
221 args: { organizationId: Id<"organizations"> },218 args: { organizationId: Id<"organizations"> },
222) {219) {
223 const typedCtx = ctx as OptionalAccountCtx;220 const account = ctx.account;
224 const account = typedCtx.account;
225 if (!account) {221 if (!account) {
226 return null;222 return null;
227 }223 }
228 224
229 const membership = await getOrganizationMembership(typedCtx, args.organizationId, account._id);225 const membership = await getOrganizationMembership(ctx, args.organizationId, account._id);
230 if (!membership || membership.status !== "active") {226 if (!membership || membership.status !== "active") {
231 return null;227 return null;
232 }228 }
@@ -240,21 +236,20 @@ export async function getOrganizationAccessHandler( · 7 unmodified lines
240}236}
241 237
242export async function resolveWorkosOrganizationIdHandler(238export async function resolveWorkosOrganizationIdHandler(
243 ctx: unknown,239 ctx: OptionalAccountCtx,
244 args: { organizationId: Id<"organizations"> },240 args: { organizationId: Id<"organizations"> },
245) {241) {
246 const typedCtx = ctx as OptionalAccountCtx;242 const account = ctx.account;
247 const account = typedCtx.account;
248 if (!account) {243 if (!account) {
249 return null;244 return null;
250 }245 }
251 246
252 const membership = await getOrganizationMembership(typedCtx, args.organizationId, account._id);247 const membership = await getOrganizationMembership(ctx, args.organizationId, account._id);
253 if (!membership || membership.status !== "active") {248 if (!membership || membership.status !== "active") {
254 return null;249 return null;
255 }250 }
256 251
257 const organization = await typedCtx.db.get(args.organizationId);252 const organization = await ctx.db.get(args.organizationId);
258 if (!organization || organization.status !== "active") {253 if (!organization || organization.status !== "active") {
259 return null;254 return null;
260 }255 }
executor/packages/database/src/workspaces/handlers.ts+25−28
@@ -152,11 +152,10 @@ async function toWorkspaceResult( · 151 unmodified lines
152}152}
153 153
154export async function createWorkspaceHandler(154export async function createWorkspaceHandler(
155 ctx: unknown,155 ctx: AuthedCtx,
156 args: { name: string; organizationId?: Id<"organizations">; iconStorageId?: Id<"_storage"> },156 args: { name: string; organizationId?: Id<"organizations">; iconStorageId?: Id<"_storage"> },
157) {157) {
158 const typedCtx = ctx as AuthedCtx;158 const account = ctx.account;
159 const account = typedCtx.account;
160 const name = args.name.trim();159 const name = args.name.trim();
161 if (name.length < 2) {160 if (name.length < 2) {
162 throw new Error("Workspace name must be at least 2 characters");161 throw new Error("Workspace name must be at least 2 characters");
@@ -164,14 +163,14 @@ export async function createWorkspaceHandler( · 1 unmodified line
164 163
165 let organizationId = args.organizationId;164 let organizationId = args.organizationId;
166 if (organizationId) {165 if (organizationId) {
167 const membership = await getOrganizationMembership(typedCtx, organizationId, account._id);166 const membership = await getOrganizationMembership(ctx, organizationId, account._id);
168 if (!membership || membership.status !== "active") {167 if (!membership || membership.status !== "active") {
169 throw new Error("You are not a member of this organization");168 throw new Error("You are not a member of this organization");
170 }169 }
171 } else {170 } else {
172 const now = Date.now();171 const now = Date.now();
173 const organizationSlug = await ensureUniqueOrganizationSlug(typedCtx, name);172 const organizationSlug = await ensureUniqueOrganizationSlug(ctx, name);
174 organizationId = await typedCtx.db.insert("organizations", {173 organizationId = await ctx.db.insert("organizations", {
175 slug: organizationSlug,174 slug: organizationSlug,
176 name,175 name,
177 status: "active",176 status: "active",
@@ -180,7 +179,7 @@ export async function createWorkspaceHandler( · 2 unmodified lines
180 updatedAt: now,179 updatedAt: now,
181 });180 });
182 181
183 await upsertOrganizationMembership(typedCtx, {182 await upsertOrganizationMembership(ctx, {
184 organizationId,183 organizationId,
185 accountId: account._id,184 accountId: account._id,
186 role: "owner",185 role: "owner",
@@ -195,9 +194,9 @@ export async function createWorkspaceHandler( · 8 unmodified lines
195 }194 }
196 195
197 const now = Date.now();196 const now = Date.now();
198 const slug = await ensureUniqueWorkspaceSlug(typedCtx, organizationId, name);197 const slug = await ensureUniqueWorkspaceSlug(ctx, organizationId, name);
199 198
200 const workspaceId = await typedCtx.db.insert("workspaces", {199 const workspaceId = await ctx.db.insert("workspaces", {
201 organizationId,200 organizationId,
202 slug,201 slug,
203 name,202 name,
@@ -207,46 +206,45 @@ export async function createWorkspaceHandler( · 3 unmodified lines
207 updatedAt: now,206 updatedAt: now,
208 });207 });
209 208
210 const workspace = await typedCtx.db.get(workspaceId);209 const workspace = await ctx.db.get(workspaceId);
211 if (!workspace) {210 if (!workspace) {
212 throw new Error("Failed to create workspace");211 throw new Error("Failed to create workspace");
213 }212 }
214 213
215 await seedWorkspaceMembersFromOrganization(typedCtx, {214 await seedWorkspaceMembersFromOrganization(ctx, {
216 organizationId,215 organizationId,
217 workspaceId,216 workspaceId,
218 now,217 now,
219 mapRole: mapOrganizationRoleToWorkspaceRole,218 mapRole: mapOrganizationRoleToWorkspaceRole,
220 });219 });
221 220
222 await cleanupEmptyStarterWorkspace(typedCtx, organizationId, workspaceId);221 await cleanupEmptyStarterWorkspace(ctx, organizationId, workspaceId);
223 222
224 return await toWorkspaceResult(typedCtx, workspace);223 return await toWorkspaceResult(ctx, workspace);
225}224}
226 225
227export async function listWorkspacesHandler(ctx: unknown, args: { organizationId?: Id<"organizations"> }) {226export async function listWorkspacesHandler(ctx: OptionalAccountCtx, args: { organizationId?: Id<"organizations"> }) {
228 const typedCtx = ctx as OptionalAccountCtx;227 const account = ctx.account;
229 const account = typedCtx.account;
230 if (!account) {228 if (!account) {
231 return [];229 return [];
232 }230 }
233 231
234 const organizationId = args.organizationId;232 const organizationId = args.organizationId;
235 if (organizationId) {233 if (organizationId) {
236 const membership = await getOrganizationMembership(typedCtx, organizationId, account._id);234 const membership = await getOrganizationMembership(ctx, organizationId, account._id);
237 if (!membership || membership.status !== "active") {235 if (!membership || membership.status !== "active") {
238 return [];236 return [];
239 }237 }
240 238
241 const docs = await typedCtx.db239 const docs = await ctx.db
242 .query("workspaces")240 .query("workspaces")
243 .withIndex("by_organization_created", (q) => q.eq("organizationId", organizationId))241 .withIndex("by_organization_created", (q) => q.eq("organizationId", organizationId))
244 .collect();242 .collect();
245 const filteredDocs = await filterDisplayWorkspaces(typedCtx, docs);243 const filteredDocs = await filterDisplayWorkspaces(ctx, docs);
246 return await Promise.all(filteredDocs.map(async (workspace) => await toWorkspaceResult(typedCtx, workspace)));244 return await Promise.all(filteredDocs.map(async (workspace) => await toWorkspaceResult(ctx, workspace)));
247 }245 }
248 246
249 const memberships = await typedCtx.db247 const memberships = await ctx.db
250 .query("organizationMembers")248 .query("organizationMembers")
251 .withIndex("by_account", (q) => q.eq("accountId", account._id))249 .withIndex("by_account", (q) => q.eq("accountId", account._id))
252 .collect();250 .collect();
@@ -255,21 +253,20 @@ export async function listWorkspacesHandler(ctx: unknown, args: { organizationId · 2 unmodified lines
255 const allWorkspaces: WorkspaceResult[] = [];253 const allWorkspaces: WorkspaceResult[] = [];
256 254
257 for (const membership of activeMemberships) {255 for (const membership of activeMemberships) {
258 const docs = await typedCtx.db256 const docs = await ctx.db
259 .query("workspaces")257 .query("workspaces")
260 .withIndex("by_organization_created", (q) => q.eq("organizationId", membership.organizationId))258 .withIndex("by_organization_created", (q) => q.eq("organizationId", membership.organizationId))
261 .collect();259 .collect();
262 const filteredDocs = await filterDisplayWorkspaces(typedCtx, docs);260 const filteredDocs = await filterDisplayWorkspaces(ctx, docs);
263 for (const workspace of filteredDocs) {261 for (const workspace of filteredDocs) {
264 allWorkspaces.push(await toWorkspaceResult(typedCtx, workspace));262 allWorkspaces.push(await toWorkspaceResult(ctx, workspace));
265 }263 }
266 }264 }
267 265
268 return Array.from(new Map(allWorkspaces.map((workspace) => [workspace.id, workspace])).values());266 return Array.from(new Map(allWorkspaces.map((workspace) => [workspace.id, workspace])).values());
269}267}
270 268
271export async function generateWorkspaceIconUploadUrlHandler(ctx: unknown) {269export async function generateWorkspaceIconUploadUrlHandler(ctx: AuthedCtx) {
272 const typedCtx = ctx as AuthedCtx;270 void ctx.account;
273 void typedCtx.account;271 return await ctx.storage.generateUploadUrl();
274 return await typedCtx.storage.generateUploadUrl();
275}272}