Onboard recipe-backend onto @recipe/contract #13

Merged
araemer merged 27 commits from onboard-recipe-platform-contract into stage 2026-09-26 05:14:07 +00:00
Owner

Implements docs/plans/onboarding-integration.md, including the base-mapper blocker fix requested after initial review.

What's in this PR

  1. Dependency + CI — adds @recipe/contract (file:../recipe-platform/api-contract) to package.json and a recipe-platform sibling-clone step to all three Forgejo workflows, per recipe-platform/docs/adr/2026-09-19-contract-ci-strategy.md.
  2. Full domain-by-domain audit — every one of the 13 contract domains diffed against this repo's live resources/DTOs. Written up in docs/audit-tags-and-domains.md.
  3. Tags wire-shape fix — GET /tag/all / GET /tag/enriched now return { tags: [...] } wrapper objects instead of bare arrays, per the list-wrapper convention. This is a breaking change and needs to land together with the recipe-app PR that unwraps the new response shape — please hold merging this until that PR is ready, or coordinate the merge order.
  4. Auth wire-format fix — LoginResponse.expiryDate / refresh's expiryDate now serialize as ISO-8601 strings instead of Date, matching the contract.
  5. Base-mapper date-typing blocker — found and fixed in this PR. AbstractDtoEntityMapper (extended by every concrete mapper) now serialises dates as ISO strings at the entity↔DTO boundary instead of raw Date objects, matching the contract's AbstractDto. This was the blocker preventing any domain beyond health from migrating; see docs/audit-tags-and-domains.md for the full writeup, including the follow-on optionality guards each write-direction mapper needed once the corresponding DTOs went optional per the contract's shared read/write shape.
  6. Full domain migration. Every domain except comments and images now imports its DTOs directly from @recipe/contract, with all local DTO files deleted: health, ratings, compact-recipes, users (incl. UserDto/UserRole), ai (incl. RecipeImportStatus/RecipeImportReason), meal-plans (incl. MealPlanPermission/MealPlanEffectivePermission), passkeys (incl. PasskeyDeviceType), auth, recipes, and tags.
  7. check-platform skill added, mirroring recipe-platform's check-siblings skill in reverse.
  8. CLAUDE.md updated with a Multi-Repo Context section reflecting the completed migration and a pointer to recipe-platform/docs/api-conventions.md instead of restated convention prose.

Explicitly NOT done here

  • comments domain left unmigrated — the audit found a real behavioural gap: the contract's createOrUpdate JSDoc says admins may edit any comment, but CommentHandler.updateComment has no admin bypass (unlike deleteComment, which does check UserRole.ADMIN). This needs a human product decision (fix the handler, or correct the contract), not a wire-format call — flagged in docs/audit-tags-and-domains.md, not fixed.
  • images domain — no local DTO exists under this repo for the GET /image/:key route; separately, the contract's path pattern for it is inaccurate (real storage keys are multi-segment) — that's a recipe-platform-side documentation fix, not something this PR can do.
  • The zod dependency in api-contract/package.json (declared, unused) — that's a recipe-platform-side fix.
  • Everything under the plan's "Out of scope" section (per-repo agent definitions).

npm run build and npm test (364 tests) both pass.

🤖 Generated with Claude Code

