@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.
- package/README.md +19 -2
- package/cli.js +21 -37
- package/package.json +3 -3
- package/src/README.md +9 -0
- package/{tools/app → src/application}/bridge-workflow.js +15 -13
- package/src/application/generate-bridge.js +50 -0
- package/{tools/app → src/application}/run-build.js +1 -1
- package/{tools/app → src/application}/run-dev.js +30 -36
- package/{tools/app → src/application}/run-generate.js +3 -3
- package/{tools → src}/domain/bridge-options.js +8 -21
- package/src/domain/list-values.js +6 -0
- package/src/generators/bridge/write-bridge-files.js +100 -0
- package/src/generators/models/interface-schema.js +116 -0
- package/src/generators/models/model-selection.js +148 -0
- package/src/generators/models/oas-contract.js +170 -0
- package/{tools/generator → src/generators}/models/oas-schema-name.js +6 -15
- package/src/generators/models/openapi-to-models.js +136 -0
- package/{tools/generator → src/generators}/models/schema-utils.js +27 -3
- package/src/generators/models/templates/type-alias.hbs +2 -0
- package/src/generators/models/type-expression.js +65 -0
- package/src/generators/models/type-resolver.js +240 -0
- package/src/generators/models/utils.js +89 -0
- package/src/generators/models/write-model-files.js +55 -0
- package/src/infrastructure/dev-project.js +48 -0
- package/{tools/infra → src/infrastructure}/exec.js +3 -2
- package/src/infrastructure/local-package-install.js +104 -0
- package/src/infrastructure/spec-loader.js +65 -0
- package/{tools/infra → src/infrastructure}/standalone-workspace.js +31 -29
- package/templates/{_lib_template → angular-bridge}/README.md +9 -0
- package/templates/angular-bridge/src/lib/bridge/rest/api-platform.adapter.ts +82 -0
- package/templates/angular-bridge/src/lib/bridge/rest/http-request.options.ts +30 -0
- package/templates/{_lib_template → angular-bridge}/src/lib/bridge/rest/query-builder.ts +10 -10
- package/templates/angular-bridge/src/lib/bridge/sse/mercure-connections.ts +255 -0
- package/templates/angular-bridge/src/lib/bridge/sse/mercure-payload.ts +82 -0
- package/templates/angular-bridge/src/lib/bridge/sse/mercure-url.builder.ts +39 -0
- package/templates/angular-bridge/src/lib/bridge/sse/mercure.adapter.ts +358 -0
- package/templates/{_lib_template → angular-bridge}/src/lib/bridge/sse/ref-count-topic.registry.ts +3 -12
- package/templates/angular-bridge/src/lib/bridge/sse/subscription-claims.ts +76 -0
- package/templates/angular-bridge/src/lib/bridge-options.ts +59 -0
- package/templates/{_lib_template → angular-bridge}/src/lib/facades/bridge.facade.ts +50 -51
- package/templates/{_lib_template → angular-bridge}/src/lib/facades/facade.factory.ts +0 -1
- package/templates/angular-bridge/src/lib/facades/facade.interface.ts +13 -0
- package/templates/{_lib_template → angular-bridge}/src/lib/facades/resource.facade.ts +24 -31
- package/templates/angular-bridge/src/lib/interceptors/bearer-auth.interceptor.ts +33 -0
- package/templates/angular-bridge/src/lib/interceptors/bridge-defaults.interceptor.ts +65 -0
- package/templates/angular-bridge/src/lib/interceptors/content-type.interceptor.ts +37 -0
- package/templates/angular-bridge/src/lib/interceptors/scoped.interceptor.ts +9 -0
- package/templates/angular-bridge/src/lib/interceptors/singleflight.interceptor.ts +46 -0
- package/templates/{_lib_template → angular-bridge}/src/lib/ports/realtime.port.ts +6 -2
- package/templates/{_lib_template → angular-bridge}/src/lib/ports/resource-repository.port.ts +7 -7
- package/templates/angular-bridge/src/lib/provide-bridge.ts +98 -0
- package/templates/angular-bridge/src/lib/utils/iri.ts +6 -0
- package/templates/angular-bridge/src/lib/utils/request-url.ts +31 -0
- package/templates/{_lib_template → angular-bridge}/src/public-api.ts +2 -0
- package/templates/_lib_template/src/lib/bridge/rest/api-platform.adapter.ts +0 -84
- package/templates/_lib_template/src/lib/bridge/rest/http-request.options.ts +0 -21
- package/templates/_lib_template/src/lib/bridge/sse/mercure-url.builder.ts +0 -17
- package/templates/_lib_template/src/lib/bridge/sse/mercure.adapter.ts +0 -806
- package/templates/_lib_template/src/lib/facades/facade.interface.ts +0 -31
- package/templates/_lib_template/src/lib/interceptors/bridge-defaults.interceptor.ts +0 -53
- package/templates/_lib_template/src/lib/interceptors/content-type.interceptor.ts +0 -49
- package/templates/_lib_template/src/lib/interceptors/singleflight.interceptor.ts +0 -55
- package/templates/_lib_template/src/lib/provide-bridge.ts +0 -186
- package/tools/build.js +0 -1
- package/tools/dev.js +0 -1
- package/tools/domain/paths.js +0 -10
- package/tools/generate.js +0 -1
- package/tools/generator/models/oas-contract.js +0 -159
- package/tools/generator/models/openapi-to-models.js +0 -240
- package/tools/generator/models/type-resolver.js +0 -107
- package/tools/generator/models/utils.js +0 -62
- package/tools/infra/dev-support.js +0 -189
- package/tools/infra/generate/bridge.js +0 -212
- package/tools/infra/spec-loader.js +0 -59
- /package/{tools → src}/domain/lib-name.js +0 -0
- /package/{tools/generator → src/generators}/models/handlebars.js +0 -0
- /package/{tools/generator → src/generators}/models/templates/index.hbs +0 -0
- /package/{tools/generator → src/generators}/models/templates/model.hbs +0 -0
- /package/{tools/infra → src/infrastructure}/json.js +0 -0
- /package/{tools/infra → src/infrastructure}/logger.js +0 -0
- /package/templates/{_lib_template → angular-bridge}/ng-package.json +0 -0
- /package/templates/{_lib_template → angular-bridge}/package.json +0 -0
- /package/templates/{_lib_template → angular-bridge}/src/lib/bridge/sse/eventsource-wrapper.ts +0 -0
- /package/templates/{_lib_template → angular-bridge}/src/lib/bridge/sse/mercure-topic.mapper.ts +0 -0
- /package/templates/{_lib_template → angular-bridge}/src/lib/bridge.types.ts +0 -0
- /package/templates/{_lib_template → angular-bridge}/src/lib/interceptors/bridge-debug.interceptor.ts +0 -0
- /package/templates/{_lib_template → angular-bridge}/src/lib/tokens.ts +0 -0
- /package/templates/{_lib_template → angular-bridge}/src/lib/utils/url.ts +0 -0
- /package/templates/{_lib_template → angular-bridge}/src/models/index.ts +0 -0
- /package/templates/{_lib_template → angular-bridge}/tsconfig.lib.json +0 -0
- /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:
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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,
|
|
67
|
-
return this.http.post<R>(
|
|
68
|
-
|
|
69
|
-
|
|
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,
|
|
74
|
-
return this.http.patch<R>(
|
|
75
|
-
|
|
76
|
-
|
|
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,
|
|
81
|
-
return this.http.put<R>(
|
|
82
|
-
|
|
83
|
-
|
|
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,
|
|
88
|
-
return this.http.delete<void>(
|
|
89
|
-
|
|
90
|
-
|
|
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>(
|
|
95
|
-
const {method, url} =
|
|
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(
|
|
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 = (
|
|
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
|
-
|
|
132
|
+
config?: WatchTypesConfig,
|
|
135
133
|
options?: WatchSubscriptionOptions
|
|
136
134
|
): Observable<WatchTypesResult<R>> {
|
|
137
|
-
const iris = (
|
|
138
|
-
const discriminator =
|
|
139
|
-
const allowedTypes = new Set(
|
|
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((
|
|
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
|
-
|
|
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(
|
|
175
|
-
if (!
|
|
176
|
-
const value = (
|
|
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');
|
|
@@ -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,
|
|
33
|
-
return this.repo.getCollection$(query,
|
|
29
|
+
getCollection$(query?: AnyQuery, options?: HttpCallOptions): Observable<Collection<T>> {
|
|
30
|
+
return this.repo.getCollection$(query, options);
|
|
34
31
|
}
|
|
35
32
|
|
|
36
|
-
get$(iri: IriRequired,
|
|
37
|
-
return this.repo.get$(iri,
|
|
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>,
|
|
41
|
-
return this.repo.patch$(iri, changes,
|
|
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>,
|
|
45
|
-
return this.repo.post$(payload,
|
|
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>,
|
|
49
|
-
return this.repo.put$(iri, payload,
|
|
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,
|
|
53
|
-
return this.repo.delete$(iri,
|
|
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>(
|
|
57
|
-
return this.repo.request$<R, B>(
|
|
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
|
-
|
|
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
|
-
|
|
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 = (
|
|
77
|
+
const iris = toIriList(iri);
|
|
85
78
|
return this.realtime
|
|
86
|
-
.subscribe$<R>(iris, {field
|
|
79
|
+
.subscribe$<R>(iris, {field}, options)
|
|
87
80
|
.pipe(
|
|
88
|
-
map(
|
|
89
|
-
filter((
|
|
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 =
|
|
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
|
+
}
|
package/templates/{_lib_template → angular-bridge}/src/lib/ports/resource-repository.port.ts
RENAMED
|
@@ -51,19 +51,19 @@ export interface HttpCallOptions {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
export interface ResourceRepository<T> {
|
|
54
|
-
getCollection$(query?: AnyQuery,
|
|
54
|
+
getCollection$(query?: AnyQuery, options?: HttpCallOptions): Observable<Collection<T>>;
|
|
55
55
|
|
|
56
|
-
get$(iri: IriRequired,
|
|
56
|
+
get$(iri: IriRequired, options?: HttpCallOptions): Observable<T>;
|
|
57
57
|
|
|
58
|
-
post$(payload: Partial<T>,
|
|
58
|
+
post$(payload: Partial<T>, options?: HttpCallOptions): Observable<T>;
|
|
59
59
|
|
|
60
|
-
patch$(iri: IriRequired, changes: Partial<T>,
|
|
60
|
+
patch$(iri: IriRequired, changes: Partial<T>, options?: HttpCallOptions): Observable<T>;
|
|
61
61
|
|
|
62
|
-
put$(iri: IriRequired, payload: Partial<T>,
|
|
62
|
+
put$(iri: IriRequired, payload: Partial<T>, options?: HttpCallOptions): Observable<T>;
|
|
63
63
|
|
|
64
|
-
delete$(iri: IriRequired,
|
|
64
|
+
delete$(iri: IriRequired, options?: HttpCallOptions): Observable<void>;
|
|
65
65
|
|
|
66
|
-
request$<R = unknown, B = unknown>(
|
|
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
|
+
}
|