Compare commits
5 Commits
todo/mvp0-
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
36b09cd9d7
|
|||
| 70fc154f97 | |||
| c4d0499d12 | |||
| d16fb6e121 | |||
| a508e3203a |
6
TODO.md
6
TODO.md
@ -32,9 +32,9 @@ This file is the single source of truth for roadmap and delivery progress.
|
||||
- [x] [P1] First-start onboarding route for initial owner creation (`/welcome`)
|
||||
- [x] [P1] Split auth entry points (`/welcome`, `/login`, `/register`) with cross-links
|
||||
- [x] [P2] Support fallback sign-in route (`/support/:key`) as break-glass access
|
||||
- [~] [P1] Reusable CRUD base patterns (list/detail/editor/service/repository)
|
||||
- [~] [P1] Shared CRUD validation strategy (Zod + server-side enforcement)
|
||||
- [~] [P1] Shared error and audit hooks for CRUD mutations
|
||||
- [x] [P1] Reusable CRUD base patterns (list/detail/editor/service/repository)
|
||||
- [x] [P1] Shared CRUD validation strategy (Zod + server-side enforcement)
|
||||
- [x] [P1] Shared error and audit hooks for CRUD mutations
|
||||
|
||||
### Admin App
|
||||
|
||||
|
||||
@ -7,6 +7,8 @@ MVP0 now includes a shared CRUD foundation package: `@cms/crud`.
|
||||
Current baseline:
|
||||
|
||||
- Shared service factory: `createCrudService`
|
||||
- Repository contract: `list`, `findById`, `create`, `update`, `delete`
|
||||
- Service surface for list/detail/editor flows: `list`, `getById`, `create`, `update`, `delete`
|
||||
- Shared validation error type: `CrudValidationError`
|
||||
- Shared not-found error type: `CrudNotFoundError`
|
||||
- Shared mutation audit hook contract: `CrudAuditHook`
|
||||
@ -24,6 +26,11 @@ Current baseline:
|
||||
- `registerPostCrudAuditHook`
|
||||
|
||||
Validation for create/update is enforced by `@cms/content` schemas.
|
||||
Contract tests validate:
|
||||
|
||||
- repository list/detail behavior via CRUD service
|
||||
- validation and not-found errors
|
||||
- audit payload propagation (`actor`, `metadata`)
|
||||
|
||||
The admin dashboard currently includes a temporary posts CRUD sandbox to validate this flow through a real app UI.
|
||||
|
||||
|
||||
@ -63,6 +63,32 @@ function createMemoryRepository() {
|
||||
}
|
||||
|
||||
describe("createCrudService", () => {
|
||||
it("supports list and detail lookups through the repository contract", async () => {
|
||||
const service = createCrudService({
|
||||
resource: "fake-entity",
|
||||
repository: createMemoryRepository(),
|
||||
schemas: {
|
||||
create: z.object({
|
||||
title: z.string().min(3),
|
||||
}),
|
||||
update: z.object({
|
||||
title: z.string().min(3).optional(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
const createdA = await service.create({ title: "First" })
|
||||
const createdB = await service.create({ title: "Second" })
|
||||
|
||||
expect(await service.getById(createdA.id)).toEqual(createdA)
|
||||
expect(await service.getById("missing")).toBeNull()
|
||||
|
||||
const listed = await service.list()
|
||||
expect(listed).toHaveLength(2)
|
||||
expect(listed).toContainEqual(createdA)
|
||||
expect(listed).toContainEqual(createdB)
|
||||
})
|
||||
|
||||
it("validates create and update payloads", async () => {
|
||||
const service = createCrudService({
|
||||
resource: "fake-entity",
|
||||
@ -106,8 +132,13 @@ describe("createCrudService", () => {
|
||||
})
|
||||
|
||||
it("emits audit events for create, update and delete", async () => {
|
||||
const events: Array<{ action: string; beforeTitle: string | null; afterTitle: string | null }> =
|
||||
[]
|
||||
const events: Array<{
|
||||
action: string
|
||||
beforeTitle: string | null
|
||||
afterTitle: string | null
|
||||
actorRole: string | null
|
||||
requestId: string | null
|
||||
}> = []
|
||||
const service = createCrudService({
|
||||
resource: "fake-entity",
|
||||
repository: createMemoryRepository(),
|
||||
@ -125,6 +156,9 @@ describe("createCrudService", () => {
|
||||
action: event.action,
|
||||
beforeTitle: event.before?.title ?? null,
|
||||
afterTitle: event.after?.title ?? null,
|
||||
actorRole: event.actor?.role ?? null,
|
||||
requestId:
|
||||
typeof event.metadata?.requestId === "string" ? event.metadata.requestId : null,
|
||||
})
|
||||
},
|
||||
],
|
||||
@ -134,6 +168,9 @@ describe("createCrudService", () => {
|
||||
{ title: "Created" },
|
||||
{
|
||||
actor: { id: "u-1", role: "owner" },
|
||||
metadata: {
|
||||
requestId: "req-1",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@ -145,16 +182,22 @@ describe("createCrudService", () => {
|
||||
action: "create",
|
||||
beforeTitle: null,
|
||||
afterTitle: "Created",
|
||||
actorRole: "owner",
|
||||
requestId: "req-1",
|
||||
},
|
||||
{
|
||||
action: "update",
|
||||
beforeTitle: "Created",
|
||||
afterTitle: "Updated",
|
||||
actorRole: null,
|
||||
requestId: null,
|
||||
},
|
||||
{
|
||||
action: "delete",
|
||||
beforeTitle: "Updated",
|
||||
afterTitle: null,
|
||||
actorRole: null,
|
||||
requestId: null,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user