feat(workspaces): scaffold feature

Scaffold the workspaces feature package via pnpm turbo gen feature
(single Workspace entity, getWorkspace use case) and hand-wire the
aggregators: bindAll dispatcher (web-next), core-api app router, and
workspace deps. Release-please registration reverted per the root-only
versioning policy (AGENTS.md, ADR-027 retrofit). Pinned @trpc/server
to the repo-wide 11.16.0 resolution so instanceof TRPCError checks
share one module instance.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016j8z4VHjedXDTjEDNg7qHK
This commit is contained in:
2026-07-12 21:37:17 +02:00
parent 97593593f2
commit 6216897680
46 changed files with 1214 additions and 5 deletions

View File

@@ -0,0 +1,5 @@
import type { Workspace } from "../../entities/models/workspace";
export interface IWorkspaceRepository {
getWorkspace(id: string): Promise<Workspace | null>;
}

View File

@@ -0,0 +1,31 @@
import { describe, expect, it } from "vitest";
import { ZodError } from "zod";
import { getWorkspaceUseCase } from "@/application/use-cases/get-workspace.use-case";
import { MockWorkspaceRepository } from "@/infrastructure/repositories/workspace.repository.mock";
import { WorkspaceNotFoundError } from "@/entities/errors/workspace";
describe("getWorkspaceUseCase", () => {
it("returns the seeded workspace by id", async () => {
const repo = new MockWorkspaceRepository();
const useCase = getWorkspaceUseCase(repo);
const result = await useCase({ id: "seed-1" });
expect(result.id).toBe("seed-1");
expect(result.name).toBeTypeOf("string");
});
it("throws WorkspaceNotFoundError when repository returns null", async () => {
const repo = new MockWorkspaceRepository(new Map());
const useCase = getWorkspaceUseCase(repo);
await expect(useCase({ id: "missing" })).rejects.toBeInstanceOf(
WorkspaceNotFoundError,
);
});
it("throws ZodError when repository returns malformed data", async () => {
const malformedRepo = {
getWorkspace: async () => ({ id: "", name: "x" }) as never,
};
const useCase = getWorkspaceUseCase(malformedRepo);
await expect(useCase({ id: "anything" })).rejects.toBeInstanceOf(ZodError);
});
});

View File

@@ -0,0 +1,30 @@
import { z } from "zod";
import { WorkspaceNotFoundError } from "../../entities/errors/workspace";
import { workspaceSchema } from "../../entities/models/workspace";
import type { IWorkspaceRepository } from "../repositories/workspace.repository.interface";
// ── Input ────────────────────────────────────────────────────────────────
export const getWorkspaceInputSchema = z
.object({
id: z.string().min(1),
})
.strict();
export type GetWorkspaceInput = z.infer<typeof getWorkspaceInputSchema>;
// ── Output ───────────────────────────────────────────────────────────────
export const getWorkspaceOutputSchema = workspaceSchema;
export type GetWorkspaceOutput = z.infer<typeof getWorkspaceOutputSchema>;
// ── Use case ─────────────────────────────────────────────────────────────
export type IGetWorkspaceUseCase = ReturnType<typeof getWorkspaceUseCase>;
export const getWorkspaceUseCase =
(workspaceRepository: IWorkspaceRepository) =>
async (input: GetWorkspaceInput): Promise<GetWorkspaceOutput> => {
const result = await workspaceRepository.getWorkspace(input.id);
if (!result) {
throw new WorkspaceNotFoundError(`Workspace not found: ${input.id}`);
}
return getWorkspaceOutputSchema.parse(result);
};