83 lines
2.6 KiB
TypeScript
83 lines
2.6 KiB
TypeScript
import type { Doc, Id } from "./_generated/dataModel";
|
|
import { mutation, query } from "./_generated/server";
|
|
import { requireAuthUserId } from "./authz";
|
|
|
|
interface OrganizationView {
|
|
readonly _id: Id<"organizations">;
|
|
readonly _creationTime: number;
|
|
readonly name: string;
|
|
readonly kind: "personal" | "team";
|
|
readonly createdBy: string;
|
|
readonly createdAt: number;
|
|
}
|
|
|
|
const toView = (org: Doc<"organizations">): OrganizationView => ({
|
|
_creationTime: org._creationTime,
|
|
_id: org._id,
|
|
createdAt: org.createdAt,
|
|
createdBy: org.createdBy,
|
|
kind: org.kind,
|
|
name: org.name,
|
|
});
|
|
|
|
/**
|
|
* Idempotently create (or return) the authenticated user's single personal
|
|
* organization plus an owner membership. Convex mutations are atomic
|
|
* transactions: if two concurrent calls both observe no existing org and both
|
|
* insert, the transaction's optimistic-concurrency check rejects one; the
|
|
* retried call re-reads and returns the now-existing org. The read+write is the
|
|
* whole transaction, so no duplicate personal org can persist.
|
|
*/
|
|
export const ensurePersonalOrganization = mutation({
|
|
args: {},
|
|
handler: async (ctx): Promise<OrganizationView> => {
|
|
const ownerId = await requireAuthUserId(ctx);
|
|
const existing = await ctx.db
|
|
.query("organizations")
|
|
.withIndex("by_createdBy_and_kind", (q) =>
|
|
q.eq("createdBy", ownerId).eq("kind", "personal")
|
|
)
|
|
.unique();
|
|
if (existing) {
|
|
return toView(existing);
|
|
}
|
|
const createdAt = Date.now();
|
|
const organizationId = await ctx.db.insert("organizations", {
|
|
createdAt,
|
|
createdBy: ownerId,
|
|
kind: "personal",
|
|
name: "Personal",
|
|
});
|
|
await ctx.db.insert("organizationMembers", {
|
|
createdAt,
|
|
organizationId,
|
|
role: "owner",
|
|
userId: ownerId,
|
|
});
|
|
const created = await ctx.db.get(organizationId);
|
|
if (!created) {
|
|
throw new Error("Failed to create personal organization");
|
|
}
|
|
return toView(created);
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Return the authenticated user's current organization. In the single personal
|
|
* organization phase this is the user's one personal org. Returns null when the
|
|
* user has not yet ensured one. Denies unauthenticated access.
|
|
*/
|
|
export const getCurrent = query({
|
|
args: {},
|
|
handler: async (ctx): Promise<OrganizationView | null> => {
|
|
const ownerId = await requireAuthUserId(ctx);
|
|
const existing = await ctx.db
|
|
.query("organizations")
|
|
.withIndex("by_createdBy_and_kind", (q) =>
|
|
q.eq("createdBy", ownerId).eq("kind", "personal")
|
|
)
|
|
.unique();
|
|
return existing ? toView(existing) : null;
|
|
},
|
|
});
|