Implements `docs/plans/onboarding-integration.md`, including the base-mapper blocker fix requested after initial review. ## What's in this PR 1. **Dependency + CI** — adds `@recipe/contract` (`file:../recipe-platform/api-contract`) to `package.json` and a `recipe-platform` sibling-clone step to all three Forgejo workflows, per `recipe-platform/docs/adr/2026-09-19-contract-ci-strategy.md`. 2. **Full domain-by-domain audit** — every one of the 13 contract domains diffed against this repo's live resources/DTOs. Written up in `docs/audit-tags-and-domains.md`. 3. **Tags wire-shape fix** — `GET /tag/all` / `GET /tag/enriched` now return `{ tags: [...] }` wrapper objects instead of bare arrays, per the list-wrapper convention. **This is a breaking change and needs to land together with the `recipe-app` PR that unwraps the new response shape** — please hold merging this until that PR is ready, or coordinate the merge order. 4. **Auth wire-format fix** — `LoginResponse.expiryDate` / refresh's `expiryDate` now serialize as ISO-8601 strings instead of `Date`, matching the contract. 5. **Base-mapper date-typing blocker — found and fixed in this PR.** `AbstractDtoEntityMapper` (extended by every concrete mapper) now serialises dates as ISO strings at the entity↔DTO boundary instead of raw `Date` objects, matching the contract's `AbstractDto`. This was the blocker preventing any domain beyond `health` from migrating; see `docs/audit-tags-and-domains.md` for the full writeup, including the follow-on optionality guards each write-direction mapper needed once the corresponding DTOs went `optional` per the contract's shared read/write shape. 6. **Full domain migration.** Every domain except `comments` and `images` now imports its DTOs directly from `@recipe/contract`, with all local DTO files deleted: `health`, `ratings`, `compact-recipes`, `users` (incl. `UserDto`/`UserRole`), `ai` (incl. `RecipeImportStatus`/`RecipeImportReason`), `meal-plans` (incl. `MealPlanPermission`/`MealPlanEffectivePermission`), `passkeys` (incl. `PasskeyDeviceType`), `auth`, `recipes`, and `tags`. 7. **`check-platform` skill** added, mirroring `recipe-platform`'s `check-siblings` skill in reverse. 8. **`CLAUDE.md` updated** with a Multi-Repo Context section reflecting the completed migration and a pointer to `recipe-platform/docs/api-conventions.md` instead of restated convention prose. ## Explicitly NOT done here - **`comments` domain left unmigrated** — the audit found a real behavioural gap: the contract's `createOrUpdate` JSDoc says admins may edit any comment, but `CommentHandler.updateComment` has no admin bypass (unlike `deleteComment`, which does check `UserRole.ADMIN`). This needs a human product decision (fix the handler, or correct the contract), not a wire-format call — flagged in `docs/audit-tags-and-domains.md`, not fixed. - **`images` domain** — no local DTO exists under this repo for the `GET /image/:key` route; separately, the contract's path pattern for it is inaccurate (real storage keys are multi-segment) — that's a `recipe-platform`-side documentation fix, not something this PR can do. - The `zod` dependency in `api-contract/package.json` (declared, unused) — that's a `recipe-platform`-side fix. - Everything under the plan's "Out of scope" section (per-repo agent definitions). `npm run build` and `npm test` (364 tests) both pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Wires this repo onto the api-contract package via file: dependency and
adds a recipe-platform sibling clone step to all three Forgejo
workflows so npm ci can resolve it, per
recipe-platform/docs/adr/2026-09-19-contract-ci-strategy.md.
The plan was written to disk during the planning phase but never
committed. Tracking it here so the audit findings and future readers
have the original plan alongside its execution.
Diffs all 13 contract domains against this repo's live resources,
services, and DTOs. Confirms tags as the only known drift plus two new
findings: an auth Date-vs-string wire type mismatch and a comments
admin-edit permission gap that needs a human decision before that
domain can migrate.
While attempting step 5's DTO deletion+import migration, found that
AbstractDtoEntityMapper's generic constraint is bound to the local
Date-typed AbstractDto and structurally rejects the contract's
string-typed AbstractDto (verified with tsc --strict). This blocks
nearly every domain's response DTOs, not just one — flagging per the
task's stop-and-flag guidance rather than folding a base-mapper change
into this PR unannounced.
GET /tag/all and GET /tag/enriched returned bare arrays; the contract
requires TagListResponse/EnrichedTagListResponse wrapper objects per
docs/api-conventions.md's list-wrapper convention. This is a breaking
wire-format change and must land together with the recipe-app PR that
updates the two tag-fetching calls to unwrap { tags: [...] }.
HealthResponse has no entity DTO fields and no nested types, so it is
unaffected by the AbstractDto Date-vs-string base-mapper blocker (see
docs/audit-tags-and-domains.md) — safe to delete the local DTO and
import directly from the contract as a proof that the pattern works.
LoginResponse.expiryDate and the internal RefreshResult.expiryDate
were typed Date locally; the contract requires string per
docs/api-conventions.md's Dates convention. Converts with
toISOString() at the three call sites that populate these fields
(password login, refresh, and passkey login, which builds the same
LoginResponse shape). Also migrates LoginRequest and the new
RefreshResponse to @recipe/contract imports — both are flat DTOs with
no nested entity types, so neither is affected by the AbstractDto
base-mapper blocker.
Mirrors recipe-platform/.claude/skills/check-siblings/SKILL.md in
reverse: checks for ../recipe-platform and specifically for
api-contract inside it (the actual npm install dependency target),
asking explicit consent before cloning. On-demand only, not run
proactively at session start.
docs(claude) Document the multi-repo context and contract migration state
Some checks failed
/ test (pull_request) Has been cancelled
6fe9e5ddca
Adds a near-top "Multi-Repo Context" section naming the three sibling
repos, the @recipe/contract dependency, and exactly which domains are
migrated vs. still locally hand-rolled (pointing at
docs/audit-tags-and-domains.md for the full picture). Trims the
existing API-convention prose down to a pointer at
recipe-platform/docs/api-conventions.md instead of a parallel
restatement that can silently drift.
AbstractDtoEntityMapper.mapBaseEntityToDto/mapBaseDtoToEntity is the
base every concrete mapper extends. It previously assigned raw Date
objects to dto.createdAt/updatedAt, which only compiled because the
local AbstractDto typed those fields as Date. The contract's
AbstractDto types them as string (ISO-8601) per
docs/api-conventions.md, and this local/contract mismatch is exactly
what blocked every domain migration in docs/audit-tags-and-domains.md.

