feat(boundaries): WP-03 — contracts purity + ApiClient confinement

Lint-enforce two architecture rules that were only documented (ADR-0001),
landing the rules with the fixes so the build stays green:

- contracts/ imports nothing: dashboard-view.dto.ts is now pure wire shapes
  (inline string-union enums, no domain imports). The DashboardView FE-view
  type moves to the adapter, which maps wire → domain (compiler-enforced seam).
- ApiClient lives only in infrastructure: change-request-form (UI) no longer
  injects ApiClient — a new ChangeRequestAdapter owns the client and the submit
  becomes a createSubmitChangeRequest() command factory (createDraftSync shape).
  draft-sync's wire-DTO import becomes type-only (allowed via allowTypeImports).
- Role type moves to shared/domain/role.ts; the ?role= reader stays in
  shared/infrastructure/role.ts.
- eslint: contracts import-ban + @typescript-eslint/no-restricted-imports on
  api-client (value-only; type imports permitted; infra + shared/upload exempt).

Also fixes a PRE-EXISTING bug found while verifying the flow: change-request-form
never imported FormsModule, so (ngSubmit) didn't bind and the submit button did a
native form submit (page reload) instead of submitting. Verified end-to-end in the
running app: submit → command → adapter → backend → reference, success alert shown.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
eho
2026-07-02 20:19:58 +02:00
co-authored by Claude Opus 4.8
parent be3a64f6cf
commit f9b76e7f6a
15 changed files with 2358 additions and 1979 deletions
@@ -1,20 +1,18 @@
import { inject } from '@angular/core';
import { Result } from '@shared/kernel/fp';
import { Valid } from '@registratie/domain/change-request.machine';
import { ApiClient } from '@shared/infrastructure/api-client';
import { runSubmit, SUBMIT_FAILED } from '@shared/application/submit';
import { ChangeRequestAdapter } from '@registratie/infrastructure/change-request.adapter';
/**
* Command: POST an address change to the backend (`/api/v1/change-requests`),
* which re-validates and returns a reference. Same shape as the other submit
* commands — a `Result`, never a thrown error — so the form's reduce can branch.
* Command factory: binds the change-request adapter (which owns the `ApiClient`)
* in an injection context and returns the submit function the form calls. Same
* field-initializer shape as `createStore`/`createDraftSync`, so the UI holds an
* application command — not the network client. Returns a `Result`, never a thrown
* error, so the form's reduce can branch on the outcome.
*/
export function submitChangeRequest(client: ApiClient, data: Valid): Promise<Result<string, string>> {
return runSubmit(async () => {
const res = await client.changeRequests({
straat: data.straat,
postcode: data.postcode,
woonplaats: data.woonplaats,
});
return res.referentie ?? '';
}, SUBMIT_FAILED);
export function createSubmitChangeRequest() {
const adapter = inject(ChangeRequestAdapter);
return (data: Valid): Promise<Result<string, string>> =>
runSubmit(() => adapter.changeRequest(data), SUBMIT_FAILED);
}