Files
zopu-code/packages/backend/convex/organizations.ts

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;
},
});