Chosen strategy: DTOs represent dates as string throughout, matching
the contract. mapBaseEntityToDto now converts with .toISOString() at
the entity->DTO boundary; mapBaseDtoToEntity parses back with
new Date(...) at the DTO->entity boundary. The same fix is applied to
the two DTOs that manually duplicate createdAt/updatedAt outside the
base mapper (EnrichedTagDto via EnrichedTagMapper, CompactMealPlanDto
via MealPlanApplicationService's raw-row projection).

Updates every mapper test asserting Date identity on createdAt/
updatedAt to compare against the ISO string instead (or, for the one
DTO->entity direction test in AbstractDtoEntityMapper.test.ts, to
toEqual() the parsed-back Date by value rather than toBe() by
reference). No other Date-arithmetic call site was found reading these
fields elsewhere in the codebase.
Now that AbstractDtoEntityMapper serializes dates as ISO strings,
TagDto and EnrichedTagDto structurally satisfy the contract's types.
Deletes the local DTO/response files and imports everything from
@recipe/contract instead. TagDtoEntityMapper/EnrichedTagMapper switch
from `new XDto()` construction (impossible with an imported interface)
to object literals. TagDtoEntityMapper.doMergeDtoIntoEntity now
explicitly validates that description is present (it's optional on
the contract's TagDto, since the same DTO covers partial reads, but
always required to persist a tag) rather than relying on a static
required-field type.

Also fixes the two cross-domain references to the local TagDto that
this pulled along: RecipeDto.tagList and
AiRecipeGenerationService.buildRecipeDto's tag construction.

Updates every affected test's TagDto/EnrichedTagDto/DeleteTagRequest
fixtures from `new X()` to object literals.
Deletes the local RatingDto and imports it from @recipe/contract.
RatingDtoEntityMapper's toEntity/doMergeDtoIntoEntity (dead code paths
in production — ratings are only ever created via
RatingApplicationService.rateRecipe() with primitive args, never by
mapping a full RatingDto back to an entity) now explicitly validate
that rating/recipeId are present, since those fields are optional on
the contract's DTO but required to persist a rating.
Deletes the local CompactRecipeDto, CompactRecipeFilterRequest,
CompactRecipeFilterResponse, and the RecipeSortField/SortOrder/
RecipeOwnerFilter TS enums, importing all six from @recipe/contract.
The local enums had zero TypeORM entity coupling (verified via grep)
so migrating them along with the DTOs that reference them was safe —
Record<RecipeSortField, string> and value accessors like
RecipeSortField.TITLE work identically against the contract's
as const objects. Updates CompactRecipeDtoEntityMapper,
CompactRecipeApplicationService, CompactRecipeRestResource, the
repository-layer CompactRecipeFilter type, and RecipeRepository's sort
expression map accordingly, plus every affected test's `new X()`
fixtures to object literals.
users/shared:
- Deletes local UserDto, CreateUserRequest, CreateUserResponse,
  ChangeUserPasswordRequest, UserListResponse — all imported from
  @recipe/contract now.
