test(api-client): generated BFF client is exposed and calls the endpoints (refs #66)
Scaffold libs/api-client (Nx Angular lib) and generate a typed HttpClient client from services/bff/openapi.json with orval (node-based; Angular target integrates with HttpClient interceptors for the S-08c auth token). A failing test drives the public API: it expects an injectable BffApiV1Service to POST /self-service/registrations and GET /openbaar/register (via HttpClientTesting), but the lib barrel doesn't export the client yet, so it fails. Normalise the vitest target to 'test'. Green exposes it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
7
libs/api-client/README.md
Normal file
7
libs/api-client/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# api-client
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `nx test api-client` to execute the unit tests.
|
||||
34
libs/api-client/eslint.config.mjs
Normal file
34
libs/api-client/eslint.config.mjs
Normal file
@@ -0,0 +1,34 @@
|
||||
import nx from '@nx/eslint-plugin';
|
||||
import baseConfig from '../../eslint.config.mjs';
|
||||
|
||||
export default [
|
||||
...nx.configs['flat/angular'],
|
||||
...nx.configs['flat/angular-template'],
|
||||
...baseConfig,
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
rules: {
|
||||
'@angular-eslint/directive-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'attribute',
|
||||
prefix: 'lib',
|
||||
style: 'camelCase',
|
||||
},
|
||||
],
|
||||
'@angular-eslint/component-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'element',
|
||||
prefix: 'lib',
|
||||
style: 'kebab-case',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.html'],
|
||||
// Override or add rules here
|
||||
rules: {},
|
||||
},
|
||||
];
|
||||
17
libs/api-client/orval.config.ts
Normal file
17
libs/api-client/orval.config.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { defineConfig } from 'orval';
|
||||
|
||||
// Generates the BFF client (Angular HttpClient service + models) from the committed
|
||||
// OpenAPI contract. Never hand-edit the generated output — re-run `nx run api-client:generate`
|
||||
// after the BFF spec changes (CLAUDE.md §10; docs/frontend-decisions.md).
|
||||
export default defineConfig({
|
||||
bff: {
|
||||
input: '../../services/bff/openapi.json',
|
||||
output: {
|
||||
target: './src/lib/generated/bff-api.ts',
|
||||
client: 'angular',
|
||||
mode: 'single',
|
||||
clean: true,
|
||||
prettier: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
13
libs/api-client/project.json
Normal file
13
libs/api-client/project.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "api-client",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/api-client/src",
|
||||
"prefix": "lib",
|
||||
"projectType": "library",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint"
|
||||
}
|
||||
}
|
||||
}
|
||||
1
libs/api-client/src/index.ts
Normal file
1
libs/api-client/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
50
libs/api-client/src/lib/bff-api.spec.ts
Normal file
50
libs/api-client/src/lib/bff-api.spec.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import {
|
||||
HttpTestingController,
|
||||
provideHttpClientTesting,
|
||||
} from '@angular/common/http/testing';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import {
|
||||
BffApiV1Service,
|
||||
type OpenbaarEntry,
|
||||
type SubmitAccepted,
|
||||
} from 'api-client';
|
||||
|
||||
describe('BffApiV1Service (generated from services/bff/openapi.json)', () => {
|
||||
let service: BffApiV1Service;
|
||||
let http: HttpTestingController;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideHttpClient(), provideHttpClientTesting()],
|
||||
});
|
||||
service = TestBed.inject(BffApiV1Service);
|
||||
http = TestBed.inject(HttpTestingController);
|
||||
});
|
||||
|
||||
afterEach(() => http.verify());
|
||||
|
||||
it('submits a registration via POST /self-service/registrations', () => {
|
||||
let result: SubmitAccepted | undefined;
|
||||
service.postSelfServiceRegistrations().subscribe((r) => (result = r));
|
||||
|
||||
const req = http.expectOne('/self-service/registrations');
|
||||
expect(req.request.method).toBe('POST');
|
||||
req.flush({ registrationId: 'reg-1', status: 'Ingediend' });
|
||||
|
||||
expect(result?.registrationId).toBe('reg-1');
|
||||
expect(result?.status).toBe('Ingediend');
|
||||
});
|
||||
|
||||
it('reads the openbaar register via GET /openbaar/register with the query', () => {
|
||||
let rows: OpenbaarEntry[] | undefined;
|
||||
service.getOpenbaarRegister({ q: 'abc' }).subscribe((r) => (rows = r));
|
||||
|
||||
const req = http.expectOne((r) => r.url === '/openbaar/register');
|
||||
expect(req.request.method).toBe('GET');
|
||||
expect(req.request.params.get('q')).toBe('abc');
|
||||
req.flush([{ id: 'abc-111', status: 'INGEDIEND' }]);
|
||||
|
||||
expect(rows?.[0].id).toBe('abc-111');
|
||||
});
|
||||
});
|
||||
216
libs/api-client/src/lib/generated/bff-api.ts
Normal file
216
libs/api-client/src/lib/generated/bff-api.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
/**
|
||||
* Generated by orval v8.19.0 🍺
|
||||
* Do not edit manually.
|
||||
* Bff.Api | v1
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
import {
|
||||
HttpClient,
|
||||
HttpHeaders,
|
||||
HttpResponse as AngularHttpResponse
|
||||
} from '@angular/common/http';
|
||||
import type {
|
||||
HttpContext,
|
||||
HttpEvent,
|
||||
HttpParams
|
||||
} from '@angular/common/http';
|
||||
|
||||
import {
|
||||
Injectable,
|
||||
inject
|
||||
} from '@angular/core';
|
||||
|
||||
import {
|
||||
Observable
|
||||
} from 'rxjs';
|
||||
|
||||
export interface OpenbaarEntry {
|
||||
id: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface SubmitAccepted {
|
||||
registrationId: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export type GetOpenbaarRegisterParams = {
|
||||
q?: string;
|
||||
};
|
||||
|
||||
interface HttpClientOptions {
|
||||
readonly headers?: HttpHeaders | Record<string, string | string[]>;
|
||||
readonly context?: HttpContext;
|
||||
readonly params?:
|
||||
| HttpParams
|
||||
| Record<string, string | number | boolean | Array<string | number | boolean>>;
|
||||
readonly reportProgress?: boolean;
|
||||
readonly withCredentials?: boolean;
|
||||
readonly credentials?: RequestCredentials;
|
||||
readonly keepalive?: boolean;
|
||||
readonly priority?: RequestPriority;
|
||||
readonly cache?: RequestCache;
|
||||
readonly mode?: RequestMode;
|
||||
readonly redirect?: RequestRedirect;
|
||||
readonly referrer?: string;
|
||||
readonly integrity?: string;
|
||||
readonly referrerPolicy?: ReferrerPolicy;
|
||||
readonly transferCache?: {includeHeaders?: string[]} | boolean;
|
||||
readonly timeout?: number;
|
||||
}
|
||||
|
||||
type HttpClientBodyOptions = HttpClientOptions & {
|
||||
readonly observe?: 'body';
|
||||
};
|
||||
|
||||
type HttpClientEventOptions = HttpClientOptions & {
|
||||
readonly observe: 'events';
|
||||
};
|
||||
|
||||
type HttpClientResponseOptions = HttpClientOptions & {
|
||||
readonly observe: 'response';
|
||||
};
|
||||
|
||||
type HttpClientObserveOptions = HttpClientOptions & {
|
||||
readonly observe?: 'body' | 'events' | 'response';
|
||||
};
|
||||
|
||||
type AngularHttpParamValue = string | number | boolean | Array<string | number | boolean>;
|
||||
type AngularHttpParamValueWithNullable = AngularHttpParamValue | null;
|
||||
|
||||
function filterParams(
|
||||
params: Record<string, unknown>,
|
||||
requiredNullableKeys?: ReadonlySet<string>,
|
||||
preserveRequiredNullables?: false,
|
||||
passthroughKeys?: undefined,
|
||||
): Record<string, AngularHttpParamValue>;
|
||||
function filterParams(
|
||||
params: Record<string, unknown>,
|
||||
requiredNullableKeys: ReadonlySet<string> | undefined,
|
||||
preserveRequiredNullables: true,
|
||||
passthroughKeys?: undefined,
|
||||
): Record<string, AngularHttpParamValueWithNullable>;
|
||||
function filterParams(
|
||||
params: Record<string, unknown>,
|
||||
requiredNullableKeys: ReadonlySet<string> | undefined,
|
||||
preserveRequiredNullables: boolean | undefined,
|
||||
passthroughKeys: ReadonlySet<string>,
|
||||
): Record<string, unknown>;
|
||||
function filterParams(
|
||||
params: Record<string, unknown>,
|
||||
requiredNullableKeys: ReadonlySet<string> = new Set(),
|
||||
preserveRequiredNullables = false,
|
||||
passthroughKeys: ReadonlySet<string> = new Set(),
|
||||
): Record<string, unknown> {
|
||||
const filteredParams: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (passthroughKeys.has(key)) {
|
||||
if (value !== undefined) {
|
||||
filteredParams[key] = value;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
const filtered = value.filter(
|
||||
(item) =>
|
||||
item != null &&
|
||||
(typeof item === 'string' ||
|
||||
typeof item === 'number' ||
|
||||
typeof item === 'boolean'),
|
||||
) as Array<string | number | boolean>;
|
||||
if (filtered.length) {
|
||||
filteredParams[key] = filtered;
|
||||
}
|
||||
} else if (
|
||||
preserveRequiredNullables &&
|
||||
value === null &&
|
||||
requiredNullableKeys.has(key)
|
||||
) {
|
||||
filteredParams[key] = null;
|
||||
} else if (
|
||||
value != null &&
|
||||
(typeof value === 'string' ||
|
||||
typeof value === 'number' ||
|
||||
typeof value === 'boolean')
|
||||
) {
|
||||
filteredParams[key] = value;
|
||||
}
|
||||
}
|
||||
return filteredParams;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class BffApiV1Service {
|
||||
private readonly http = inject(HttpClient);
|
||||
postSelfServiceRegistrations<TData = SubmitAccepted>( options?: HttpClientBodyOptions): Observable<TData>;
|
||||
postSelfServiceRegistrations<TData = SubmitAccepted>( options?: HttpClientEventOptions): Observable<HttpEvent<TData>>;
|
||||
postSelfServiceRegistrations<TData = SubmitAccepted>( options?: HttpClientResponseOptions): Observable<AngularHttpResponse<TData>>;
|
||||
postSelfServiceRegistrations<TData = SubmitAccepted>(
|
||||
options?: HttpClientObserveOptions): Observable<TData | HttpEvent<TData> | AngularHttpResponse<TData>> {
|
||||
if (options?.observe === 'events') {
|
||||
return this.http.post<TData>(
|
||||
`/self-service/registrations`,
|
||||
undefined,{
|
||||
...(options as Omit<NonNullable<typeof options>, 'observe'>),
|
||||
observe: 'events',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (options?.observe === 'response') {
|
||||
return this.http.post<TData>(
|
||||
`/self-service/registrations`,
|
||||
undefined,{
|
||||
...(options as Omit<NonNullable<typeof options>, 'observe'>),
|
||||
observe: 'response',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return this.http.post<TData>(
|
||||
`/self-service/registrations`,
|
||||
undefined,{
|
||||
...(options as Omit<NonNullable<typeof options>, 'observe'>),
|
||||
observe: 'body',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
getOpenbaarRegister<TData = OpenbaarEntry[]>(params?: GetOpenbaarRegisterParams, options?: HttpClientBodyOptions): Observable<TData>;
|
||||
getOpenbaarRegister<TData = OpenbaarEntry[]>(params?: GetOpenbaarRegisterParams, options?: HttpClientEventOptions): Observable<HttpEvent<TData>>;
|
||||
getOpenbaarRegister<TData = OpenbaarEntry[]>(params?: GetOpenbaarRegisterParams, options?: HttpClientResponseOptions): Observable<AngularHttpResponse<TData>>;
|
||||
getOpenbaarRegister<TData = OpenbaarEntry[]>(
|
||||
params?: GetOpenbaarRegisterParams, options?: HttpClientObserveOptions): Observable<TData | HttpEvent<TData> | AngularHttpResponse<TData>> {
|
||||
const filteredParams = filterParams({...params, ...options?.params}, new Set<string>([]));
|
||||
|
||||
if (options?.observe === 'events') {
|
||||
return this.http.get<TData>(
|
||||
`/openbaar/register`,{
|
||||
...(options as Omit<NonNullable<typeof options>, 'observe'>),
|
||||
observe: 'events',
|
||||
params: filteredParams,}
|
||||
);
|
||||
}
|
||||
|
||||
if (options?.observe === 'response') {
|
||||
return this.http.get<TData>(
|
||||
`/openbaar/register`,{
|
||||
...(options as Omit<NonNullable<typeof options>, 'observe'>),
|
||||
observe: 'response',
|
||||
params: filteredParams,}
|
||||
);
|
||||
}
|
||||
|
||||
return this.http.get<TData>(
|
||||
`/openbaar/register`,{
|
||||
...(options as Omit<NonNullable<typeof options>, 'observe'>),
|
||||
observe: 'body',
|
||||
params: filteredParams,}
|
||||
);
|
||||
}
|
||||
|
||||
};
|
||||
5
libs/api-client/src/test-setup.ts
Normal file
5
libs/api-client/src/test-setup.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import '@angular/compiler';
|
||||
import '@analogjs/vitest-angular/setup-snapshots';
|
||||
import { setupTestBed } from '@analogjs/vitest-angular/setup-testbed';
|
||||
|
||||
setupTestBed({ zoneless: false });
|
||||
31
libs/api-client/tsconfig.json
Normal file
31
libs/api-client/tsconfig.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"isolatedModules": true,
|
||||
"target": "es2022",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"emitDecoratorMetadata": false,
|
||||
"module": "preserve"
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
26
libs/api-client/tsconfig.lib.json
Normal file
26
libs/api-client/tsconfig.lib.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"inlineSources": true,
|
||||
"types": []
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": [
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.ts",
|
||||
"vite.config.ts",
|
||||
"vite.config.mts",
|
||||
"vitest.config.ts",
|
||||
"vitest.config.mts",
|
||||
"src/**/*.test.tsx",
|
||||
"src/**/*.spec.tsx",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.test.jsx",
|
||||
"src/**/*.spec.jsx",
|
||||
"src/test-setup.ts"
|
||||
]
|
||||
}
|
||||
29
libs/api-client/tsconfig.spec.json
Normal file
29
libs/api-client/tsconfig.spec.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"types": [
|
||||
"vitest/globals",
|
||||
"vitest/importMeta",
|
||||
"vite/client",
|
||||
"node",
|
||||
"vitest"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts",
|
||||
"vite.config.mts",
|
||||
"vitest.config.ts",
|
||||
"vitest.config.mts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.tsx",
|
||||
"src/**/*.spec.tsx",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.test.jsx",
|
||||
"src/**/*.spec.jsx",
|
||||
"src/**/*.d.ts"
|
||||
],
|
||||
"files": ["src/test-setup.ts"]
|
||||
}
|
||||
28
libs/api-client/vite.config.mts
Normal file
28
libs/api-client/vite.config.mts
Normal file
@@ -0,0 +1,28 @@
|
||||
/// <reference types='vitest' />
|
||||
import { defineConfig } from 'vite';
|
||||
import angular from '@analogjs/vite-plugin-angular';
|
||||
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
|
||||
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
|
||||
|
||||
export default defineConfig(() => ({
|
||||
root: __dirname,
|
||||
cacheDir: '../../node_modules/.vite/libs/api-client',
|
||||
plugins: [angular(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
|
||||
// Uncomment this if you are using workers.
|
||||
// worker: {
|
||||
// plugins: () => [ nxViteTsPaths() ],
|
||||
// },
|
||||
test: {
|
||||
name: 'api-client',
|
||||
watch: false,
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
||||
setupFiles: ['src/test-setup.ts'],
|
||||
reporters: ['default'],
|
||||
coverage: {
|
||||
reportsDirectory: '../../coverage/libs/api-client',
|
||||
provider: 'v8' as const,
|
||||
},
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user