@obsidiane/meridiane 2.0.10 → 2.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +19 -2
  2. package/cli.js +21 -37
  3. package/package.json +3 -3
  4. package/src/README.md +9 -0
  5. package/{tools/app → src/application}/bridge-workflow.js +15 -13
  6. package/src/application/generate-bridge.js +50 -0
  7. package/{tools/app → src/application}/run-build.js +1 -1
  8. package/{tools/app → src/application}/run-dev.js +30 -36
  9. package/{tools/app → src/application}/run-generate.js +3 -3
  10. package/{tools → src}/domain/bridge-options.js +8 -21
  11. package/src/domain/list-values.js +6 -0
  12. package/src/generators/bridge/write-bridge-files.js +100 -0
  13. package/src/generators/models/interface-schema.js +116 -0
  14. package/src/generators/models/model-selection.js +148 -0
  15. package/src/generators/models/oas-contract.js +170 -0
  16. package/{tools/generator → src/generators}/models/oas-schema-name.js +6 -15
  17. package/src/generators/models/openapi-to-models.js +136 -0
  18. package/{tools/generator → src/generators}/models/schema-utils.js +27 -3
  19. package/src/generators/models/templates/type-alias.hbs +2 -0
  20. package/src/generators/models/type-expression.js +65 -0
  21. package/src/generators/models/type-resolver.js +240 -0
  22. package/src/generators/models/utils.js +89 -0
  23. package/src/generators/models/write-model-files.js +55 -0
  24. package/src/infrastructure/dev-project.js +48 -0
  25. package/{tools/infra → src/infrastructure}/exec.js +3 -2
  26. package/src/infrastructure/local-package-install.js +104 -0
  27. package/src/infrastructure/spec-loader.js +65 -0
  28. package/{tools/infra → src/infrastructure}/standalone-workspace.js +31 -29
  29. package/templates/{_lib_template → angular-bridge}/README.md +9 -0
  30. package/templates/angular-bridge/src/lib/bridge/rest/api-platform.adapter.ts +82 -0
  31. package/templates/angular-bridge/src/lib/bridge/rest/http-request.options.ts +30 -0
  32. package/templates/{_lib_template → angular-bridge}/src/lib/bridge/rest/query-builder.ts +10 -10
  33. package/templates/angular-bridge/src/lib/bridge/sse/mercure-connections.ts +255 -0
  34. package/templates/angular-bridge/src/lib/bridge/sse/mercure-payload.ts +82 -0
  35. package/templates/angular-bridge/src/lib/bridge/sse/mercure-url.builder.ts +39 -0
  36. package/templates/angular-bridge/src/lib/bridge/sse/mercure.adapter.ts +358 -0
  37. package/templates/{_lib_template → angular-bridge}/src/lib/bridge/sse/ref-count-topic.registry.ts +3 -12
  38. package/templates/angular-bridge/src/lib/bridge/sse/subscription-claims.ts +76 -0
  39. package/templates/angular-bridge/src/lib/bridge-options.ts +59 -0
  40. package/templates/{_lib_template → angular-bridge}/src/lib/facades/bridge.facade.ts +50 -51
  41. package/templates/{_lib_template → angular-bridge}/src/lib/facades/facade.factory.ts +0 -1
  42. package/templates/angular-bridge/src/lib/facades/facade.interface.ts +13 -0
  43. package/templates/{_lib_template → angular-bridge}/src/lib/facades/resource.facade.ts +24 -31
  44. package/templates/angular-bridge/src/lib/interceptors/bearer-auth.interceptor.ts +33 -0
  45. package/templates/angular-bridge/src/lib/interceptors/bridge-defaults.interceptor.ts +65 -0
  46. package/templates/angular-bridge/src/lib/interceptors/content-type.interceptor.ts +37 -0
  47. package/templates/angular-bridge/src/lib/interceptors/scoped.interceptor.ts +9 -0
  48. package/templates/angular-bridge/src/lib/interceptors/singleflight.interceptor.ts +46 -0
  49. package/templates/{_lib_template → angular-bridge}/src/lib/ports/realtime.port.ts +6 -2
  50. package/templates/{_lib_template → angular-bridge}/src/lib/ports/resource-repository.port.ts +7 -7
  51. package/templates/angular-bridge/src/lib/provide-bridge.ts +98 -0
  52. package/templates/angular-bridge/src/lib/utils/iri.ts +6 -0
  53. package/templates/angular-bridge/src/lib/utils/request-url.ts +31 -0
  54. package/templates/{_lib_template → angular-bridge}/src/public-api.ts +2 -0
  55. package/templates/_lib_template/src/lib/bridge/rest/api-platform.adapter.ts +0 -84
  56. package/templates/_lib_template/src/lib/bridge/rest/http-request.options.ts +0 -21
  57. package/templates/_lib_template/src/lib/bridge/sse/mercure-url.builder.ts +0 -17
  58. package/templates/_lib_template/src/lib/bridge/sse/mercure.adapter.ts +0 -806
  59. package/templates/_lib_template/src/lib/facades/facade.interface.ts +0 -31
  60. package/templates/_lib_template/src/lib/interceptors/bridge-defaults.interceptor.ts +0 -53
  61. package/templates/_lib_template/src/lib/interceptors/content-type.interceptor.ts +0 -49
  62. package/templates/_lib_template/src/lib/interceptors/singleflight.interceptor.ts +0 -55
  63. package/templates/_lib_template/src/lib/provide-bridge.ts +0 -186
  64. package/tools/build.js +0 -1
  65. package/tools/dev.js +0 -1
  66. package/tools/domain/paths.js +0 -10
  67. package/tools/generate.js +0 -1
  68. package/tools/generator/models/oas-contract.js +0 -159
  69. package/tools/generator/models/openapi-to-models.js +0 -240
  70. package/tools/generator/models/type-resolver.js +0 -107
  71. package/tools/generator/models/utils.js +0 -62
  72. package/tools/infra/dev-support.js +0 -189
  73. package/tools/infra/generate/bridge.js +0 -212
  74. package/tools/infra/spec-loader.js +0 -59
  75. /package/{tools → src}/domain/lib-name.js +0 -0
  76. /package/{tools/generator → src/generators}/models/handlebars.js +0 -0
  77. /package/{tools/generator → src/generators}/models/templates/index.hbs +0 -0
  78. /package/{tools/generator → src/generators}/models/templates/model.hbs +0 -0
  79. /package/{tools/infra → src/infrastructure}/json.js +0 -0
  80. /package/{tools/infra → src/infrastructure}/logger.js +0 -0
  81. /package/templates/{_lib_template → angular-bridge}/ng-package.json +0 -0
  82. /package/templates/{_lib_template → angular-bridge}/package.json +0 -0
  83. /package/templates/{_lib_template → angular-bridge}/src/lib/bridge/sse/eventsource-wrapper.ts +0 -0
  84. /package/templates/{_lib_template → angular-bridge}/src/lib/bridge/sse/mercure-topic.mapper.ts +0 -0
  85. /package/templates/{_lib_template → angular-bridge}/src/lib/bridge.types.ts +0 -0
  86. /package/templates/{_lib_template → angular-bridge}/src/lib/interceptors/bridge-debug.interceptor.ts +0 -0
  87. /package/templates/{_lib_template → angular-bridge}/src/lib/tokens.ts +0 -0
  88. /package/templates/{_lib_template → angular-bridge}/src/lib/utils/url.ts +0 -0
  89. /package/templates/{_lib_template → angular-bridge}/src/models/index.ts +0 -0
  90. /package/templates/{_lib_template → angular-bridge}/tsconfig.lib.json +0 -0
  91. /package/templates/{_lib_template → angular-bridge}/tsconfig.lib.prod.json +0 -0