- Deletes the local UserRole TS enum; UserEntity's
  @Column({ type: "enum", enum: UserRole }) now references the
  contract's as const object directly (TypeORM only reads
  Object.values() off it, so this is a drop-in swap). UserRoleHelper
  (display names, dropdown options — no contract equivalent) stays
  local in src/api/enums/UserRole.ts, now built on the contract's type.
- UserDtoEntityMapper.doMergeDtoIntoEntity explicitly validates
  userName/email presence, since both are optional on the contract's
  UserDto (same shape covers partial reads) but required to persist a
  user.
- Updates every other file that referenced the local UserRole/UserDto
  (AuthPayload, CommentDto, MealPlanShareDto, handlers, middleware,
  services) to import from @recipe/contract instead.

auth:
- Fully migrates LoginRequest/LoginResponse/RefreshResponse to
  @recipe/contract (LoginResponse was previously just fixed in place;
  now imported directly since it has no remaining local-type coupling).

passkeys:
- Deletes local PasskeyDto, PasskeyListResponse,
  PasskeyAuthBeginResponse, PasskeyRegisterBeginResponse,
  PasskeyAuthCompleteRequest, PasskeyRegisterCompleteRequest,
  PasskeyRenameRequest, and the PasskeyDeviceType TS enum (zero
  TypeORM coupling — UserPasskeyEntity.deviceType is a plain string
  column).
- PasskeyApplicationService now casts the contract's `unknown`-typed
  WebAuthn response/options fields to the concrete
  @simplewebauthn/server types PasskeyHandler expects, per the
  contract's own documented rationale for typing them loosely.

Converts every `new XDto()`/`new XRequest()` construction site touched
by this change (production and test) to object literals, since none
of these types can be `new`'d once imported as contract interfaces.
Deletes local AiImportStatsRequest, AiImportStatsResponse,
AiImportSummaryDto, AiImportLogEntryDto — all imported from
@recipe/contract now; none extend AbstractDto and their required-ness
already matched the contract field-for-field, so this needed no
optionality guards.

Also migrates RecipeImportStatus and RecipeImportReason (used by both
the ai and recipes domains) from local TS enums to @recipe/contract's
as const objects — confirmed zero TypeORM coupling
(AiRecipeImportLogEntity stores both as plain varchar columns, not
TypeORM enum columns), so this was a safe mechanical swap across
AiRecipeGenerationService, RecipeImportResponse, and the entity.
Deletes MealPlanDto, MealPlanDayDto, MealPlanDayDishDto,
MealPlanShareDto, MealPlanFilterRequest, MealPlanListResponse,
CompactMealPlanDto, and the MealPlanPermission/
MealPlanEffectivePermission TS enums — all imported from
@recipe/contract now.

MealPlanShareEntity's @Column({ type: "enum", enum: MealPlanPermission
}) now references the contract's as const object directly (same
drop-in swap pattern as UserRole).

