feat: added schemas for openapi (#452)

This commit is contained in:
Morfixx
2026-04-18 13:29:35 -07:00
committed by GitHub
parent 6056167063
commit ae4bc77407
15 changed files with 606 additions and 68 deletions

View File

@@ -8,6 +8,7 @@ import * as workspaceRepo from "@kan/db/repository/workspace.repo";
import { generateUID } from "@kan/shared/utils";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { attachmentConfirmResponseSchema } from "../schemas";
import { assertPermission } from "../utils/permissions";
import { deleteObject, generateUploadUrl } from "@kan/shared/utils";
@@ -110,7 +111,7 @@ export const attachmentRouter = createTRPCRouter({
size: z.number().positive(),
}),
)
.output(z.custom<Awaited<ReturnType<typeof cardAttachmentRepo.create>>>())
.output(attachmentConfirmResponseSchema)
.mutation(async ({ ctx, input }) => {
const userId = ctx.user?.id;

View File

@@ -16,6 +16,13 @@ import {
} from "@kan/shared/utils";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
import {
boardListItemSchema,
boardDetailSchema,
boardBySlugSchema,
boardCreateResponseSchema,
boardUpdateResponseSchema,
} from "../schemas";
import { assertCanDelete, assertCanEdit, assertPermission } from "../utils/permissions";
export const boardRouter = createTRPCRouter({
@@ -37,9 +44,7 @@ export const boardRouter = createTRPCRouter({
archived: z.boolean().optional(),
}),
)
.output(
z.custom<Awaited<ReturnType<typeof boardRepo.getAllByWorkspaceId>>>(),
)
.output(z.array(boardListItemSchema))
.query(async ({ ctx, input }) => {
const userId = ctx.user?.id;
@@ -106,7 +111,7 @@ export const boardRouter = createTRPCRouter({
type: z.enum(["regular", "template"]).optional(),
}),
)
.output(z.custom<Awaited<ReturnType<typeof boardRepo.getByPublicId>>>())
.output(boardDetailSchema)
.query(async ({ ctx, input }) => {
const userId = ctx.user?.id;
@@ -246,7 +251,7 @@ export const boardRouter = createTRPCRouter({
.optional(),
}),
)
.output(z.custom<Awaited<ReturnType<typeof boardRepo.getBySlug>>>())
.output(boardBySlugSchema.nullable())
.query(async ({ ctx, input }) => {
const workspace = await workspaceRepo.getBySlugWithBoards(
ctx.db,
@@ -299,7 +304,7 @@ export const boardRouter = createTRPCRouter({
sourceBoardPublicId: z.string().min(12).optional(),
}),
)
.output(z.custom<Awaited<ReturnType<typeof boardRepo.create>>>())
.output(boardCreateResponseSchema)
.mutation(async ({ ctx, input }) => {
const userId = ctx.user?.id;
@@ -469,7 +474,7 @@ export const boardRouter = createTRPCRouter({
isArchived: z.boolean().optional(),
}),
)
.output(z.object({ success: z.boolean() }).or(z.custom<Awaited<ReturnType<typeof boardRepo.update>>>()))
.output(boardUpdateResponseSchema)
.mutation(async ({ ctx, input }) => {
const userId = ctx.user?.id;

View File

@@ -10,6 +10,14 @@ import * as listRepo from "@kan/db/repository/list.repo";
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
import {
cardCreateResponseSchema,
cardUpdateResponseSchema,
cardDetailSchema,
commentResponseSchema,
commentDeleteResponseSchema,
activityItemSchema,
} from "../schemas";
import { mergeActivities } from "../utils/activities";
import { sendMentionEmails } from "../utils/notifications";
import { assertCanDelete, assertCanEdit, assertPermission } from "../utils/permissions";
@@ -42,7 +50,7 @@ export const cardRouter = createTRPCRouter({
dueDate: z.date().nullable().optional(),
}),
)
.output(z.custom<Awaited<ReturnType<typeof cardRepo.create>>>())
.output(cardCreateResponseSchema)
.mutation(async ({ ctx, input }) => {
const userId = ctx.user?.id;
@@ -215,7 +223,7 @@ export const cardRouter = createTRPCRouter({
comment: z.string().min(1),
}),
)
.output(z.custom<Awaited<ReturnType<typeof cardCommentRepo.create>>>())
.output(commentResponseSchema)
.mutation(async ({ ctx, input }) => {
const userId = ctx.user?.id;
@@ -288,7 +296,7 @@ export const cardRouter = createTRPCRouter({
comment: z.string().min(1),
}),
)
.output(z.custom<Awaited<ReturnType<typeof cardCommentRepo.update>>>())
.output(commentResponseSchema)
.mutation(async ({ ctx, input }) => {
const userId = ctx.user?.id;
@@ -376,7 +384,7 @@ export const cardRouter = createTRPCRouter({
commentPublicId: z.string().min(12),
}),
)
.output(z.custom<Awaited<ReturnType<typeof cardCommentRepo.softDelete>>>())
.output(commentDeleteResponseSchema)
.mutation(async ({ ctx, input }) => {
const userId = ctx.user?.id;
@@ -435,7 +443,7 @@ export const cardRouter = createTRPCRouter({
createdBy: userId,
});
return deletedComment;
return { publicId: input.commentPublicId };
}),
addOrRemoveLabel: protectedProcedure
.meta({
@@ -639,25 +647,7 @@ export const cardRouter = createTRPCRouter({
},
})
.input(z.object({ cardPublicId: z.string().min(12) }))
.output(
z.custom<
Omit<
NonNullable<
Awaited<ReturnType<typeof cardRepo.getWithListAndMembersByPublicId>>
>,
"attachments"
> & {
attachments: {
publicId: string;
contentType: string;
s3Key: string;
originalFilename: string | null;
size?: number | null;
url: string | null;
}[];
}
>(),
)
.output(cardDetailSchema)
.query(async ({ ctx, input }) => {
const card = await cardRepo.getWorkspaceAndCardIdByCardPublicId(
ctx.db,
@@ -763,15 +753,7 @@ export const cardRouter = createTRPCRouter({
)
.output(
z.object({
activities: z.array(
z.custom<
NonNullable<
Awaited<
ReturnType<typeof cardActivityRepo.getPaginatedActivities>
>
>["activities"][number]
>(),
),
activities: z.array(activityItemSchema),
hasMore: z.boolean(),
nextCursor: z.string().datetime().nullable(),
}),
@@ -871,7 +853,7 @@ export const cardRouter = createTRPCRouter({
dueDate: z.date().nullable().optional(),
}),
)
.output(z.custom<Awaited<ReturnType<typeof cardRepo.update>>>())
.output(cardUpdateResponseSchema)
.mutation(async ({ ctx, input }) => {
const userId = ctx.user?.id;

View File

@@ -7,6 +7,7 @@ import * as activityRepo from "@kan/db/repository/cardActivity.repo";
import * as listRepo from "@kan/db/repository/list.repo";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { listCreateResponseSchema, listUpdateResponseSchema } from "../schemas";
import { assertCanDelete, assertCanEdit, assertPermission } from "../utils/permissions";
export const listRouter = createTRPCRouter({
@@ -27,7 +28,7 @@ export const listRouter = createTRPCRouter({
boardPublicId: z.string().min(12),
}),
)
.output(z.custom<Awaited<ReturnType<typeof listRepo.create>>>())
.output(listCreateResponseSchema)
.mutation(async ({ ctx, input }) => {
const userId = ctx.user?.id;
@@ -163,12 +164,7 @@ export const listRouter = createTRPCRouter({
index: z.number().optional(),
}),
)
.output(
z.custom<
| Awaited<ReturnType<typeof listRepo.update>>
| Awaited<ReturnType<typeof listRepo.reorder>>
>(),
)
.output(listUpdateResponseSchema)
.mutation(async ({ ctx, input }) => {
const userId = ctx.user?.id;

View File

@@ -16,6 +16,7 @@ import {
import { updateSubscriptionSeats } from "@kan/stripe";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
import { memberInviteResponseSchema } from "../schemas";
import {
assertCanManageMember,
assertCanManageRole,
@@ -40,7 +41,7 @@ export const memberRouter = createTRPCRouter({
workspacePublicId: z.string().min(12),
}),
)
.output(z.custom<Awaited<ReturnType<typeof memberRepo.create>>>())
.output(memberInviteResponseSchema)
.mutation(async ({ ctx, input }) => {
const userId = ctx.user?.id;

View File

@@ -7,6 +7,14 @@ import * as workspaceSlugRepo from "@kan/db/repository/workspaceSlug.repo";
import { generateAvatarUrl, generateUID } from "@kan/shared/utils";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
import {
workspaceListItemSchema,
workspaceDetailSchema,
workspaceWithBoardsSchema,
workspaceCreateResponseSchema,
workspaceUpdateResponseSchema,
workspaceDeleteResponseSchema,
} from "../schemas";
import { assertPermission } from "../utils/permissions";
export const workspaceRouter = createTRPCRouter({
@@ -22,9 +30,7 @@ export const workspaceRouter = createTRPCRouter({
},
})
.input(z.void())
.output(
z.custom<Awaited<ReturnType<typeof workspaceRepo.getAllByUserId>>>(),
)
.output(z.array(workspaceListItemSchema))
.query(async ({ ctx }) => {
const userId = ctx.user?.id;
@@ -50,11 +56,7 @@ export const workspaceRouter = createTRPCRouter({
},
})
.input(z.object({ workspacePublicId: z.string().min(12) }))
.output(
z.custom<
Awaited<ReturnType<typeof workspaceRepo.getByPublicIdWithMembers>>
>(),
)
.output(workspaceDetailSchema)
.query(async ({ ctx, input }) => {
const userId = ctx.user?.id;
@@ -134,7 +136,7 @@ export const workspaceRouter = createTRPCRouter({
return {
...result,
members: sanitizedMembers,
} as Awaited<ReturnType<typeof workspaceRepo.getByPublicIdWithMembers>>;
};
}
return {
@@ -162,9 +164,7 @@ export const workspaceRouter = createTRPCRouter({
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/),
}),
)
.output(
z.custom<Awaited<ReturnType<typeof workspaceRepo.getBySlugWithBoards>>>(),
)
.output(workspaceWithBoardsSchema)
.query(async ({ ctx, input }) => {
const userId = ctx.user?.id;
@@ -211,7 +211,7 @@ export const workspaceRouter = createTRPCRouter({
.optional(),
}),
)
.output(z.custom<Awaited<ReturnType<typeof workspaceRepo.create>>>())
.output(workspaceCreateResponseSchema)
.mutation(async ({ ctx, input }) => {
const userId = ctx.user?.id;
const userEmail = ctx.user?.email;
@@ -270,7 +270,13 @@ export const workspaceRouter = createTRPCRouter({
code: "INTERNAL_SERVER_ERROR",
});
return result;
return {
publicId: result.publicId,
name: result.name!,
slug: result.slug!,
description: result.description ?? null,
plan: result.plan!,
};
}),
update: protectedProcedure
.meta({
@@ -300,7 +306,7 @@ export const workspaceRouter = createTRPCRouter({
.optional(),
}),
)
.output(z.custom<Awaited<ReturnType<typeof workspaceRepo.update>>>())
.output(workspaceUpdateResponseSchema)
.mutation(async ({ ctx, input }) => {
const userId = ctx.user?.id;
@@ -383,7 +389,7 @@ export const workspaceRouter = createTRPCRouter({
},
})
.input(z.object({ workspacePublicId: z.string().min(12) }))
.output(z.custom<Awaited<ReturnType<typeof workspaceRepo.hardDelete>>>())
.output(workspaceDeleteResponseSchema)
.mutation(async ({ ctx, input }) => {
const userId = ctx.user?.id;
@@ -405,12 +411,12 @@ export const workspaceRouter = createTRPCRouter({
});
await assertPermission(ctx.db, userId, workspace.id, "workspace:delete");
const result = await workspaceRepo.hardDelete(
await workspaceRepo.hardDelete(
ctx.db,
input.workspacePublicId,
);
return result;
return { success: true };
}),
checkSlugAvailability: publicProcedure
.meta({

View File

@@ -0,0 +1,12 @@
import { z } from "zod";
// ─── attachment.confirm ──────────────────────────────────────
export const attachmentConfirmResponseSchema = z.object({
publicId: z.string(),
filename: z.string(),
originalFilename: z.string(),
contentType: z.string(),
size: z.number(),
s3Key: z.string(),
createdAt: z.date(),
});

View File

@@ -0,0 +1,130 @@
import { z } from "zod";
import {
checklistResponseSchema,
labelSchema,
workspaceMemberSchema,
} from "./common";
// ─── board.all ───────────────────────────────────────────────
export const boardListItemSchema = z.object({
publicId: z.string(),
name: z.string(),
favorite: z.boolean(),
lists: z.array(
z.object({
publicId: z.string(),
name: z.string(),
index: z.number(),
}),
),
labels: z.array(labelSchema),
});
// ─── Card sub-object inside board detail (byId) ─────────────
const boardCardMemberSchema = z.object({
publicId: z.string(),
email: z.string(),
user: z
.object({
name: z.string().nullable(),
email: z.string(),
image: z.string().nullable(),
})
.nullable(),
});
const boardDetailCardSchema = z.object({
publicId: z.string(),
title: z.string(),
description: z.string().nullable(),
index: z.number(),
dueDate: z.date().nullable(),
labels: z.array(labelSchema),
members: z.array(boardCardMemberSchema),
attachments: z.array(z.object({ publicId: z.string() })),
checklists: z.array(checklistResponseSchema),
comments: z.array(z.object({ publicId: z.string() })),
});
// ─── board.byId ──────────────────────────────────────────────
export const boardDetailSchema = z.object({
publicId: z.string(),
name: z.string(),
slug: z.string(),
visibility: z.string(),
isArchived: z.boolean(),
favorite: z.boolean(),
workspace: z.object({
publicId: z.string(),
members: z.array(workspaceMemberSchema),
}),
labels: z.array(labelSchema),
lists: z.array(
z.object({
publicId: z.string(),
name: z.string(),
index: z.number(),
cards: z.array(boardDetailCardSchema),
}),
),
allLists: z.array(
z.object({
publicId: z.string(),
name: z.string(),
}),
),
});
// ─── Card sub-object inside board detail (bySlug — no members) ─
const boardSlugCardSchema = z.object({
publicId: z.string(),
title: z.string(),
description: z.string().nullable(),
index: z.number(),
dueDate: z.date().nullable(),
labels: z.array(labelSchema),
attachments: z.array(z.object({ publicId: z.string() })),
checklists: z.array(checklistResponseSchema),
comments: z.array(z.object({ publicId: z.string() })),
});
// ─── board.bySlug ────────────────────────────────────────────
export const boardBySlugSchema = z.object({
publicId: z.string(),
name: z.string(),
slug: z.string(),
visibility: z.string(),
workspace: z.object({
publicId: z.string(),
name: z.string(),
slug: z.string(),
}),
labels: z.array(labelSchema),
lists: z.array(
z.object({
publicId: z.string(),
name: z.string(),
index: z.number(),
cards: z.array(boardSlugCardSchema),
}),
),
allLists: z.array(
z.object({
publicId: z.string(),
name: z.string(),
}),
),
});
// ─── board.create / board.createFromSnapshot ─────────────────
export const boardCreateResponseSchema = z.object({
publicId: z.string(),
name: z.string(),
});
// ─── board.update ────────────────────────────────────────────
export const boardUpdateResponseSchema = z.union([
z.object({ success: z.boolean() }),
z.object({ publicId: z.string(), name: z.string() }),
]);

View File

@@ -0,0 +1,213 @@
import { z } from "zod";
import {
checklistResponseSchema,
labelSchema,
workspaceMemberSchema,
} from "./common";
// ─── card.create ─────────────────────────────────────────────
export const cardCreateResponseSchema = z.object({
publicId: z.string(),
});
// ─── card.update ─────────────────────────────────────────────
export const cardUpdateResponseSchema = z.object({
publicId: z.string(),
title: z.string(),
description: z.string().nullable(),
dueDate: z.date().nullable(),
});
// ─── Comment responses ───────────────────────────────────────
export const commentResponseSchema = z.object({
publicId: z.string(),
comment: z.string(),
});
export const commentDeleteResponseSchema = z.object({
publicId: z.string(),
});
// ─── card.byId ───────────────────────────────────────────────
const cardMemberSchema = z.object({
publicId: z.string(),
email: z.string(),
user: z
.object({
name: z.string().nullable(),
})
.nullable(),
});
export const cardDetailSchema = z.object({
publicId: z.string(),
title: z.string(),
description: z.string().nullable(),
dueDate: z.date().nullable(),
createdBy: z.string().nullable(),
labels: z.array(labelSchema),
attachments: z.array(
z.object({
publicId: z.string(),
contentType: z.string(),
s3Key: z.string(),
originalFilename: z.string().nullable(),
size: z.number().nullable(),
url: z.string().nullable(),
}),
),
checklists: z.array(checklistResponseSchema),
list: z.object({
publicId: z.string(),
name: z.string(),
board: z.object({
publicId: z.string(),
name: z.string(),
labels: z.array(labelSchema),
lists: z.array(
z.object({
publicId: z.string(),
name: z.string(),
}),
),
workspace: z.object({
publicId: z.string(),
members: z.array(workspaceMemberSchema),
}),
}),
}),
members: z.array(cardMemberSchema),
activities: z.array(
z.object({
publicId: z.string(),
type: z.string(),
createdAt: z.date(),
fromIndex: z.number().nullable(),
toIndex: z.number().nullable(),
fromTitle: z.string().nullable(),
toTitle: z.string().nullable(),
fromDescription: z.string().nullable(),
toDescription: z.string().nullable(),
fromDueDate: z.date().nullable(),
toDueDate: z.date().nullable(),
fromList: z
.object({
publicId: z.string(),
name: z.string(),
index: z.number(),
})
.nullable(),
toList: z
.object({
publicId: z.string(),
name: z.string(),
index: z.number(),
})
.nullable(),
label: z
.object({
publicId: z.string(),
name: z.string(),
})
.nullable(),
member: z
.object({
publicId: z.string(),
user: z
.object({
name: z.string().nullable(),
email: z.string(),
})
.nullable(),
})
.nullable(),
user: z
.object({
name: z.string().nullable(),
email: z.string(),
})
.nullable(),
comment: z
.object({
publicId: z.string(),
comment: z.string(),
createdBy: z.string().nullable(),
updatedAt: z.date().nullable(),
deletedAt: z.date().nullable(),
})
.nullable(),
}),
),
});
// ─── card.getActivities ──────────────────────────────────────
export const activityItemSchema = z.object({
publicId: z.string(),
type: z.string(),
createdAt: z.date(),
fromIndex: z.number().nullable(),
toIndex: z.number().nullable(),
fromTitle: z.string().nullable(),
toTitle: z.string().nullable(),
fromDescription: z.string().nullable(),
toDescription: z.string().nullable(),
fromDueDate: z.date().nullable(),
toDueDate: z.date().nullable(),
fromList: z
.object({
publicId: z.string(),
name: z.string(),
index: z.number(),
})
.nullable(),
toList: z
.object({
publicId: z.string(),
name: z.string(),
index: z.number(),
})
.nullable(),
label: z
.object({
publicId: z.string(),
name: z.string(),
})
.nullable(),
member: z
.object({
publicId: z.string(),
user: z
.object({
name: z.string().nullable(),
email: z.string(),
image: z.string().nullable(),
})
.nullable(),
})
.nullable(),
user: z
.object({
name: z.string().nullable(),
email: z.string(),
image: z.string().nullable(),
})
.nullable(),
comment: z
.object({
publicId: z.string(),
comment: z.string(),
createdBy: z.string().nullable(),
updatedAt: z.date().nullable(),
deletedAt: z.date().nullable(),
})
.nullable(),
attachment: z
.object({
publicId: z.string(),
filename: z.string(),
originalFilename: z.string().nullable(),
})
.nullable(),
});

View File

@@ -0,0 +1,39 @@
import { z } from "zod";
// Shared label schema used across board and card responses
export const labelSchema = z.object({
publicId: z.string(),
name: z.string(),
colourCode: z.string().nullable(),
});
// Shared checklist item schema
export const checklistItemResponseSchema = z.object({
publicId: z.string(),
title: z.string(),
completed: z.boolean(),
index: z.number(),
});
// Shared checklist schema with items
export const checklistResponseSchema = z.object({
publicId: z.string(),
name: z.string(),
index: z.number(),
items: z.array(checklistItemResponseSchema),
});
// Shared user sub-object (used inside member/workspace responses)
export const userSchema = z.object({
name: z.string().nullable(),
email: z.string(),
image: z.string().nullable(),
});
// Workspace member sub-object
export const workspaceMemberSchema = z.object({
publicId: z.string(),
email: z.string(),
status: z.string(),
user: userSchema.nullable(),
});

View File

@@ -0,0 +1,39 @@
export {
boardListItemSchema,
boardDetailSchema,
boardBySlugSchema,
boardCreateResponseSchema,
boardUpdateResponseSchema,
} from "./board";
export {
cardCreateResponseSchema,
cardUpdateResponseSchema,
cardDetailSchema,
commentResponseSchema,
commentDeleteResponseSchema,
activityItemSchema,
} from "./card";
export {
labelSchema,
checklistItemResponseSchema,
checklistResponseSchema,
userSchema,
workspaceMemberSchema,
} from "./common";
export {
workspaceListItemSchema,
workspaceDetailSchema,
workspaceWithBoardsSchema,
workspaceCreateResponseSchema,
workspaceUpdateResponseSchema,
workspaceDeleteResponseSchema,
} from "./workspace";
export { listCreateResponseSchema, listUpdateResponseSchema } from "./list";
export { memberInviteResponseSchema } from "./member";
export { attachmentConfirmResponseSchema } from "./attachment";

View File

@@ -0,0 +1,13 @@
import { z } from "zod";
// ─── list.create ─────────────────────────────────────────────
export const listCreateResponseSchema = z.object({
publicId: z.string(),
name: z.string(),
});
// ─── list.update / list.reorder ──────────────────────────────
export const listUpdateResponseSchema = z.object({
publicId: z.string(),
name: z.string(),
});

View File

@@ -0,0 +1,6 @@
import { z } from "zod";
// ─── member.invite ───────────────────────────────────────────
export const memberInviteResponseSchema = z.object({
publicId: z.string(),
});

View File

@@ -0,0 +1,92 @@
import { z } from "zod";
import { userSchema } from "./common";
// ─── workspace.all ───────────────────────────────────────────
export const workspaceListItemSchema = z.object({
role: z.string(),
workspace: z.object({
publicId: z.string(),
name: z.string(),
description: z.string().nullable(),
slug: z.string(),
plan: z.enum(["free", "pro", "enterprise"]),
weekStartDay: z.number().nullable(),
deletedAt: z.date().nullable(),
}),
});
// ─── workspace.byId ──────────────────────────────────────────
const workspaceMemberDetailSchema = z.object({
publicId: z.string(),
email: z.string().optional(),
role: z.string(),
status: z.string(),
createdAt: z.date(),
user: z
.object({
name: z.string().nullable(),
email: z.string().optional(),
image: z.string().nullable(),
})
.nullable(),
});
const workspaceSubscriptionSchema = z.object({
plan: z.string(),
status: z.string(),
seats: z.number().nullable(),
unlimitedSeats: z.boolean().nullable(),
periodStart: z.date().nullable(),
periodEnd: z.date().nullable(),
});
export const workspaceDetailSchema = z.object({
publicId: z.string(),
name: z.string(),
slug: z.string(),
showEmailsToMembers: z.boolean().nullable(),
weekStartDay: z.number().nullable(),
members: z.array(workspaceMemberDetailSchema),
subscriptions: z.array(workspaceSubscriptionSchema),
});
// ─── workspace.bySlug ────────────────────────────────────────
export const workspaceWithBoardsSchema = z.object({
publicId: z.string(),
name: z.string(),
description: z.string().nullable(),
slug: z.string(),
boards: z.array(
z.object({
publicId: z.string(),
slug: z.string(),
name: z.string(),
}),
),
});
// ─── workspace.create ────────────────────────────────────────
export const workspaceCreateResponseSchema = z.object({
publicId: z.string(),
name: z.string(),
slug: z.string(),
description: z.string().nullable(),
plan: z.enum(["free", "pro", "enterprise"]),
});
// ─── workspace.update ────────────────────────────────────────
export const workspaceUpdateResponseSchema = z.object({
publicId: z.string(),
name: z.string(),
slug: z.string(),
description: z.string().nullable(),
plan: z.enum(["free", "pro", "enterprise"]),
showEmailsToMembers: z.boolean().nullable(),
weekStartDay: z.number().nullable(),
});
// ─── workspace.delete ────────────────────────────────────────
export const workspaceDeleteResponseSchema = z.object({
success: z.boolean(),
});