@obsidiane/meridiane 0.0.6 → 0.0.7

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 (37) hide show
  1. package/README.md +42 -2
  2. package/cli.js +27 -1
  3. package/models.config.example.js +15 -0
  4. package/package.json +1 -3
  5. package/templates/_lib_template/src/lib/bridge/rest/api-platform.adapter.ts +48 -47
  6. package/templates/_lib_template/src/lib/bridge/rest/query-builder.ts +29 -29
  7. package/templates/_lib_template/src/lib/bridge/sse/eventsource-wrapper.ts +50 -54
  8. package/templates/_lib_template/src/lib/bridge/sse/mercure-topic.mapper.ts +48 -0
  9. package/templates/_lib_template/src/lib/bridge/sse/mercure-url.builder.ts +7 -7
  10. package/templates/_lib_template/src/lib/bridge/sse/mercure.adapter.ts +104 -49
  11. package/templates/_lib_template/src/lib/bridge/sse/ref-count-topic.registry.ts +23 -21
  12. package/templates/_lib_template/src/lib/bridge.types.ts +33 -0
  13. package/templates/_lib_template/src/lib/facades/bridge.facade.ts +116 -0
  14. package/templates/_lib_template/src/lib/facades/facade.factory.ts +21 -19
  15. package/templates/_lib_template/src/lib/facades/facade.interface.ts +13 -18
  16. package/templates/_lib_template/src/lib/facades/resource.facade.ts +42 -56
  17. package/templates/_lib_template/src/lib/interceptors/bridge-debug.interceptor.ts +32 -0
  18. package/templates/_lib_template/src/lib/interceptors/bridge-defaults.interceptor.ts +53 -0
  19. package/templates/_lib_template/src/lib/interceptors/content-type.interceptor.ts +36 -37
  20. package/templates/_lib_template/src/lib/ports/realtime.port.ts +5 -3
  21. package/templates/_lib_template/src/lib/ports/resource-repository.port.ts +14 -15
  22. package/templates/_lib_template/src/lib/provide-bridge.ts +122 -19
  23. package/templates/_lib_template/src/lib/tokens.ts +14 -1
  24. package/templates/_lib_template/src/lib/utils/url.ts +15 -0
  25. package/templates/_lib_template/src/public-api.ts +3 -2
  26. package/tools/dev-bridge.js +138 -0
  27. package/tools/generate-lib.js +60 -45
  28. package/tools/generate-models.js +73 -19
  29. package/tools/generator/models/openapi-to-models.js +13 -2
  30. package/tools/generator/models/schema-utils.js +59 -0
  31. package/tools/generator/models/utils.js +8 -2
  32. package/tools/init.js +148 -0
  33. package/tools/sandbox-bridge.js +77 -0
  34. package/tools/utils/cli-args.js +41 -0
  35. package/tools/utils/dotenv.js +55 -0
  36. package/templates/_lib_template/src/lib/bridge/credentials.policy.ts +0 -9
  37. package/templates/_lib_template/src/lib/utils/utils.ts +0 -21
@@ -1,14 +1,15 @@
1
- import {DOCUMENT, Inject, Injectable, OnDestroy, PLATFORM_ID} from '@angular/core';
2
- import {BehaviorSubject, defer, fromEvent, Observable, of, Subject} from 'rxjs';
1
+ import {Inject, Injectable, OnDestroy, Optional, PLATFORM_ID} from '@angular/core';
2
+ import {BehaviorSubject, defer, EMPTY, fromEvent, Observable, of, Subject} from 'rxjs';
3
3
  import {auditTime, concatMap, filter, finalize, map, share, takeUntil} from 'rxjs/operators';
4
- import {API_BASE_URL, MERCURE_CONFIG, MERCURE_HUB_URL} from '../../tokens';
4
+ import {API_BASE_URL, BRIDGE_LOGGER, BRIDGE_WITH_CREDENTIALS, MERCURE_HUB_URL, MERCURE_TOPIC_MODE} from '../../tokens';
5
+ import {BridgeLogger, MercureTopicMode} from '../../bridge.types';
5
6
  import {RealtimeEvent, RealtimePort, RealtimeStatus} from '../../ports/realtime.port';
6
7
  import {EventSourceWrapper} from './eventsource-wrapper';
7
- import {Item} from "../../ports/resource-repository.port";
8
+ import {Item} from '../../ports/resource-repository.port';
8
9
  import {isPlatformBrowser} from '@angular/common';