Every write-direction mapper (MealPlanDtoEntityMapper,
MealPlanDayDtoEntityMapper, MealPlanDayDishDtoEntityMapper,
MealPlanShareDtoEntityMapper) explicitly validates the fields that are
optional on the contract's DTOs (same shape covers detail reads) but
required to actually persist a plan/day/dish/share: title, date,
recipeId/servings/sortOrder, and sharedWithUserId/permission
respectively — each throws ValidationError if missing, consistent with
the pattern established in the tags/ratings/users migrations.
Deletes RecipeDto, RecipeImageDto, RecipeIngredientDto,
RecipeIngredientGroupDto, RecipeInstructionStepDto, RecipeRatingRequest,
and RecipeImportResponse — all imported from @recipe/contract now.
RecipeRatingRequest needed no optionality guard (rating was already
required in both places); every other write-direction mapper
(RecipeDtoEntityMapper, RecipeIngredientDtoEntityMapper,
RecipeIngredientGroupDtoEntityMapper, RecipeInstructionStepDtoEntityMapper)
explicitly validates the fields that are optional on the contract's
DTOs (the same shape covers detail reads) but required to persist a
recipe/group/ingredient/step — title, name, text, sortOrder — each
throwing ValidationError if missing, consistent with every other
domain migrated in this branch. RecipeImageDtoEntityMapper needed no
guards: toEntity/doMergeDtoIntoEntity are already unconditional no-ops
(images are write-once, created only via RecipeImageHandler).

AnthropicClient.ts's ClaudeRecipeOutput type (derived from the DTOs via
Pick, sent to Claude as RECIPE_TOOL.input_schema) is adjusted to keep
wrapping the fields the JSON schema's own `required` arrays mark
mandatory in Required<Pick<...>> — preserving the exact same
compile-time guarantees as before the DTOs' fields became optional at
the contract level.

Converts every `new XDto()` construction site this pulled in (recipe
generation, mapper tests, service tests) to object literals.
docs(audit) Update audit doc and CLAUDE.md to reflect completed migration
Some checks failed
/ test (pull_request) Has been cancelled
7eba2ccf72
Rewrites docs/audit-tags-and-domains.md to record the base-mapper
blocker as resolved in this PR (not deferred) and lists every domain
now actually migrated to @recipe/contract, per the human's explicit
instruction to fix it in this same branch rather than split into a
follow-up PR. Updates CLAUDE.md's Multi-Repo Context section to match:
every domain except comments (blocked on a product decision) and
images (no local DTO to migrate) now imports its DTOs from the
contract.
docs(plans) Merge audit results into the onboarding plan
Some checks failed
/ test (pull_request) Failing after 2s
befa9a4fe4
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
docs(plans) Update status line to reflect implementation is done
Some checks failed
/ test (pull_request) Failing after 2s
4544ccc7dd
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CommentHandler.updateComment only checked comment ownership, so an admin
clicking "edit" on someone else's comment (the frontend already renders
that button for admins) got a 403. deleteComment already allowed the
admin bypass, and the contract's JSDoc documents update as admin-or-creator
too — only the handler was out of step. Threads the full AuthPayload
(id + role) through CommentApplicationService and the create-or-update
route instead of a bare user id, so the handler can check role.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
fix(ci) Clone recipe-platform over HTTPS, not SSH
Some checks failed
/ test (pull_request) Failing after 4s
10be94b45e
The self-hosted runners have no SSH key configured, so the pull_platform
step hung every pipeline. Use the same authenticated HTTPS mechanism the
existing pull step already uses to check out this repo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
fix(ci) Install api-contract's own deps in CI; declare them as direct deps here
All checks were successful
/ test (pull_request) Successful in 44s
d78a34059a
npm ci failed with EUSAGE: @ts-rest/core and zod are dependencies of
@recipe/contract but were never declared here, so this repo's own
package-lock.json had no resolved entries for them.

That alone isn't enough — npm creates a symlink for file: dependencies, so
tsc resolves api-contract's own "@ts-rest/core" import from api-contract's
real location, never through this repo's node_modules. CI never ran
"npm install" inside api-contract (ONBOARDING.md's step 3 is a manual-only
instruction), so that import failed regardless of this repo's lockfile.

Fixes both: adds @ts-rest/core/zod as direct dependencies here, and adds
an install_platform_contract CI step that runs npm install inside
recipe-platform/api-contract after cloning it, in all three workflows.