@@ -3,12 +3,13 @@ import {HttpClient} from '@angular/common/http';
3
3
  import {Observable} from 'rxjs';
4
4
  import {filter, map, share} from 'rxjs/operators';
5
5
  import {toHttpParams} from '../bridge/rest/query-builder';
6
- import {buildHttpRequestOptions} from '../bridge/rest/http-request.options';
6
+ import {buildHttpRequestOptions, resolveHttpCallOptions} from '../bridge/rest/http-request.options';
7
7
  import {MercureRealtimeAdapter} from '../bridge/sse/mercure.adapter';
8
8
  import {API_BASE_URL, BRIDGE_HTTP_WITH_CREDENTIALS} from '../tokens';
9
9
  import {AnyQuery, Collection, HttpCallOptions, HttpRequestConfig, Iri, IriRequired, Item} from '../ports/resource-repository.port';
10
- import {SubscribeFilter} from '../ports/realtime.port';
10
+ import {RealtimeDiagnosticsPort, SubscribeFilter} from '../ports/realtime.port';
11
11
  import {resolveUrl} from '../utils/url';
12
+ import {toIriList} from '../utils/iri';
12
13
  import {RealtimeDiagnostics, WatchConnectionOptions} from '../bridge.types';
13
14
 
14
15
  export type TypedEvent<TType extends string = string, TPayload = unknown> = {
@@ -35,78 +36,75 @@ export type WatchSubscriptionOptions = WatchConnectionOptions;
35
36
  export class BridgeFacade {
36
37
  constructor(
37
38
  private readonly http: HttpClient,
38
- private readonly realtime: MercureRealtimeAdapter,
39
+ @Inject(MercureRealtimeAdapter) private readonly realtime: RealtimeDiagnosticsPort,
39
40
  @Inject(API_BASE_URL) private readonly apiBase: string,
40
41
  @Inject(BRIDGE_HTTP_WITH_CREDENTIALS) private readonly httpWithCredentialsDefault: boolean,
41
42
  ) {
42
43
  }
43
44
 
44
- // ──────────────── HTTP ────────────────
45
-
46
- get$<R = unknown>(url: IriRequired, opts?: HttpCallOptions): Observable<R> {
47
- return this.http.get<R>(this.resolveUrl(url), {
48
- headers: opts?.headers,
49
- withCredentials: opts?.withCredentials ?? this.httpWithCredentialsDefault,
50
- });
45
+ get$<R = unknown>(url: IriRequired, options?: HttpCallOptions): Observable<R> {
46
+ return this.http.get<R>(
47
+ this.resolveUrl(url),
48
+ resolveHttpCallOptions(options, this.httpWithCredentialsDefault)
49
+ );
51
50
  }
52
51
 
53
52
  getCollection$<T extends Item = Item>(
54
53
  url: IriRequired,
55
54
  query?: AnyQuery,
56
- opts?: HttpCallOptions
55
+ options?: HttpCallOptions
57
56
  ): Observable<Collection<T>> {
58
- const params = toHttpParams(query);
59
57
  return this.http.get<Collection<T>>(this.resolveUrl(url), {
60
- params,
61
- headers: opts?.headers,
62
- withCredentials: opts?.withCredentials ?? this.httpWithCredentialsDefault,
58
+ ...resolveHttpCallOptions(options, this.httpWithCredentialsDefault),
59
+ params: toHttpParams(query),
63
60
  });
64
61
  }
65
62
 
66
- post$<R = unknown, B = unknown>(url: IriRequired, payload: B, opts?: HttpCallOptions): Observable<R> {
67
- return this.http.post<R>(this.resolveUrl(url), payload, {
68
- headers: opts?.headers,
69
- withCredentials: opts?.withCredentials ?? this.httpWithCredentialsDefault,
70
- });
63
+ post$<R = unknown, B = unknown>(url: IriRequired, payload: B, options?: HttpCallOptions): Observable<R> {
64
+ return this.http.post<R>(
65
+ this.resolveUrl(url),
66
+ payload,
67
+ resolveHttpCallOptions(options, this.httpWithCredentialsDefault)
68
+ );
71
69
  }
72
70
 
73
- patch$<R = unknown, B = unknown>(url: IriRequired, changes: B, opts?: HttpCallOptions): Observable<R> {
74
- return this.http.patch<R>(this.resolveUrl(url), changes, {
75
- headers: opts?.headers,
76
- withCredentials: opts?.withCredentials ?? this.httpWithCredentialsDefault,
77
- });
71
+ patch$<R = unknown, B = unknown>(url: IriRequired, changes: B, options?: HttpCallOptions): Observable<R> {
72
+ return this.http.patch<R>(
73
+ this.resolveUrl(url),
74
+ changes,
75
+ resolveHttpCallOptions(options, this.httpWithCredentialsDefault)
76
+ );
78
77
  }
79
78
 
80
- put$<R = unknown, B = unknown>(url: IriRequired, payload: B, opts?: HttpCallOptions): Observable<R> {
81
- return this.http.put<R>(this.resolveUrl(url), payload, {
82
- headers: opts?.headers,
83
- withCredentials: opts?.withCredentials ?? this.httpWithCredentialsDefault,
84
- });
79
+ put$<R = unknown, B = unknown>(url: IriRequired, payload: B, options?: HttpCallOptions): Observable<R> {
80
+ return this.http.put<R>(
81
+ this.resolveUrl(url),
82
+ payload,
83
+ resolveHttpCallOptions(options, this.httpWithCredentialsDefault)
84
+ );
85
85
  }
86
86
 
87
- delete$(url: IriRequired, opts?: HttpCallOptions): Observable<void> {
88
- return this.http.delete<void>(this.resolveUrl(url), {
89
- headers: opts?.headers,
90
- withCredentials: opts?.withCredentials ?? this.httpWithCredentialsDefault,
91
- });
87
+ delete$(url: IriRequired, options?: HttpCallOptions): Observable<void> {
88
+ return this.http.delete<void>(
89
+ this.resolveUrl(url),
90
+ resolveHttpCallOptions(options, this.httpWithCredentialsDefault)
91
+ );
92
92
  }
93
93
 
94
- request$<R = unknown, B = unknown>(req: HttpRequestConfig<B>): Observable<R> {
95
- const {method, url} = req;
94
+ request$<R = unknown, B = unknown>(request: HttpRequestConfig<B>): Observable<R> {
95
+ const {method, url} = request;
96
96
  const targetUrl = this.resolveUrl(url);
97
97
 
98
- const mergedOptions = buildHttpRequestOptions(req, {withCredentialsDefault: this.httpWithCredentialsDefault});
98
+ const mergedOptions = buildHttpRequestOptions(request, {withCredentialsDefault: this.httpWithCredentialsDefault});
99
99
  return this.http.request<R>(method, targetUrl, mergedOptions as {observe: 'body'});
100
100
  }
101
101
 
102
- // ──────────────── SSE / Mercure ────────────────
103
-
104
102
  watch$<T = Item>(
105
103
  iri: Iri | Iri[],
106
104
  subscribeFilter?: SubscribeFilter,
107
105
  options?: WatchSubscriptionOptions
108
106
  ): Observable<T> {
109
- const iris = (Array.isArray(iri) ? iri : [iri]).filter((v): v is string => typeof v === 'string' && v.length > 0);
107
+ const iris = toIriList(iri);
110
108
  return this.realtime
111
109
  .subscribe$<T>(iris, subscribeFilter, options)
112
110
  .pipe(
@@ -131,12 +129,14 @@ export class BridgeFacade {
131
129
  watchTypes$<R extends Record<string, any>>(
132
130
  iri: Iri | Iri[],
133
131
  resourceTypes: (keyof R & string)[],
134
- cfg?: WatchTypesConfig,
132
+ config?: WatchTypesConfig,
135
133
  options?: WatchSubscriptionOptions
136
134
  ): Observable<WatchTypesResult<R>> {
137
- const iris = (Array.isArray(iri) ? iri : [iri]).filter((v): v is string => typeof v === 'string' && v.length > 0);
138
- const discriminator = cfg?.discriminator ?? '@type';
139
- const allowedTypes = new Set(resourceTypes.filter((v): v is string => typeof v === 'string' && v.length > 0));
135
+ const iris = toIriList(iri);
136
+ const discriminator = config?.discriminator ?? '@type';
137
+ const allowedTypes = new Set(
138
+ resourceTypes.filter((type): type is string => typeof type === 'string' && type.length > 0)
139
+ );
140
140
 
141
141
  return this.realtime
142
142
  .subscribeAll$<unknown>(iris, options)
@@ -151,14 +151,13 @@ export class BridgeFacade {
151
151
  const key = resolvedType as keyof R & string;
152
152
  return {resourceType: key, payload: raw as R[keyof R]} as WatchTypesResult<R>;
153
153
  }),
154
- filter((evt): evt is WatchTypesResult<R> => !!evt),
154
+ filter((event): event is WatchTypesResult<R> => !!event),
155
155
  share()
156
156
  );
157
157
  }
158
158
 
159
159
  unwatch(iri: Iri | Iri[]): void {
160
- const iris = (Array.isArray(iri) ? iri : [iri]).filter((v): v is string => typeof v === 'string' && v.length > 0);
161
- this.realtime.unsubscribe(iris);
160
+ this.realtime.unsubscribe(toIriList(iri));
162
161
  }
163
162
 
164
163
  realtimeDiagnostics$(): Observable<RealtimeDiagnostics> {
@@ -171,9 +170,9 @@ export class BridgeFacade {
171
170
  }
172
171
  }
173
172
 
174
- function readDiscriminator(raw: unknown, discriminator: string): string | undefined {
175
- if (!raw || typeof raw !== 'object') return undefined;
176
- const value = (raw as any)?.[discriminator];
173
+ function readDiscriminator(payload: unknown, discriminator: string): string | undefined {
174
+ if (!payload || typeof payload !== 'object') return undefined;
175
+ const value = (payload as Record<string, unknown>)[discriminator];
177
176
  if (typeof value === 'string') return value;
178
177
  if (Array.isArray(value)) {
179
178
  const first = value.find((v) => typeof v === 'string');
@@ -13,7 +13,6 @@ export type FacadeConfig<T> = {
13
13
  realtime?: RealtimePort;
14
14
  };
15
15
 
16
-
17
16
  @Injectable({providedIn: 'root'})
18
17
  export class FacadeFactory {
19
18
  private readonly env = inject(EnvironmentInjector);
@@ -0,0 +1,13 @@
1
+ import {
2
+ Iri,
3
+ Item,
4
+ ResourceRepository,
5
+ } from '../ports/resource-repository.port';
6
+ import {Observable} from 'rxjs';
7
+ import {WatchConnectionOptions} from '../bridge.types';
8
+
9
+ export interface Facade<T extends Item> extends ResourceRepository<T> {
10
+ watch$(iri: Iri | Iri[], options?: WatchConnectionOptions): Observable<T>;
11
+
12
+ unwatch(iri: Iri | Iri[]): void;
13
+ }
@@ -7,54 +7,51 @@ import {
7
7
  IriRequired,
8
8
  Collection,
9
9
  Item,
10
- HttpRequestConfig
10
+ HttpRequestConfig,
11
11
  } from '../ports/resource-repository.port';
12
12
  import {RealtimePort, RealtimeStatus} from '../ports/realtime.port';
13
13
  import {toSignal} from '@angular/core/rxjs-interop';
14
14
  import {filter, map, Observable, share} from 'rxjs';
15
15
  import {Facade} from './facade.interface';
16
16
  import {WatchConnectionOptions} from '../bridge.types';
17
+ import {toIriList} from '../utils/iri';
17
18
 
18
19
  export class ResourceFacade<T extends Item> implements Facade<T> {
19
-
20
20
  readonly connectionStatus: Signal<RealtimeStatus>;
21
21
 
22
22
  constructor(
23
23
  protected readonly repo: ResourceRepository<T>,
24
24
  protected readonly realtime: RealtimePort
25
25
  ) {
26
- this.connectionStatus = toSignal(
27
- this.realtime.status$(),
28
- {initialValue: 'closed'}
29
- );
26
+ this.connectionStatus = toSignal(this.realtime.status$(), {initialValue: 'closed'});
30
27
  }
31
28
 
32
- getCollection$(query?: AnyQuery, opts?: HttpCallOptions): Observable<Collection<T>> {
33
- return this.repo.getCollection$(query, opts);
29
+ getCollection$(query?: AnyQuery, options?: HttpCallOptions): Observable<Collection<T>> {
30
+ return this.repo.getCollection$(query, options);
34
31
  }
35
32
 
36
- get$(iri: IriRequired, opts?: HttpCallOptions): Observable<T> {
37
- return this.repo.get$(iri, opts);
33
+ get$(iri: IriRequired, options?: HttpCallOptions): Observable<T> {
34
+ return this.repo.get$(iri, options);
38
35
  }
39
36
 
40
- patch$(iri: IriRequired, changes: Partial<T>, opts?: HttpCallOptions): Observable<T> {
41
- return this.repo.patch$(iri, changes, opts);
37
+ patch$(iri: IriRequired, changes: Partial<T>, options?: HttpCallOptions): Observable<T> {
38
+ return this.repo.patch$(iri, changes, options);
42
39
  }
43
40
 
44
- post$(payload: Partial<T>, opts?: HttpCallOptions): Observable<T> {
45
- return this.repo.post$(payload, opts);
41
+ post$(payload: Partial<T>, options?: HttpCallOptions): Observable<T> {
42
+ return this.repo.post$(payload, options);
46
43
  }
47
44
 
48
- put$(iri: IriRequired, payload: Partial<T>, opts?: HttpCallOptions): Observable<T> {
49
- return this.repo.put$(iri, payload, opts);
45
+ put$(iri: IriRequired, payload: Partial<T>, options?: HttpCallOptions): Observable<T> {
46
+ return this.repo.put$(iri, payload, options);
50
47
  }
51
48
 
52
- delete$(iri: IriRequired, opts?: HttpCallOptions): Observable<void> {
53
- return this.repo.delete$(iri, opts);
49
+ delete$(iri: IriRequired, options?: HttpCallOptions): Observable<void> {
50
+ return this.repo.delete$(iri, options);
54
51
  }
55
52
 
56
- request$<R = unknown, B = unknown>(req: HttpRequestConfig<B>): Observable<R> {
57
- return this.repo.request$<R, B>(req);
53
+ request$<R = unknown, B = unknown>(request: HttpRequestConfig<B>): Observable<R> {
54
+ return this.repo.request$<R, B>(request);
58
55
  }
59
56
 
60
57
  /**
@@ -62,14 +59,10 @@ export class ResourceFacade<T extends Item> implements Facade<T> {
62
59
  * Undefined/empty values are ignored.
63
60
  */
64
61
  watch$(iri: Iri | Iri[], options?: WatchConnectionOptions): Observable<T> {
65
- const iris = (Array.isArray(iri) ? iri : [iri]).filter((v): v is string => typeof v === 'string' && v.length > 0);
66
- return this.subscribeAndSync(iris, options);
62
+ return this.subscribeAndSync(toIriList(iri), options);
67
63
  }
68
-
69
-
70
64
  unwatch(iri: Iri | Iri[]): void {
71
- const iris = (Array.isArray(iri) ? iri : [iri]).filter((v): v is string => typeof v === 'string' && v.length > 0);
72
- this.realtime.unsubscribe(iris);
65
+ this.realtime.unsubscribe(toIriList(iri));
73
66
  }
74
67
 
75
68
  /**
@@ -81,12 +74,12 @@ export class ResourceFacade<T extends Item> implements Facade<T> {
81
74
  field: string,
82
75
  options?: WatchConnectionOptions
83
76
  ): Observable<R> {
84
- const iris = (Array.isArray(iri) ? iri : [iri]).filter((v): v is string => typeof v === 'string' && v.length > 0);
77
+ const iris = toIriList(iri);
85
78
  return this.realtime
86
- .subscribe$<R>(iris, {field: field}, options)
79
+ .subscribe$<R>(iris, {field}, options)
87
80
  .pipe(
88
- map(e => e.data),
89
- filter((d): d is R => d !== undefined),
81
+ map((event) => event.data),
82
+ filter((data): data is R => data !== undefined),
90
83
  share()
91
84
  );
92
85
  }
@@ -95,7 +88,7 @@ export class ResourceFacade<T extends Item> implements Facade<T> {
95
88
  return this.realtime
96
89
  .subscribe$<T>(iris, undefined, options)
97
90
  .pipe(
98
- map(event => event.data),
91
+ map((event) => event.data),
99
92
  filter((data): data is T => data !== undefined),
100
93
  share()
101
94
  );
@@ -0,0 +1,33 @@
1
+ import {HttpInterceptorFn} from '@angular/common/http';
2
+ import {from, switchMap} from 'rxjs';
3
+ import {BridgeAuth} from '../bridge-options';
4
+ import {isRequestWithinBaseUrl} from '../utils/request-url';
5
+
6
+ type TokenProvider = () => string | undefined | Promise<string | undefined>;
7
+
8
+ export function createAuthInterceptors(auth: BridgeAuth | undefined, baseUrl: string): HttpInterceptorFn[] {
9
+ if (!auth) return [];
10
+ if (typeof auth === 'function') return [auth];
11
+
12
+ const bearer = typeof auth === 'string' ? {type: 'bearer' as const, token: auth} : auth;
13
+ if (bearer.type !== 'bearer') return [];
14
+
15
+ const getToken: TokenProvider = 'token' in bearer ? () => bearer.token : bearer.getToken;
16
+ return [createBearerAuthInterceptor(baseUrl, getToken)];
17
+ }
18
+
19
+ function createBearerAuthInterceptor(baseUrl: string, getToken: TokenProvider): HttpInterceptorFn {
20
+ return (request, next) => {
21
+ if (!isRequestWithinBaseUrl(request.url, baseUrl) || request.headers.has('Authorization')) {
22
+ return next(request);
23
+ }
24
+
25
+ return from(Promise.resolve(getToken())).pipe(
26
+ switchMap((token) => {
27
+ if (!token) return next(request);
28
+ const headers = request.headers.set('Authorization', `Bearer ${token}`);
29
+ return next(request.clone({headers}));
30
+ })
31
+ );
32
+ };
33
+ }
@@ -0,0 +1,65 @@
1
+ import {HttpInterceptorFn, HttpRequest} from '@angular/common/http';
2
+ import {inject} from '@angular/core';
3
+ import {retry, timeout, timer} from 'rxjs';
4
+ import {BridgeDefaults} from '../bridge.types';
5
+ import {BRIDGE_DEFAULTS, BRIDGE_LOGGER} from '../tokens';
6
+
7
+ const DEFAULT_RETRY_METHODS = ['GET', 'HEAD', 'OPTIONS'];
8
+
9
+ type RetryPolicy = {
10
+ count: number;
11
+ delayMs: number;
12
+ methods: Set<string>;
13
+ };
14
+
15
+ /**
16
+ * Applies default headers, timeout and retry policy configured via `provideBridge({defaults: ...})`.
17
+ */
18
+ export const bridgeDefaultsInterceptor: HttpInterceptorFn = (req, next) => {
19
+ const defaults = inject(BRIDGE_DEFAULTS, {optional: true}) ?? {};
20
+ const logger = inject(BRIDGE_LOGGER, {optional: true});
21
+ const request = applyDefaultHeaders(req, defaults.headers);
22
+
23
+ let response$ = next(request);
24
+ if (typeof defaults.timeoutMs === 'number' && defaults.timeoutMs > 0) {
25
+ response$ = response$.pipe(timeout({first: defaults.timeoutMs}));
26
+ }
27
+
28
+ const retryPolicy = resolveRetryPolicy(defaults.retries);
29
+ if (!retryPolicy || !retryPolicy.methods.has(req.method.toUpperCase())) {
30
+ return response$;
31
+ }
32
+
33
+ return response$.pipe(
34
+ retry({
35
+ count: retryPolicy.count,
36
+ delay: (_error, retryIndex) => {
37
+ logger?.debug?.('[Bridge] retry', {url: req.urlWithParams, method: req.method, retryIndex});
38
+ return timer(retryPolicy.delayMs);
39
+ },
40
+ })
41
+ );
42
+ };
43
+
44
+ function applyDefaultHeaders(request: HttpRequest<unknown>, defaults?: Record<string, string>): HttpRequest<unknown> {
45
+ if (!defaults) return request;
46
+
47
+ let headers = request.headers;
48
+ for (const [name, value] of Object.entries(defaults)) {
49
+ if (!headers.has(name)) headers = headers.set(name, value);
50
+ }
51
+
52
+ return headers === request.headers ? request : request.clone({headers});
53
+ }
54
+
55
+ function resolveRetryPolicy(retries: BridgeDefaults['retries']): RetryPolicy | undefined {
56
+ const count = typeof retries === 'number' ? retries : retries?.count;
57
+ if (!count || count <= 0) return undefined;
58
+
59
+ const methods = typeof retries === 'object' && retries.methods ? retries.methods : DEFAULT_RETRY_METHODS;
60
+ return {
61
+ count,
62
+ delayMs: typeof retries === 'object' ? (retries.delayMs ?? 250) : 250,
63
+ methods: new Set(methods.map((method) => method.toUpperCase())),
64
+ };
65
+ }
@@ -0,0 +1,37 @@
1
+ import {HttpInterceptorFn} from '@angular/common/http';
2
+
3
+ export interface ApiMediaTypes {
4
+ accept: string;
5
+ post: string;
6
+ put: string;
7
+ patch: string;
8
+ }
9
+
10
+ export const DEFAULT_MEDIA_TYPES: ApiMediaTypes = {
11
+ accept: 'application/ld+json',
12
+ post: 'application/ld+json',
13
+ put: 'application/ld+json',
14
+ patch: 'application/merge-patch+json',
15
+ };
16
+
17
+ const CONTENT_TYPE_BY_METHOD: Record<string, string> = {
18
+ POST: DEFAULT_MEDIA_TYPES.post,
19
+ PUT: DEFAULT_MEDIA_TYPES.put,
20
+ PATCH: DEFAULT_MEDIA_TYPES.patch,
21
+ };
22
+
23
+ export const contentTypeInterceptor: HttpInterceptorFn = (req, next) => {
24
+ let headers = req.headers;
25
+
26
+ if (!headers.has('Accept')) {
27
+ headers = headers.set('Accept', DEFAULT_MEDIA_TYPES.accept);
28
+ }
29
+
30
+ const contentType = CONTENT_TYPE_BY_METHOD[req.method];
31
+ const isFormData = typeof FormData !== 'undefined' && req.body instanceof FormData;
32
+ if (contentType && !isFormData && !headers.has('Content-Type')) {
33
+ headers = headers.set('Content-Type', contentType);
34
+ }
35
+
36
+ return next(headers === req.headers ? req : req.clone({headers}));
37
+ };
@@ -0,0 +1,9 @@
1
+ import {HttpInterceptorFn} from '@angular/common/http';
2
+ import {isRequestWithinBaseUrl} from '../utils/request-url';
3
+
4
+ /** Limits a bridge interceptor to requests targeting the configured API. */
5
+ export function scopeInterceptor(baseUrl: string, interceptor: HttpInterceptorFn): HttpInterceptorFn {
6
+ return (request, next) => isRequestWithinBaseUrl(request.url, baseUrl)
7
+ ? interceptor(request, next)
8
+ : next(request);
9
+ }
@@ -0,0 +1,46 @@
1
+ import {HttpEvent, HttpHeaders, HttpInterceptorFn, HttpRequest} from '@angular/common/http';
2
+ import {finalize, Observable, shareReplay} from 'rxjs';
3
+
4
+ const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);
5
+
6
+ export function createSingleFlightInterceptor(): HttpInterceptorFn {
7
+ const inFlight = new Map<string, Observable<HttpEvent<unknown>>>();
8
+
9
+ return (request, next) => {
10
+ const method = request.method.toUpperCase();
11
+ if (!SAFE_METHODS.has(method) || request.reportProgress) {
12
+ return next(request);
13
+ }
14
+
15
+ const key = computeKey(request);
16
+ const existing = inFlight.get(key);
17
+ if (existing) {
18
+ return existing;
19
+ }
20
+
21
+ const shared$ = next(request).pipe(
22
+ finalize(() => inFlight.delete(key)),
23
+ shareReplay({bufferSize: 1, refCount: true})
24
+ );
25
+
26
+ inFlight.set(key, shared$);
27
+ return shared$;
28
+ };
29
+ }
30
+
31
+ function computeKey(request: HttpRequest<unknown>): string {
32
+ return [
33
+ request.method.toUpperCase(),
34
+ request.urlWithParams,
35
+ `rt=${request.responseType}`,
36
+ `wc=${request.withCredentials ? '1' : '0'}`,
37
+ `h=${serializeHeaders(request.headers)}`,
38
+ ].join('::');
39
+ }
40
+
41
+ function serializeHeaders(headers: HttpHeaders): string {
42
+ return headers.keys()
43
+ .sort((left, right) => left.localeCompare(right))
44
+ .map((name) => `${name.toLowerCase()}=${JSON.stringify(headers.getAll(name) ?? [])}`)
45
+ .join('&');
46
+ }
@@ -1,8 +1,8 @@
1
1
  import {Observable} from 'rxjs';
2
2
  import {Iri} from './resource-repository.port';
3
- import {WatchConnectionOptions} from '../bridge.types';
3
+ import {RealtimeConnectionStatus, RealtimeDiagnostics, WatchConnectionOptions} from '../bridge.types';
4
4
 
5
- export type RealtimeStatus = 'connecting' | 'connected' | 'closed';
5
+ export type RealtimeStatus = RealtimeConnectionStatus;
6
6
 
7
7
  export interface SseEvent {
8
8
  type: string;
@@ -65,3 +65,7 @@ export interface RealtimePort {
65
65
 
66
66
  status$(): Observable<RealtimeStatus>;
67
67
  }
68
+
69
+ export interface RealtimeDiagnosticsPort extends RealtimePort {
70
+ diagnostics$(): Observable<RealtimeDiagnostics>;
71
+ }
@@ -51,19 +51,19 @@ export interface HttpCallOptions {
51
51
  }
52
52
 
53
53
  export interface ResourceRepository<T> {
54
- getCollection$(query?: AnyQuery, opts?: HttpCallOptions): Observable<Collection<T>>;
54
+ getCollection$(query?: AnyQuery, options?: HttpCallOptions): Observable<Collection<T>>;
55
55
 
56
- get$(iri: IriRequired, opts?: HttpCallOptions): Observable<T>;
56
+ get$(iri: IriRequired, options?: HttpCallOptions): Observable<T>;
57
57
 
58
- post$(payload: Partial<T>, opts?: HttpCallOptions): Observable<T>;
58
+ post$(payload: Partial<T>, options?: HttpCallOptions): Observable<T>;
59
59
 
60
- patch$(iri: IriRequired, changes: Partial<T>, opts?: HttpCallOptions): Observable<T>;
60
+ patch$(iri: IriRequired, changes: Partial<T>, options?: HttpCallOptions): Observable<T>;
61
61
 
62
- put$(iri: IriRequired, payload: Partial<T>, opts?: HttpCallOptions): Observable<T>;
62
+ put$(iri: IriRequired, payload: Partial<T>, options?: HttpCallOptions): Observable<T>;
63
63
 
64
- delete$(iri: IriRequired, opts?: HttpCallOptions): Observable<void>;
64
+ delete$(iri: IriRequired, options?: HttpCallOptions): Observable<void>;
65
65
 
66
- request$<R = unknown, B = unknown>(req: HttpRequestConfig<B>): Observable<R>;
66
+ request$<R = unknown, B = unknown>(request: HttpRequestConfig<B>): Observable<R>;
67
67
  }
68
68
 
69
69
  export interface HttpRequestConfig<TBody = unknown> {
@@ -0,0 +1,98 @@
1
+ import {EnvironmentProviders, makeEnvironmentProviders} from '@angular/core';
2
+ import {HttpInterceptorFn, provideHttpClient, withFetch, withInterceptors} from '@angular/common/http';
3
+ import {BridgeOptions} from './bridge-options';
4
+ import {BridgeLogger, MercureConnectionMode, MercureTopicMode} from './bridge.types';
5
+ import {createAuthInterceptors} from './interceptors/bearer-auth.interceptor';
6
+ import {bridgeDebugInterceptor} from './interceptors/bridge-debug.interceptor';
7
+ import {bridgeDefaultsInterceptor} from './interceptors/bridge-defaults.interceptor';
8
+ import {contentTypeInterceptor} from './interceptors/content-type.interceptor';
9
+ import {scopeInterceptor} from './interceptors/scoped.interceptor';
10
+ import {createSingleFlightInterceptor} from './interceptors/singleflight.interceptor';
11
+ import {
12
+ API_BASE_URL,
13
+ BRIDGE_DEFAULTS,
14
+ BRIDGE_HTTP_WITH_CREDENTIALS,
15
+ BRIDGE_LOGGER,
16
+ BRIDGE_MERCURE_WITH_CREDENTIALS,
17
+ MERCURE_CONNECTION_MODE,
18
+ MERCURE_HUB_URL,
19
+ MERCURE_MAX_URL_LENGTH,
20
+ MERCURE_TOPIC_MODE,
21
+ } from './tokens';
22
+
23
+ export type {BridgeAuth, BridgeHttpOptions, BridgeMercureOptions, BridgeOptions} from './bridge-options';
24
+
25
+ /** Registers the bridge HTTP client, interceptors, Mercure realtime adapter and configuration tokens. */
26
+ export function provideBridge(options: BridgeOptions): EnvironmentProviders {
27
+ const {
28
+ baseUrl,
29
+ auth,
30
+ http,
31
+ mercure,
32
+ defaults,
33
+ singleFlight = true,
34
+ debug = false,
35
+ extraInterceptors = [],
36
+ } = options;
37
+
38
+ if (!baseUrl) {
39
+ throw new Error("provideBridge(): missing 'baseUrl'");
40
+ }
41
+
42
+ const resolvedMercureInit: RequestInit = mercure?.init ?? {credentials: 'include' as RequestCredentials};
43
+ const resolvedMercureHubUrl = mercure?.hubUrl;
44
+ const resolvedMercureTopicMode: MercureTopicMode = mercure?.topicMode ?? 'url';
45
+ const resolvedMercureConnectionMode: MercureConnectionMode = mercure?.connectionMode ?? 'auto';
46
+ const resolvedMercureMaxUrlLength = normalizeMercureMaxUrlLength(mercure?.maxUrlLength, 1900);
47
+ const mercureWithCredentials = resolveWithCredentials(resolvedMercureInit);
48
+ const httpWithCredentials = http?.withCredentials ?? mercureWithCredentials;
49
+
50
+ const loggerProvider: BridgeLogger = createBridgeLogger(debug);
51
+ const scopeToBridge = (interceptor: HttpInterceptorFn) => scopeInterceptor(baseUrl, interceptor);
52
+
53
+ const interceptors: HttpInterceptorFn[] = [
54
+ scopeToBridge(contentTypeInterceptor),
55
+ scopeToBridge(bridgeDefaultsInterceptor),
56
+ ...createAuthInterceptors(auth, baseUrl),
57
+ ...(singleFlight ? [scopeToBridge(createSingleFlightInterceptor())] : []),
58
+ ...(debug ? [scopeToBridge(bridgeDebugInterceptor)] : []),
59
+ ...extraInterceptors,
60
+ ];
61
+
62
+ return makeEnvironmentProviders([
63
+ provideHttpClient(
64
+ withFetch(),
65
+ withInterceptors(interceptors)
66
+ ),
67
+ {provide: API_BASE_URL, useValue: baseUrl},
68
+ {provide: BRIDGE_HTTP_WITH_CREDENTIALS, useValue: httpWithCredentials},
69
+ {provide: BRIDGE_MERCURE_WITH_CREDENTIALS, useValue: mercureWithCredentials},
70
+ ...(resolvedMercureHubUrl ? [{provide: MERCURE_HUB_URL, useValue: resolvedMercureHubUrl}] : []),
71
+ {provide: MERCURE_TOPIC_MODE, useValue: resolvedMercureTopicMode},
72
+ {provide: MERCURE_CONNECTION_MODE, useValue: resolvedMercureConnectionMode},
73
+ {provide: MERCURE_MAX_URL_LENGTH, useValue: resolvedMercureMaxUrlLength},
74
+ {provide: BRIDGE_DEFAULTS, useValue: defaults ?? {}},
75
+ {provide: BRIDGE_LOGGER, useValue: loggerProvider},
76
+ ]);
77
+ }
78
+
79
+ function createBridgeLogger(debug: boolean): BridgeLogger {
80
+ const noop = () => undefined;
81
+ return {
82
+ debug: debug ? console.debug.bind(console) : noop,
83
+ info: debug ? console.info.bind(console) : noop,
84
+ warn: debug ? console.warn.bind(console) : noop,
85
+ error: console.error.bind(console),
86
+ };
87
+ }
88
+
89
+ function resolveWithCredentials(init: RequestInit | undefined): boolean {
90
+ if (!init) return false;
91
+ const anyInit = init as RequestInit & {withCredentials?: boolean};
92
+ return anyInit.withCredentials === true || init.credentials === 'include';
93
+ }
94
+
95
+ function normalizeMercureMaxUrlLength(input: unknown, fallback: number): number {
96
+ const value = typeof input === 'number' ? Math.floor(input) : fallback;
97
+ return Number.isFinite(value) && value >= 256 ? value : fallback;
98
+ }