9
10
  import {MercureUrlBuilder} from './mercure-url.builder';
10
11
  import {RefCountTopicRegistry} from './ref-count-topic.registry';
11
- import {CredentialsPolicy} from '../credentials.policy';
12
+ import {MercureTopicMapper} from './mercure-topic.mapper';
12
13
 
13
14
 
14
15
  @Injectable({providedIn: 'root'})
@@ -19,7 +20,7 @@ export class MercureRealtimeAdapter implements RealtimePort, OnDestroy {
19
20
 
20
21
  private readonly topicsRegistry = new RefCountTopicRegistry();
21
22
  private readonly urlBuilder: MercureUrlBuilder;
22
- private readonly credentialsPolicy: CredentialsPolicy;
23
+ private readonly topicMapper: MercureTopicMapper;
23
24
 
24
25
  private readonly destroy$ = new Subject<void>();
25
26
  private connectionStop$ = new Subject<void>();
@@ -31,13 +32,16 @@ export class MercureRealtimeAdapter implements RealtimePort, OnDestroy {
31
32
 
32
33
  constructor(
33
34
  @Inject(API_BASE_URL) private readonly apiBase: string,
34
- @Inject(MERCURE_CONFIG) private readonly init: string,
35
- @Inject(MERCURE_HUB_URL) private readonly hubUrl: string,
36
- @Inject(DOCUMENT) private readonly doc: Document,
35
+ @Inject(BRIDGE_WITH_CREDENTIALS) private readonly withCredentialsDefault: boolean,
37
36
  @Inject(PLATFORM_ID) private readonly platformId: object,
37
+ @Optional() @Inject(MERCURE_HUB_URL) private readonly hubUrl?: string,
38
+ @Optional() @Inject(MERCURE_TOPIC_MODE) topicMode?: MercureTopicMode,
39
+ @Optional() @Inject(BRIDGE_LOGGER) private readonly logger?: BridgeLogger,
38
40
  ) {
39
- this.urlBuilder = new MercureUrlBuilder(apiBase);
40
- this.credentialsPolicy = new CredentialsPolicy(init);
41
+ this.urlBuilder = new MercureUrlBuilder();
42
+ // `topicMode` affects only the `topic=` query param sent to the hub.
43
+ // Payload IRIs are always matched using same-origin relative IRIs (`/api/...`) when possible.
44
+ this.topicMapper = new MercureTopicMapper(apiBase, topicMode ?? 'url');
41
45
 
42
46
  this.rebuild$
43
47
  .pipe(
@@ -65,42 +69,54 @@ export class MercureRealtimeAdapter implements RealtimePort, OnDestroy {
65
69
 
66
70
 
67
71
  subscribe$<T>(iris: string[], _filter?: { field?: string }): Observable<RealtimeEvent<T>> {
68
- if (iris.length === 0) {
69
- return new Observable<RealtimeEvent<T>>((sub) => sub.complete());
70
- }
71
-
72
- this.topicsRegistry.addAll(iris);
73
- this.scheduleRebuild();
74
-
75
- const filterSet = new Set(iris);
76
- const fieldPath = _filter?.field;
72
+ return defer(() => {
73
+ const inputIris = iris.filter((v): v is string => typeof v === 'string' && v.length > 0);
74
+ if (inputIris.length === 0) return EMPTY;
77
75
 
78
- return this.incoming$.pipe(
79
- map((evt) => this.safeParse(evt.data)),
76
+ if (!this.hubUrl) {
77
+ this.logger?.debug?.('[Mercure] hubUrl not configured → realtime disabled');
78
+ return EMPTY;
79
+ }
80
80
 
81
- filter((raw: any) => {
82
- if (fieldPath) {
83
- const relIri = this.extractRelationIris(raw, fieldPath);
84
- return Array.from(filterSet).some((iri) => relIri === iri || relIri?.startsWith(`${iri}/`));
85
- }
86
- const id = raw?.['@id'];
87
- return typeof id === 'string' && Array.from(filterSet).some((iri) => {
88
- const normalized = iri.endsWith('/') ? iri.slice(0, -1) : iri;
89
- return id === normalized || id.startsWith(`${normalized}/`);
90
- });
91
- }),
92
- map((payload) => ({iri: payload['@id'] as string, data: payload as T})),
93
- finalize(() => {
94
- this.topicsRegistry.removeAll(iris);
95
- this.scheduleRebuild();
96
- }),
97
- share()
98
- );
81
+ // Canonicalise topics (ref-count + URL) to avoid duplicates like:
82
+ // - "/api/conversations/1" and "http://localhost:8000/api/conversations/1"
83
+ const registeredTopics = Array.from(new Set(inputIris.map((i) => this.topicMapper.toTopic(i))));
84
+
85
+ this.topicsRegistry.addAll(registeredTopics);
86
+ this.scheduleRebuild();
87
+
88
+ // Matching is done against the payload IRIs (typically "/api/...").
89
+ const subscribed = inputIris.map((i) => normalizeIri(this.topicMapper.toPayloadIri(i)));
90
+ const fieldPath = _filter?.field;
91
+
92
+ return this.incoming$.pipe(
93
+ map((evt) => this.safeParse(evt.data)),
94
+ filter((raw): raw is Item => !!raw),
95
+
96
+ filter((raw: any) => {
97
+ if (fieldPath) {
98
+ const relIris = this.extractRelationIris(raw, fieldPath).map((i) => normalizeIri(this.topicMapper.toPayloadIri(i)));
99
+ return relIris.some((relIri) => matchesAnySubscribed(relIri, subscribed));
100
+ }
101
+ const rawId = raw?.['@id'];
102
+ const id = typeof rawId === 'string' ? normalizeIri(this.topicMapper.toPayloadIri(rawId)) : undefined;
103
+ return typeof id === 'string' && matchesAnySubscribed(id, subscribed);
104
+ }),
105
+ map((payload) => ({iri: payload['@id'] as string, data: payload as T})),
106
+ finalize(() => {
107
+ this.topicsRegistry.removeAll(registeredTopics);
108
+ this.scheduleRebuild();
109
+ }),
110
+ share()
111
+ );
112
+ });
99
113
  }
100
114
 
101
115
  unsubscribe(iris: string[]): void {
102
- if (!iris || iris.length === 0) return;
103
- this.topicsRegistry.removeAll(iris);
116
+ const inputIris = iris.filter((v): v is string => typeof v === 'string' && v.length > 0);
117
+ if (inputIris.length === 0) return;
118
+ const topics = Array.from(new Set(inputIris.map((i) => this.topicMapper.toTopic(i))));
119
+ this.topicsRegistry.removeAll(topics);
104
120
  this.scheduleRebuild();
105
121
  }
106
122
 
@@ -131,8 +147,14 @@ export class MercureRealtimeAdapter implements RealtimePort, OnDestroy {
131
147
  if (this.shuttingDown) return of(void 0);
132
148
 
133
149
  try {
150
+ if (!this.hubUrl) {
151
+ this.currentKey = undefined;
152
+ this._status$.next('closed');
153
+ return of(void 0);
154
+ }
155
+
134
156
  const hasTopics = this.topicsRegistry.hasAny();
135
- const key = hasTopics ? this.topicsRegistry.computeKey(this.hubUrl, this.credentialsPolicy.withCredentials()) : undefined;
157
+ const key = hasTopics ? this.topicsRegistry.computeKey(this.hubUrl, this.withCredentialsDefault) : undefined;
136
158
 
137
159
  if (!hasTopics) {
138
160
  if (this.es) this.teardownConnection();
@@ -148,7 +170,8 @@ export class MercureRealtimeAdapter implements RealtimePort, OnDestroy {
148
170
  this.teardownConnection();
149
171
 
150
172
  const url = this.urlBuilder.build(this.hubUrl, this.topicsRegistry.snapshot(), this.lastEventId);
151
- this.es = new EventSourceWrapper(url, {withCredentials: this.credentialsPolicy.withCredentials()});
173
+ this.logger?.debug?.('[Mercure] connect', {hubUrl: this.hubUrl, topics: this.topicsRegistry.snapshot(), lastEventId: this.lastEventId});
174
+ this.es = new EventSourceWrapper(url, {withCredentials: this.withCredentialsDefault}, this.logger);
152
175
 
153
176
  this.connectionStop$ = new Subject<void>();
154
177
 
@@ -167,7 +190,7 @@ export class MercureRealtimeAdapter implements RealtimePort, OnDestroy {
167
190
  this.es.open();
168
191
  this.currentKey = key;
169
192
  } catch (err) {
170
- console.error('[Mercure] rebuild failed:', err);
193
+ this.logger?.error?.('[Mercure] rebuild failed:', err);
171
194
  this.currentKey = undefined;
172
195
  this._status$.next(this.topicsRegistry.hasAny() ? 'connecting' : 'closed');
173
196
  }
@@ -195,12 +218,44 @@ export class MercureRealtimeAdapter implements RealtimePort, OnDestroy {
195
218
  this._status$.next(this.topicsRegistry.hasAny() ? 'connecting' : 'closed');
196
219
  }
197
220
 
198
- private safeParse(raw: string): Item {
199
- return JSON.parse(raw) as Item;
221
+ private safeParse(raw: string): Item | undefined {
222
+ try {
223
+ return JSON.parse(raw) as Item;
224
+ } catch (err) {
225
+ this.logger?.debug?.('[Mercure] invalid JSON payload ignored', {raw});
226
+ return undefined;
227
+ }
200
228
  }
201
229
 
202
- private extractRelationIris(raw: any, path: string): string {
203
- return raw?.[path];
230
+ private extractRelationIris(raw: any, path: string): string[] {
231
+ const readPath = (obj: any, dotPath: string): any => {
232
+ return dotPath
233
+ .split('.')
234
+ .filter(Boolean)
235
+ .reduce((acc, key) => acc?.[key], obj);
236
+ };
237
+
238
+ const normalize = (value: any): string[] => {
239
+ if (!value) return [];
240
+ if (typeof value === 'string') return value.length > 0 ? [value] : [];
241
+ if (typeof value === 'object' && typeof value['@id'] === 'string') return [value['@id']];
242
+ if (Array.isArray(value)) return value.flatMap(normalize);
243
+ return [];
244
+ };
245
+
246
+ return normalize(readPath(raw, path));
204
247
  }
205
248
 
206
249
  }
250
+
251
+ function normalizeIri(iri: string): string {
252
+ return iri.endsWith('/') ? iri.slice(0, -1) : iri;
253
+ }
254
+
255
+ function matchesAnySubscribed(candidate: string, subscribed: string[]): boolean {
256
+ for (const iri of subscribed) {
257
+ if (candidate === iri) return true;
258
+ if (candidate.startsWith(`${iri}/`)) return true;
259
+ }
260
+ return false;
261
+ }
@@ -1,43 +1,45 @@
1
1
  export class RefCountTopicRegistry {
2
- private readonly iris = new Set<string>();
2
+ private readonly topics = new Set<string>();
3
3
  private readonly refCounts = new Map<string, number>();
4
4
 
5
-
6
- addAll(iris: Iterable<string>): void {
7
- for (const iri of iris) {
8
- const next = (this.refCounts.get(iri) ?? 0) + 1;
9
- this.refCounts.set(iri, next);
10
- this.iris.add(iri);
5
+ /**
6
+ * Increments ref-count for each topic.
7
+ * Callers should pass unique topic strings (deduped).
8
+ */
9
+ addAll(topics: Iterable<string>): void {
10
+ for (const topic of topics) {
11
+ const next = (this.refCounts.get(topic) ?? 0) + 1;
12
+ this.refCounts.set(topic, next);
13
+ this.topics.add(topic);
11
14
  }
12
15
  }
13
16
 
14
-
15
- removeAll(iris: Iterable<string>): void {
16
- for (const iri of iris) {
17
- const next = (this.refCounts.get(iri) ?? 0) - 1;
17
+ /**
18
+ * Decrements ref-count for each topic.
19
+ * Callers should pass unique topic strings (deduped).
20
+ */
21
+ removeAll(topics: Iterable<string>): void {
22
+ for (const topic of topics) {
23
+ const next = (this.refCounts.get(topic) ?? 0) - 1;
18
24
  if (next <= 0) {
19
- this.refCounts.delete(iri);
20
- this.iris.delete(iri);
25
+ this.refCounts.delete(topic);
26
+ this.topics.delete(topic);
21
27
  } else {
22
- this.refCounts.set(iri, next);
28
+ this.refCounts.set(topic, next);
23
29
  }
24
30
  }
25
31
  }
26
32
 
27
-
28
33
  hasAny(): boolean {
29
- return this.iris.size > 0;
34
+ return this.topics.size > 0;
30
35
  }
31
36
 
32
-
33
37
  snapshot(): ReadonlySet<string> {
34
- return new Set(this.iris);
38
+ return new Set(this.topics);
35
39
  }
36
40
 
37
-
38
-
39
41
  computeKey(hubUrl: string, credentialsOn: boolean): string {
40
- const topicsSorted = Array.from(this.iris).sort().join('|');
42
+ const topicsSorted = Array.from(this.topics).sort().join('|');
41
43
  return `${hubUrl}::${credentialsOn ? '1' : '0'}::${topicsSorted}`;
42
44
  }
43
45
  }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Public configuration types for the bridge.
3
+ *
4
+ * Keep this file dependency-free (no Angular imports) so it can be used from both
5
+ * runtime code and type-only contexts.
6
+ */
7
+
8
+ export interface BridgeLogger {
9
+ debug: (...args: unknown[]) => void;
10
+ info: (...args: unknown[]) => void;
11
+ warn: (...args: unknown[]) => void;
12
+ error: (...args: unknown[]) => void;
13
+ }
14
+
15
+ export interface BridgeDefaults {
16
+ headers?: Record<string, string>;
17
+ timeoutMs?: number;
18
+ retries?:
19
+ | number
20
+ | {
21
+ count: number;
22
+ delayMs?: number;
23
+ methods?: string[];
24
+ };
25
+ }
26
+
27
+ /**
28
+ * Controls how Mercure topics are sent in the hub URL.
29
+ * - `url`: topics are absolute URLs (recommended default)
30
+ * - `iri`: topics are same-origin relative IRIs (e.g. `/api/...`)
31
+ */
32
+ export type MercureTopicMode = 'url' | 'iri';
33
+
@@ -0,0 +1,116 @@
1
+ import {Inject, Injectable} from '@angular/core';
2
+ import {HttpClient} from '@angular/common/http';
3
+ import {Observable} from 'rxjs';
4
+ import {filter, map, share} from 'rxjs/operators';
5
+ import {toHttpParams} from '../bridge/rest/query-builder';
6
+ import {MercureRealtimeAdapter} from '../bridge/sse/mercure.adapter';
7
+ import {API_BASE_URL, BRIDGE_WITH_CREDENTIALS} from '../tokens';
8
+ import {AnyQuery, Collection, HttpCallOptions, HttpRequestConfig, Iri, IriRequired, Item} from '../ports/resource-repository.port';
9
+ import {SubscribeFilter} from '../ports/realtime.port';
10
+ import {resolveUrl} from '../utils/url';
11
+
12
+ /**
13
+ * High-level facade for ad-hoc HTTP calls and Mercure subscriptions.
14
+ *
15
+ * Prefer `FacadeFactory` + `ResourceFacade<T>` when you want a resource-oriented API.
16
+ */
17
+ @Injectable({providedIn: 'root'})
18
+ export class BridgeFacade {
19
+ constructor(
20
+ private readonly http: HttpClient,
21
+ private readonly realtime: MercureRealtimeAdapter,
22
+ @Inject(API_BASE_URL) private readonly apiBase: string,
23
+ @Inject(BRIDGE_WITH_CREDENTIALS) private readonly withCredentialsDefault: boolean,
24
+ ) {
25
+ }
26
+
27
+ // ──────────────── HTTP ────────────────
28
+
29
+ get$<R = unknown>(url: IriRequired, opts?: HttpCallOptions): Observable<R> {
30
+ return this.http.get<R>(this.resolveUrl(url), {
31
+ headers: opts?.headers,
32
+ withCredentials: opts?.withCredentials ?? this.withCredentialsDefault,
33
+ });
34
+ }
35
+
36
+ getCollection$<T extends Item = Item>(
37
+ url: IriRequired,
38
+ query?: AnyQuery,
39
+ opts?: HttpCallOptions
40
+ ): Observable<Collection<T>> {
41
+ const params = toHttpParams(query);
42
+ return this.http.get<Collection<T>>(this.resolveUrl(url), {
43
+ params,
44
+ headers: opts?.headers,
45
+ withCredentials: opts?.withCredentials ?? this.withCredentialsDefault,
46
+ });
47
+ }
48
+
49
+ post$<R = unknown, B = unknown>(url: IriRequired, payload: B, opts?: HttpCallOptions): Observable<R> {
50
+ return this.http.post<R>(this.resolveUrl(url), payload, {
51
+ headers: opts?.headers,
52
+ withCredentials: opts?.withCredentials ?? this.withCredentialsDefault,
53
+ });
54
+ }
55
+
56
+ patch$<R = unknown, B = unknown>(url: IriRequired, changes: B, opts?: HttpCallOptions): Observable<R> {
57
+ return this.http.patch<R>(this.resolveUrl(url), changes, {
58
+ headers: opts?.headers,
59
+ withCredentials: opts?.withCredentials ?? this.withCredentialsDefault,
60
+ });
61
+ }
62
+
63
+ put$<R = unknown, B = unknown>(url: IriRequired, payload: B, opts?: HttpCallOptions): Observable<R> {
64
+ return this.http.put<R>(this.resolveUrl(url), payload, {
65
+ headers: opts?.headers,
66
+ withCredentials: opts?.withCredentials ?? this.withCredentialsDefault,
67
+ });
68
+ }
69
+
70
+ delete$(url: IriRequired, opts?: HttpCallOptions): Observable<void> {
71
+ return this.http.delete<void>(this.resolveUrl(url), {
72
+ headers: opts?.headers,
73
+ withCredentials: opts?.withCredentials ?? this.withCredentialsDefault,
74
+ });
75
+ }
76
+
77
+ request$<R = unknown, B = unknown>(req: HttpRequestConfig<B>): Observable<R> {
78
+ const {method, url, query, body, headers, responseType, withCredentials, options = {}} = req;
79
+ const targetUrl = this.resolveUrl(url);
80
+
81
+ const mergedOptions: Record<string, unknown> = {...options};
82
+ if (headers) mergedOptions['headers'] = headers;
83
+ if (query) mergedOptions['params'] = toHttpParams(query);
84
+ if (body !== undefined) mergedOptions['body'] = body;
85
+
86
+ mergedOptions['responseType'] = (responseType ?? (mergedOptions['responseType'] as any) ?? 'json') as any;
87
+ mergedOptions['withCredentials'] =
88
+ withCredentials ?? (mergedOptions['withCredentials'] as any) ?? this.withCredentialsDefault;
89
+ mergedOptions['observe'] = 'body';
90
+
91
+ return this.http.request<R>(method, targetUrl, mergedOptions as {observe: 'body'});
92
+ }
93
+
94
+ // ──────────────── SSE / Mercure ────────────────
95
+
96
+ watch$<T = Item>(iri: Iri | Iri[], subscribeFilter?: SubscribeFilter): Observable<T> {
97
+ const iris = (Array.isArray(iri) ? iri : [iri]).filter((v): v is string => typeof v === 'string' && v.length > 0);
98
+ return this.realtime
99
+ .subscribe$<T>(iris, subscribeFilter)
100
+ .pipe(
101
+ map((event) => event.data),
102
+ filter((data): data is T => data !== undefined),
103
+ share()
104
+ );
105
+ }
106
+
107
+ unwatch(iri: Iri | Iri[]): void {
108
+ const iris = (Array.isArray(iri) ? iri : [iri]).filter((v): v is string => typeof v === 'string' && v.length > 0);
109
+ this.realtime.unsubscribe(iris);
110
+ }
111
+
112
+ private resolveUrl(path?: Iri): string {
113
+ if (!path) throw new Error('BridgeFacade: missing url');
114
+ return resolveUrl(this.apiBase, path);
115
+ }
116
+ }
@@ -1,11 +1,11 @@
1
- import {Injectable, Inject, runInInjectionContext, inject, EnvironmentInjector} from '@angular/core';
1
+ import {EnvironmentInjector, inject, Injectable, runInInjectionContext} from '@angular/core';
2
2
  import {HttpClient} from '@angular/common/http';
3
3
  import {RealtimePort} from '../ports/realtime.port';
4
4
  import {Item, ResourceRepository} from '../ports/resource-repository.port';
5
- import {API_BASE_URL, MERCURE_CONFIG} from "../tokens";
6
- import {MercureRealtimeAdapter} from "../bridge/sse/mercure.adapter";
7
- import {ApiPlatformRestRepository} from "../bridge/rest/api-platform.adapter";
8
- import {ResourceFacade} from "./resource.facade";
5
+ import {API_BASE_URL, BRIDGE_WITH_CREDENTIALS} from '../tokens';
6
+ import {MercureRealtimeAdapter} from '../bridge/sse/mercure.adapter';
7
+ import {ApiPlatformRestRepository} from '../bridge/rest/api-platform.adapter';
8
+ import {ResourceFacade} from './resource.facade';
9
9
 
10
10
  export type FacadeConfig<T> = {
11
11
  url: string;
@@ -16,21 +16,23 @@ export type FacadeConfig<T> = {
16
16
 
17
17
  @Injectable({providedIn: 'root'})
18
18
  export class FacadeFactory {
19
+ private readonly env = inject(EnvironmentInjector);
20
+ private readonly http = inject(HttpClient);
21
+ private readonly baseUrl = inject(API_BASE_URL);
22
+ private readonly withCredentials = inject(BRIDGE_WITH_CREDENTIALS);
23
+ private readonly mercure = inject(MercureRealtimeAdapter);
19
24
 
20
- private env = inject(EnvironmentInjector);
21
-
22
- constructor(
23
- private readonly http: HttpClient,
24
- @Inject(API_BASE_URL) private readonly baseUrl: string,
25
- @Inject(MERCURE_CONFIG) private readonly init: string,
26
- private readonly mercureAny: MercureRealtimeAdapter
27
- ) {
28
- }
29
-
25
+ /**
26
+ * Creates a `ResourceFacade<T>`.
27
+ *
28
+ * Important: `ResourceFacade` uses `toSignal()`, which requires an injection context.
29
+ * This factory ensures that by using `runInInjectionContext`.
30
+ */
30
31
  create<T extends Item>(config: FacadeConfig<T>): ResourceFacade<T> {
31
- const path = config.url;
32
- const repo = new ApiPlatformRestRepository<T>(this.http, this.baseUrl, path , this.init);
33
- const realtime = this.mercureAny as MercureRealtimeAdapter;
34
- return runInInjectionContext(this.env, () => new ResourceFacade<T>(repo, realtime, path));
32
+ const url = config.url;
33
+ const repo = config.repo ?? new ApiPlatformRestRepository<T>(this.http, this.baseUrl, url, this.withCredentials);
34
+ const realtime = config.realtime ?? this.mercure;
35
+
36
+ return runInInjectionContext(this.env, () => new ResourceFacade<T>(repo, realtime));
35
37
  }
36
38
  }
@@ -1,35 +1,30 @@
1
1
  import {
2
2
  Collection,
3
- CreateCommand,
4
3
  HttpRequestConfig,
5
4
  Iri,
5
+ IriRequired,
6
6
  Item,
7
- Query,
8
- UpdateCommand
9
- } from "../ports/resource-repository.port";
10
- import {Observable} from "rxjs";
7
+ HttpCallOptions,
8
+ AnyQuery,
9
+ } from '../ports/resource-repository.port';
10
+ import {Observable} from 'rxjs';
11
11
 
12
12
  export interface Facade<T extends Item> {
13
+ getCollection$(query?: AnyQuery, opts?: HttpCallOptions): Observable<Collection<T>>;
13
14
 
14
- list$(query?: Query): Observable<Collection<T>>;
15
+ get$(iri: IriRequired, opts?: HttpCallOptions): Observable<T>;
15
16
 
16
- get$(iri: Iri): Observable<T>;
17
+ post$(payload: Partial<T>, opts?: HttpCallOptions): Observable<T>;
17
18
 
18
- create$(cmd: CreateCommand<T>): Observable<T>;
19
+ patch$(iri: IriRequired, changes: Partial<T>, opts?: HttpCallOptions): Observable<T>;
19
20
 
20
- update$(cmd: UpdateCommand<T>): Observable<T>;
21
+ put$(iri: IriRequired, payload: Partial<T>, opts?: HttpCallOptions): Observable<T>;
21
22
 
22
- delete$(iri: Iri): Observable<void>;
23
-
24
- post$<R = unknown, B = unknown>(req: RestRequest<B>): Observable<R>;
25
- put$<R = unknown, B = unknown>(req: RestRequest<B>): Observable<R>;
26
- patch$<R = unknown, B = unknown>(req: RestRequest<B>): Observable<R>;
23
+ delete$(iri: IriRequired, opts?: HttpCallOptions): Observable<void>;
27
24
 
28
25
  request$<R = unknown, B = unknown>(req: HttpRequestConfig<B>): Observable<R>;
29
26
 
30
- watch$(iri: Iri|Iri[]): Observable<T>;
27
+ watch$(iri: Iri | Iri[]): Observable<T>;
31
28
 
32
- unwatch(iri: Iri|Iri[]): void;
29
+ unwatch(iri: Iri | Iri[]): void;
33
30
  }
34
-
35
- export type RestRequest<B> = Omit<HttpRequestConfig<B>, 'method'>;