See recipe-platform docs/adr/2026-09-19-contract-ci-install-step.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CLAUDE.md Outdated
@ -5,0 +12,4 @@
| `recipe-backend` (this repo) | REST API server |
| `recipe-app` | React SPA frontend |
`@recipe/contract` (installed via `file:../recipe-platform/api-contract`) is the source
Author
Owner

this is lots of historic notes instead of the current state. is that necessary? It seems like it's filling the context unnecessarily.

this is lots of historic notes instead of the current state. is that necessary? It seems like it's filling the context unnecessarily.
araemer marked this conversation as resolved
docs(claude) Update stale comments-permission and audit-doc references
All checks were successful
/ test (pull_request) Successful in 44s
23a2b2cf91
CLAUDE.md still said "only creator may edit/delete" for comments and
framed the admin-edit gap as "pending a product decision" — both stale
since the fix landed (admins may edit and delete, matching the product
decision). Also fixed two dead links to the deleted
docs/audit-tags-and-domains.md, now merged into docs/plans/onboarding-integration.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CLAUDE.md Outdated
@ -5,0 +26,4 @@
`docs/audit-tags-and-domains.md` for the full domain-by-domain audit, including the
`AbstractDtoEntityMapper` date-typing fix every migrated mapper depends on.
Cross-cutting API conventions (query/filter endpoints use POST, named-verb mutations,
Author
Owner

This is quite a lot of text for a simple "There's a strict rule on API conventions we MUST follow in ...

This is quite a lot of text for a simple "There's a strict rule on API conventions we MUST follow in ...
araemer marked this conversation as resolved
CLAUDE.md Outdated
@ -5,0 +32,4 @@
migrated mapper depends on.
Cross-cutting API conventions (query/filter endpoints use POST, named-verb mutations,
list-response wrappers, UUID/ISO-date wire formats, the `{ error }` envelope, optimistic
Author
Owner

Again: What is that doing here? Why does this repo need to know about the parent repo's agents? Naively, I'd say it only needs to know how to check that the platform is there.

Again: What is that doing here? Why does this repo need to know about the parent repo's agents? Naively, I'd say it only needs to know how to check that the platform is there.
araemer marked this conversation as resolved
CLAUDE.md Outdated
Author
Owner

Here you refer to the same conventions document as above. Once should suffice.

Here you refer to the same conventions document as above. Once should suffice.
Nothing was actually blocking this — the earlier fix only scoped in the
admin-edit-permission bug, not the DTO migration itself. CommentDto is
structurally identical to the contract's version (AbstractDto is already
string-dated); the only gap was optionality (body/recipeId required
locally, optional on the contract), handled the same way every other
migrated domain handles it: validate on write.

CommentDtoEntityMapper.toEntity/doMergeDtoIntoEntity now validate body/
recipeId presence before assigning to the entity. CommentApplicationService
validates body before calling updateComment. Local CommentDto.ts deleted;
all three files plus both test files import from @recipe/contract instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@ -12,3 +2,3 @@
/**
* Helper functions for UserRole
* Helper functions for UserRole. Backend-only display/validation concerns
Author
Owner

Does this file still carry the correct name? We somehow reduced it to an API util - the UserRoleHelper.

Does this file still carry the correct name? We somehow reduced it to an API util - the UserRoleHelper.
araemer marked this conversation as resolved
@ -24,7 +23,8 @@ export interface LoginResult {
*/
export interface RefreshResult {
Author
Owner

The interfaces here are used by the frontend, right? Why are they local to the backend and not part of the contract?

The interfaces here are used by the frontend, right? Why are they local to the backend and not part of the contract?
@ -58,0 +59,4 @@
// recipeId/servings/sortOrder are optional on the contract's
// MealPlanDayDishDto (the same shape covers detail reads), but all are
// always required to persist a dish.
if (!dto.recipeId) throw new ValidationError("recipeId is required");
Author
Owner

We do that twice here. Would a validation method be better?

We do that twice here. Would a validation method be better?
araemer marked this conversation as resolved
@ -18,3 +22,3 @@
this.mapBaseDtoToEntity(dto, entity);
if (!dto.text) throw new ValidationError("text is required");
Author
Owner

Again, validation in two places. Validation method?

Again, validation in two places. Validation method?
araemer marked this conversation as resolved
@ -0,0 +269,4 @@
1. ~~`comments` admin-edit permission~~ — **resolved**: fixed to match the contract and the
frontend's existing assumption (admins can edit any comment, same as delete). See above.
2. **`api-contract/src/images.ts`'s `GET /image/:key` path pattern** doesn't reflect the
Author
Owner

Are 2 and 3 still open for follow-up?

Are 2 and 3 still open for follow-up?
araemer marked this conversation as resolved
Author
Owner

Re: "Are 2 and 3 still open for follow-up?" (on docs/plans/onboarding-integration.md) — yes, both still open. Tracked as issues in recipe-platform now, so they do not get lost:

Re: "Are 2 and 3 still open for follow-up?" (on docs/plans/onboarding-integration.md) — yes, both still open. Tracked as issues in recipe-platform now, so they do not get lost: - #2 (images.ts path pattern) → https://git.raemer.net/araemer/recipe-platform/issues/6 - #3 (unused zod dependency) → https://git.raemer.net/araemer/recipe-platform/issues/7
refactor Address PR review feedback
All checks were successful
/ test (pull_request) Successful in 44s
44088fa30a
- CLAUDE.md: trim Multi-Repo Context to current state instead of migration
  history (history lives in docs/plans/onboarding-integration.md); cut the
  cross-repo workflow paragraph down to just check-platform, since this
  repo's session never runs /plan-feature etc.; remove the duplicate
  api-conventions.md pointer (kept one, in Multi-Repo Context).
- AuthHandler.ts: add comments explaining why LoginResult/RefreshResult
  aren't in @recipe/contract — sessionCookie has no JSON representation
  (it becomes a Set-Cookie header), so half of each interface structurally
  cannot live in a JSON-body-typed contract.
- UserRole.ts -> utils/UserRoleHelper.ts: the file only exports
  UserRoleHelper now (UserRole itself moved to the contract) — renamed and
  relocated to match, since it's no longer an enum.
- Dedupe validation across toEntity/doMergeDtoIntoEntity in six mappers
  (CommentDtoEntityMapper, RecipeIngredientDtoEntityMapper,
  RecipeInstructionStepDtoEntityMapper, MealPlanShareDtoEntityMapper,
  RatingDtoEntityMapper, MealPlanDayDishDtoEntityMapper) via a shared
  private validation method per mapper. Deliberately NOT full delegation
  (toEntity calling the base class's mergeDtoIntoEntity) — caught this
  breaking a real test first: mergeDtoIntoEntity never calls
  mapBaseDtoToEntity, so nested toEntity calls (e.g. recipe instruction
  steps mapped inside RecipeDtoEntityMapper.toEntity) would silently drop
  a DTO's existing id. Kept mapBaseDtoToEntity explicit in toEntity in all
  six, extracted only the validation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
fix(mappers) Use type assertion for incrementally-built DTOs
All checks were successful
/ test (pull_request) Successful in 44s
f80d53be37
Mappers build a DTO by starting from an empty object literal and
assigning every field unconditionally before returning it. This
compiled fine while the fields were optional, but recipe-platform
PR #11 (fix-contract-mandatory-fields) correctly marks 20 previously
mis-typed fields as mandatory, which makes the initial empty object
literal fail the type check even though the object is fully populated
by the time it's returned.

Switch `const dto: XDto = {}` to `const dto = {} as XDto` in the 11
affected mappers — the standard idiom for a value that is genuinely
built up field-by-field and guaranteed complete before use. No runtime
behavior changes.

Required for recipe-platform PR #11 to be safe to merge.
araemer merged commit c9d70fddc1 into stage 2026-09-26 05:14:07 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
araemer/recipe-backend!13
No description provided.