@obsidiane/meridiane 2.0.12 → 3.0.0

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 (58) hide show
  1. package/README.md +30 -68
  2. package/cli.js +1 -10
  3. package/package.json +1 -1
  4. package/src/application/bridge-workflow.js +23 -66
  5. package/src/application/generate-bridge.js +32 -36
  6. package/src/application/run-build.js +8 -30
  7. package/src/application/run-dev.js +8 -63
  8. package/src/application/run-generate.js +8 -47
  9. package/src/domain/bridge-options.js +10 -63
  10. package/src/generators/bridge/write-bridge-files.js +11 -29
  11. package/src/generators/models/interface-schema.js +3 -20
  12. package/src/generators/models/model-selection.js +67 -106
  13. package/src/generators/models/oas-contract.js +1 -108
  14. package/src/generators/models/oas-schema-name.js +1 -19
  15. package/src/generators/models/openapi-to-models.js +51 -42
  16. package/src/generators/models/schema-utils.js +0 -34
  17. package/src/generators/models/type-resolver.js +2 -20
  18. package/src/generators/models/utils.js +0 -11
  19. package/src/generators/models/write-model-files.js +5 -15
  20. package/src/infrastructure/dev-project.js +0 -33
  21. package/src/infrastructure/files.js +15 -0
  22. package/src/infrastructure/local-package-install.js +2 -2
  23. package/src/infrastructure/logger.js +0 -2
  24. package/templates/angular-bridge/README.md +29 -281
  25. package/templates/angular-bridge/package.json +2 -2
  26. package/templates/angular-bridge/src/lib/bridge/rest/http-request.options.ts +10 -19
  27. package/templates/angular-bridge/src/lib/bridge/sse/mercure-hub.ts +449 -0
  28. package/templates/angular-bridge/src/lib/bridge/sse/mercure-url.ts +28 -0
  29. package/templates/angular-bridge/src/lib/bridge/sse/resource-topic.ts +83 -0
  30. package/templates/angular-bridge/src/lib/bridge-options.ts +21 -17
  31. package/templates/angular-bridge/src/lib/bridge.types.ts +22 -33
  32. package/templates/angular-bridge/src/lib/facades/bridge.facade.ts +58 -134
  33. package/templates/angular-bridge/src/lib/facades/facade.factory.ts +25 -25
  34. package/templates/angular-bridge/src/lib/facades/facade.interface.ts +4 -9
  35. package/templates/angular-bridge/src/lib/facades/resource.facade.ts +38 -61
  36. package/templates/angular-bridge/src/lib/interceptors/bearer-auth.interceptor.ts +8 -12
  37. package/templates/angular-bridge/src/lib/interceptors/bridge-defaults.interceptor.ts +19 -11
  38. package/templates/angular-bridge/src/lib/interceptors/content-type.interceptor.ts +5 -17
  39. package/templates/angular-bridge/src/lib/interceptors/singleflight.interceptor.ts +28 -24
  40. package/templates/angular-bridge/src/lib/ports/realtime.port.ts +34 -53
  41. package/templates/angular-bridge/src/lib/ports/resource-repository.port.ts +31 -21
  42. package/templates/angular-bridge/src/lib/provide-bridge.ts +14 -38
  43. package/templates/angular-bridge/src/lib/tokens.ts +4 -15
  44. package/templates/angular-bridge/src/lib/utils/request-url.ts +4 -6
  45. package/templates/angular-bridge/src/lib/utils/url.ts +32 -9
  46. package/templates/angular-bridge/src/public-api.ts +0 -1
  47. package/templates/angular-bridge/tsconfig.lib.json +0 -3
  48. package/src/infrastructure/json.js +0 -71
  49. package/templates/angular-bridge/src/lib/bridge/rest/api-platform.adapter.ts +0 -82
  50. package/templates/angular-bridge/src/lib/bridge/sse/eventsource-wrapper.ts +0 -70
  51. package/templates/angular-bridge/src/lib/bridge/sse/mercure-connections.ts +0 -255
  52. package/templates/angular-bridge/src/lib/bridge/sse/mercure-payload.ts +0 -82
  53. package/templates/angular-bridge/src/lib/bridge/sse/mercure-topic.mapper.ts +0 -48
  54. package/templates/angular-bridge/src/lib/bridge/sse/mercure-url.builder.ts +0 -39
  55. package/templates/angular-bridge/src/lib/bridge/sse/mercure.adapter.ts +0 -358
  56. package/templates/angular-bridge/src/lib/bridge/sse/ref-count-topic.registry.ts +0 -36
  57. package/templates/angular-bridge/src/lib/bridge/sse/subscription-claims.ts +0 -76
  58. package/templates/angular-bridge/src/lib/utils/iri.ts +0 -6
@@ -1,6 +1,3 @@
1
- /** Hydra base schemas to ignore in safe `allOf` merges */
2
- const HYDRA_BASE_RE = /^Hydra(?:Item|Collection)BaseSchema$/;
3
-
4
1
  /**
5
2
  * Détermine si une valeur est un objet simple (non tableau)
6
3
  * @param {unknown} v
@@ -40,34 +37,3 @@ export function schemaNameFromRef($ref) {
40
37
  const match = String($ref ?? '').match(/^#\/components\/schemas\/([^/]+)$/);
41
38
  return match ? decodeJsonPointerSegment(match[1]) : undefined;
42
39
  }
43
-
44
- /**
45
- * Essaie d'aplatir un allOf en un objet unique.
46
- * Ignore HydraItemBaseSchema/HydraCollectionBaseSchema quand rencontré.
47
- * Retourne null si l'aplatissement n'est pas possible de manière sûre.
48
- * @param {any[]} schemas
49
- * @returns {any|null}
50
- */
51
- export function mergeAllOf(schemas) {
52
- const out = {type: 'object', properties: {}, required: []};
53
- for (const s of schemas) {
54
- if (s.$ref) {
55
- const ref = schemaNameFromRef(s.$ref);
56
- if (ref && HYDRA_BASE_RE.test(ref)) {
57
- continue;
58
- }
59
- return null;
60
- }
61
- if (s.type === 'object') {
62
- for (const [name, property] of Object.entries(s.properties ?? {})) {
63
- if (hasOwn(out.properties, name)) return null;
64
- out.properties[name] = property;
65
- }
66
- if (Array.isArray(s.required)) out.required.push(...s.required);
67
- } else {
68
- return null;
69
- }
70
- }
71
- out.required = [...new Set(out.required)];
72
- return out;
73
- }
@@ -53,7 +53,6 @@ function nonNegativeInteger(value) {
53
53
  export function createTypeResolver({
54
54
  dependencies = new Set(),
55
55
  typeNames = new Map(),
56
- requiredMode = 'all',
57
56
  spec,
58
57
  } = {}) {
59
58
  const resolvingRefs = new Set();
@@ -81,11 +80,6 @@ export function createTypeResolver({
81
80
  }
82
81
  }
83
82
 
84
- function resolveProperty(schema) {
85
- const type = resolve(schema);
86
- return requiredMode === 'spec' ? type : nullableType(type);
87
- }
88
-
89
83
  function resolveDictionaryValue(additionalProperties, properties) {
90
84
  const additionalType = additionalProperties === true
91
85
  ? atomicType('any')
@@ -101,8 +95,8 @@ export function createTypeResolver({
101
95
  const requiredProperties = new Set(schema.required ?? []);
102
96
  const properties = Object.entries(schema.properties ?? {}).map(([key, propertySchema]) => ({
103
97
  key,
104
- optional: requiredMode !== 'spec' || !requiredProperties.has(key),
105
- type: resolveProperty(propertySchema),
98
+ optional: !requiredProperties.has(key),
99
+ type: resolve(propertySchema),
106
100
  }));
107
101
  const members = properties.map(({key, optional, type}) =>
108
102
  `${quoteKeyIfNeeded(key)}${optional ? '?' : ''}: ${type.code};`
@@ -224,17 +218,5 @@ export function createTypeResolver({
224
218
 
225
219
  return {
226
220
  typeOf: (schema) => resolve(schema).code,
227
- nullableTypeOf: (schema) => nullableType(resolve(schema)).code,
228
221
  };
229
222
  }
230
-
231
- /**
232
- * Resolves an OpenAPI schema to TypeScript and collects component dependencies.
233
- */
234
- export function tsTypeOf(schema, dependencies, typeNames, options) {
235
- return createTypeResolver({dependencies, typeNames, ...options}).typeOf(schema);
236
- }
237
-
238
- export function nullableTsTypeOf(schema, dependencies, typeNames, options) {
239
- return createTypeResolver({dependencies, typeNames, ...options}).nullableTypeOf(schema);
240
- }
@@ -1,14 +1,3 @@
1
- import fs from 'node:fs/promises';
2
-
3
- /**
4
- * Supprime et recrée un dossier (idempotent)
5
- * @param {string} dir
6
- */
7
- export async function ensureCleanDir(dir) {
8
- await fs.rm(dir, { recursive: true, force: true });
9
- await fs.mkdir(dir, { recursive: true });
10
- }
11
-
12
1
  const RESERVED = new Set([
13
2
  'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do',
14
3
  'else', 'enum', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', 'import',
@@ -1,30 +1,20 @@
1
1
  import path from 'node:path';
2
2
  import { fileURLToPath } from 'node:url';
3
3
 
4
+ import { ensureCleanDir } from '../../infrastructure/files.js';
4
5
  import { renderTemplateToFile } from './handlebars.js';
5
6
  import { buildModelsFromOpenAPI } from './openapi-to-models.js';
6
- import { ensureCleanDir } from './utils.js';
7
7
 
8
8
  const currentDir = path.dirname(fileURLToPath(import.meta.url));
9
9
  const templatesDir = path.join(currentDir, 'templates');
10
10
 
11
- export async function writeModelFiles({ projectDir, spec, requiredMode, formats, include, exclude, debug, log }) {
11
+ export async function writeModelFiles({ projectDir, spec, include, exclude, log }) {
12
12
  const outputDir = path.resolve(projectDir, 'src', 'models');
13
- if (debug) {
14
- log?.debug?.('[meridiane] generate models', {
15
- outputDir,
16
- requiredMode,
17
- formats: Array.isArray(formats) ? formats : [],
18
- includeCount: include.length,
19
- excludeCount: exclude.length,
20
- });
21
- }
13
+ log.debug('[meridiane] generate models', { outputDir, includeCount: include.length, excludeCount: exclude.length });
22
14
 
23
15
  await ensureCleanDir(outputDir);
24
16
 
25
17
  const { models } = buildModelsFromOpenAPI(spec, {
26
- requiredMode,
27
- formats: Array.isArray(formats) ? formats : [],
28
18
  includeSchemaNames: include,
29
19
  excludeSchemaNames: exclude,
30
20
  });
@@ -37,7 +27,7 @@ export async function writeModelFiles({ projectDir, spec, requiredMode, formats,
37
27
  name: model.name,
38
28
  props: model.props,
39
29
  imports: model.imports,
40
- extendsTypes: model.extendsTypes || [],
30
+ extendsTypes: model.extendsTypes,
41
31
  extendsItem: model.extendsItem === true,
42
32
  type: model.type,
43
33
  separator: model.imports.length > 0 ? '\n' : '',
@@ -51,5 +41,5 @@ export async function writeModelFiles({ projectDir, spec, requiredMode, formats,
51
41
  ctx: { models: models.map(({ name }) => ({ name })) },
52
42
  });
53
43
 
54
- if (debug) log?.debug?.(`[meridiane] ${models.length} model(s) generated`);
44
+ log.debug(`[meridiane] ${models.length} model(s) generated`);
55
45
  }
@@ -1,8 +1,6 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
 
4
- const SANDBOX_PACKAGE_NAME = '@obsidiane/meridiane-sandbox';
5
-
6
4
  async function isFile(filePath) {
7
5
  try {
8
6
  return (await fs.stat(filePath)).isFile();
@@ -11,38 +9,7 @@ async function isFile(filePath) {
11
9
  }
12
10
  }
13
11
 
14
- async function readPackageName(projectRoot) {
15
- try {
16
- const packageJson = JSON.parse(await fs.readFile(path.join(projectRoot, 'package.json'), 'utf8'));
17
- return typeof packageJson.name === 'string' ? packageJson.name : undefined;
18
- } catch {
19
- return undefined;
20
- }
21
- }
22
-
23
- async function isSandboxProject(projectRoot) {
24
- return (await readPackageName(projectRoot)) === SANDBOX_PACKAGE_NAME;
25
- }
26
-
27
12
  export async function resolveDevAppRoot(startDir) {
28
13
  if (await isFile(path.join(startDir, 'package.json'))) return startDir;
29
14
  throw new Error('No package.json found. Run `meridiane dev` from your Angular app root.');
30
15
  }
31
-
32
- export async function resolveSandboxAppRoot(startDir) {
33
- if (await isSandboxProject(startDir)) return startDir;
34
-
35
- const playgroundRoot = path.join(startDir, 'playground', 'angular-app');
36
- if (await isSandboxProject(playgroundRoot)) return playgroundRoot;
37
-
38
- throw new Error(
39
- 'Missing <packageName>. Run from the playground app (`playground/angular-app`) or provide a package name.'
40
- );
41
- }
42
-
43
- export async function resolveSpecSource({useSandboxDefaults, appRoot, specArg}) {
44
- if (!useSandboxDefaults || specArg) return specArg;
45
-
46
- const localSpec = path.resolve(appRoot, '..', 'api-platform', 'var', 'openapi.json');
47
- return (await isFile(localSpec)) ? localSpec : 'http://localhost:8000/api/docs.json';
48
- }
@@ -0,0 +1,15 @@
1
+ import fs from 'node:fs/promises';
2
+
3
+ export async function readJson(filePath) {
4
+ return JSON.parse(await fs.readFile(filePath, 'utf8'));
5
+ }
6
+
7
+ export async function writeJson(filePath, data) {
8
+ await fs.writeFile(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8');
9
+ }
10
+
11
+ /** Removes then recreates a directory. */
12
+ export async function ensureCleanDir(dir) {
13
+ await fs.rm(dir, { recursive: true, force: true });
14
+ await fs.mkdir(dir, { recursive: true });
15
+ }
@@ -38,7 +38,7 @@ export async function installPackageFromDist({packageName, distDir, appRoot, log
38
38
  const packageDir = scope ? path.join(nodeModules, scope, name) : path.join(nodeModules, name);
39
39
  const displayName = scope ? `${scope}/${name}` : name;
40
40
 
41
- log?.step?.(`installation locale dans node_modules (${displayName})`);
41
+ log.step(`installation locale dans node_modules (${displayName})`);
42
42
 
43
43
  await fs.rm(packageDir, {recursive: true, force: true});
44
44
  await fs.mkdir(path.dirname(packageDir), {recursive: true});
@@ -71,7 +71,7 @@ async function invalidateAngularViteCache({appRoot, packageName, log}) {
71
71
  }
72
72
 
73
73
  if (removedFiles > 0) {
74
- log?.info?.(`cache Angular/Vite invalidé (${removedFiles} fichier(s)) pour ${packageName}`);
74
+ log.info(`cache Angular/Vite invalidé (${removedFiles} fichier(s)) pour ${packageName}`);
75
75
  }
76
76
  }
77
77
 
@@ -13,8 +13,6 @@ export function createLogger({ debug = false } = {}) {
13
13
  const prefix = color('35', 'meridiane');
14
14
 
15
15
  return {
16
- debugEnabled: debug,
17
-
18
16
  title(text) {
19
17
  console.log(line('🌙', `${prefix} ${text}`));
20
18
  },
@@ -1,308 +1,56 @@
1
1
  # __PACKAGE_NAME__
2
2
 
3
- Bridge Angular (runtime + models TypeScript) pour une API Platform (Hydra/JSON-LD), avec support Mercure/SSE optionnel.
3
+ Bridge Angular de l'API, généré par [Meridiane 3](https://github.com/obsidiane-lab/meridiane/blob/master/docs/README.md) depuis son contrat OpenAPI. Ne pas modifier : régénérer quand le contrat change.
4
4
 
5
- Le package expose une API Angular volontairement minimaliste :
6
- - `provideBridge()` pour configurer le bridge (HTTP + tokens + Mercure) ;
7
- - `FacadeFactory` / `ResourceFacade<T>` pour une API orientée ressource ;
8
- - `BridgeFacade` pour des appels ad-hoc.
5
+ Il fournit les modèles TypeScript, le client HTTP (JSON-LD, Hydra, merge-patch) et le temps réel Mercure 1.0. Angular 20, 21 ou 22 ; RxJS 7.8.
9
6
 
10
- ## Installation
11
-
12
- ```bash
13
- npm i __PACKAGE_NAME__
14
- ```
15
-
16
- Le bridge est conçu pour être installé dans une application Angular. Les dépendances Angular et RxJS sont des `peerDependencies`.
17
-
18
- ## Compatibilité
19
-
20
- - Angular `@angular/*` `^20.0.0 || ^21.0.0`
21
- - RxJS `^7.8.0`
22
-
23
- ## Démarrage rapide
24
-
25
- Configurez le bridge au démarrage de l’application avec `provideBridge()` (requis).
26
-
27
- ### Application standalone (recommandé)
7
+ ## Configurer
28
8
 
29
9
  ```ts
30
- // app.config.ts (ou main.ts)
31
- import {ApplicationConfig} from '@angular/core';
32
10
  import {provideBridge} from '__PACKAGE_NAME__';
33
11
 
34
12
  export const appConfig: ApplicationConfig = {
35
13
  providers: [
36
14
  provideBridge({
37
- baseUrl: 'https://api.example.com',
38
- mercure: {hubUrl: 'https://api.example.com/.well-known/mercure'},
15
+ baseUrl: '/', // API sur la même origine que le front
16
+ mercure: {
17
+ hubUrl: '/.well-known/mercure',
18
+ authorize: '/api/mercure/authorization', // pose et renouvelle le cookie Mercure
19
+ },
39
20
  }),
40
21
  ],
41
22
  };
42
23
  ```
43
24
 
44
- ### Application NgModule
45
-
46
- ```ts
47
- import {NgModule} from '@angular/core';
48
- import {provideBridge} from '__PACKAGE_NAME__';
49
-
50
- @NgModule({
51
- providers: [
52
- provideBridge({baseUrl: 'https://api.example.com'}),
53
- ],
54
- })
55
- export class AppModule {}
56
- ```
57
-
58
- ## Configuration
59
-
60
- `provideBridge({ ... })` accepte notamment :
61
- - `baseUrl` (requis) ;
62
- - `auth` (Bearer ou interceptor custom) ;
63
- - `http` (`withCredentials` HTTP par défaut) ;
64
- - `mercure` (hubUrl + `topicMode` + stratégie de connexions SSE `connectionMode`/`maxUrlLength`) ;
65
- - `defaults` (headers/timeout/retries) ;
66
- - `singleFlight` (déduplication “in-flight” des requêtes HTTP identiques `GET/HEAD/OPTIONS`) ;
67
- - `debug` (logs runtime) ;
68
- - `extraInterceptors` (interceptors Angular additionnels).
69
-
70
- Les comportements HTTP internes du bridge s'appliquent uniquement aux requêtes
71
- appartenant à l'origine et au chemin de `baseUrl`. Les interceptors personnalisés
72
- restent responsables de leur propre périmètre.
73
-
74
- ### Cookies / `withCredentials`
75
-
76
- Le bridge peut envoyer des cookies (sessions) côté HTTP et SSE.
77
-
78
- Vous pouvez configurer explicitement le HTTP via `http.withCredentials` :
79
- - `true` → cookies envoyés sur les requêtes HTTP
80
- - `false` → cookies désactivés côté HTTP
81
-
82
- `mercure.init.credentials` pilote uniquement Mercure/SSE :
83
- - `credentials: 'include'` (défaut) → cookies envoyés sur EventSource
84
- - `credentials: 'omit'` → cookies désactivés côté SSE
85
-
86
- Compat legacy :
87
- - si `http.withCredentials` est absent, le bridge conserve le comportement historique et le déduit de `mercure.init.credentials`
88
-
89
- Vous pouvez aussi surcharger appel par appel via `opts.withCredentials` sur les appels HTTP.
90
-
91
- ```ts
92
- import {provideBridge} from '__PACKAGE_NAME__';
93
-
94
- provideBridge({
95
- baseUrl: 'https://api.example.com',
96
- http: {withCredentials: true},
97
- mercure: {
98
- hubUrl: 'https://api.example.com/.well-known/mercure',
99
- init: {credentials: 'omit'},
100
- },
101
- });
102
- ```
103
-
104
- ### Auth (Bearer)
105
-
106
- `auth` accepte :
107
- - une string (token Bearer),
108
- - `{ type: 'bearer', token }`,
109
- - `{ type: 'bearer', getToken }` (sync ou async),
110
- - ou un `HttpInterceptorFn` custom.
111
-
112
- Le Bearer intégré est ajouté uniquement aux requêtes dont l’origine et le chemin
113
- appartiennent au `baseUrl` configuré. Une requête vers un autre service conserve
114
- donc ses en-têtes d’origine. Un interceptor custom garde le comportement Angular
115
- standard et reste responsable de son propre périmètre.
116
-
117
- ```ts
118
- import {provideBridge} from '__PACKAGE_NAME__';
119
-
120
- provideBridge({
121
- baseUrl: 'https://api.example.com',
122
- auth: {type: 'bearer', getToken: () => localStorage.getItem('token') ?? undefined},
123
- });
124
- ```
125
-
126
- ### Defaults (headers / timeout / retries)
127
-
128
- ```ts
129
- import {provideBridge} from '__PACKAGE_NAME__';
130
-
131
- provideBridge({
132
- baseUrl: 'https://api.example.com',
133
- defaults: {
134
- headers: {'X-Requested-With': 'fetch'},
135
- timeoutMs: 15_000,
136
- retries: {count: 2, delayMs: 250, methods: ['GET']},
137
- },
138
- });
139
- ```
140
-
141
- ### Debug
142
-
143
- ```ts
144
- import {provideBridge} from '__PACKAGE_NAME__';
145
-
146
- provideBridge({baseUrl: 'https://api.example.com', debug: true});
147
- ```
148
-
149
- ## Appeler l’API
150
-
151
- ### Style orienté ressource (`FacadeFactory` + `ResourceFacade<T>`)
152
-
153
- ```ts
154
- import {inject} from '@angular/core';
155
- import {FacadeFactory, ResourceFacade, Item} from '__PACKAGE_NAME__';
156
-
157
- type Book = Item & {title?: string};
158
-
159
- export class BooksService {
160
- private readonly factory = inject(FacadeFactory);
161
- readonly books: ResourceFacade<Book> = this.factory.create<Book>({url: '/api/books'});
162
- }
163
- ```
164
-
165
- Exemples :
166
-
167
- ```ts
168
- // collection Hydra
169
- this.books.getCollection$({page: 1, itemsPerPage: 20, filters: {title: 'Dune'}});
170
-
171
- // item (souvent via @id)
172
- this.books.get$(book['@id']!);
173
-
174
- // write
175
- this.books.post$({title: 'Neuromancer'});
176
- this.books.patch$(book['@id']!, {title: 'Count Zero'});
177
- this.books.delete$(book['@id']!);
178
- ```
179
-
180
- ### Style ad-hoc (`BridgeFacade`)
181
-
182
- ```ts
183
- import {inject} from '@angular/core';
184
- import {BridgeFacade} from '__PACKAGE_NAME__';
185
-
186
- export class HealthService {
187
- private readonly bridge = inject(BridgeFacade);
188
- getHealth$() {
189
- return this.bridge.get$<{status: string}>('/health');
190
- }
191
- }
192
- ```
193
-
194
- ## Realtime (Mercure/SSE)
195
-
196
- Le realtime est inactif tant que `mercure.hubUrl` n’est pas fourni à `provideBridge()`.
197
-
198
- ```ts
199
- provideBridge({
200
- baseUrl: 'https://api.example.com',
201
- mercure: {hubUrl: 'https://api.example.com/.well-known/mercure', topicMode: 'url'},
202
- });
203
- ```
204
-
205
- ### Concurrence
25
+ `provideBridge()` installe `HttpClient`. Options, cookies et topologies : [Configuration](https://github.com/obsidiane-lab/meridiane/blob/master/docs/configuration.md).
206
26
 
207
- Le bridge propose 2 stratégies SSE configurables via `provideBridge({mercure: ...})` :
208
- - `connectionMode: 'single'` : chaque appel `watch*` ouvre sa propre connexion SSE ;
209
- - `connectionMode: 'auto'` (défaut) : les topics sont mutualisés, puis découpés automatiquement en plusieurs connexions si l’URL Mercure dépasse `maxUrlLength` (défaut `1900`).
210
-
211
- Vous pouvez forcer une connexion dédiée par appel avec `newConnection: true` sur `watch$`, `watchSubResource$` et `watchTypes$`.
27
+ ## Utiliser une ressource
212
28
 
213
29
  ```ts
214
- provideBridge({
215
- baseUrl: 'https://api.example.com',
216
- mercure: {
217
- hubUrl: 'https://api.example.com/.well-known/mercure',
218
- connectionMode: 'auto',
219
- maxUrlLength: 1900,
220
- },
221
- });
222
- ```
223
-
224
- Côté HTTP, le bridge déduplique les requêtes identiques tant qu’elles sont en cours (single-flight, activé par défaut) :
225
- - `GET` / `HEAD` / `OPTIONS` : deux appels identiques partagent le même appel réseau et reçoivent la même réponse ;
226
- - les méthodes avec body (`POST`/`PUT`/`PATCH`/`DELETE`) ne sont pas dédupliquées.
227
-
228
- Ce n’est pas un cache : une fois la requête terminée, un nouvel appel identique relance un nouvel appel réseau.
229
-
230
- Pour désactiver :
231
-
232
- ```ts
233
- provideBridge({baseUrl: 'https://api.example.com', singleFlight: false});
234
- ```
235
-
236
- API :
237
- - `ResourceFacade<T>.watch$(iri | iri[], {newConnection?})` / `unwatch(iri | iri[])`
238
- - `ResourceFacade<T>.watchSubResource$(iri | iri[], 'field.path', {newConnection?})`
239
- - `BridgeFacade.watch$(iri | iri[], filter?, {newConnection?})` / `unwatch(iri | iri[])`
240
- - `BridgeFacade.watchTypes$(iri | iri[], resourceTypes, cfg?, {newConnection?})` (topics “multi-entités”, filtrage par type)
241
- - `BridgeFacade.realtimeDiagnostics$()` (état courant des connexions SSE actives)
242
-
243
- Note SSR : la connexion SSE ne s’ouvre que dans le navigateur.
30
+ import {FacadeFactory, isResourceDeletion, type Project} from '__PACKAGE_NAME__';
244
31
 
245
- ### Topic “multi-entités” (event-bus)
32
+ const projects = inject(FacadeFactory).create<Project>({url: '/api/projects'});
246
33
 
247
- Quand un même topic Mercure publie plusieurs types de payloads (sans lien entre eux),
248
- utilisez `BridgeFacade.watchTypes$()`.
249
- Le flux renvoie une union discriminée par `resourceType` (par défaut via `@type` en JSON-LD).
34
+ projects.getCollection$({page: 1, itemsPerPage: 20}); // Collection<Project>
35
+ projects.get$('/api/projects/42'); // Project
36
+ projects.post$({name: 'Refonte'});
37
+ projects.patch$('/api/projects/42', {archived: true});
38
+ projects.delete$('/api/projects/42');
250
39
 
251
- Fonctionnement (résumé) :
252
- - le bridge s’abonne au(x) topic(s) Mercure (connexion dédiée ou mutualisée selon `connectionMode`)
253
- - chaque event JSON recu est testé sur le champ de discrimination (`@type` par défaut)
254
- - seuls les `resourceType` explicitement demandés sont émis
255
- - chaque event émis est `{ resourceType, payload }`
256
-
257
- Important (provenance des events) :
258
- - `watchTypes$` filtre par type (`payload[discriminator]`), pas par provenance réseau du topic.
259
- - En mode mutualisé (`connectionMode: 'auto'` et sans `newConnection`), un event d’un autre topic partagé peut être émis s’il porte un type autorisé.
260
- - Si vous voulez ecouter strictement un seul topic, utilisez une connexion dédiée :
261
- `watchTypes$(..., {newConnection: true})` (ou `connectionMode: 'single'` globalement).
262
-
263
- Configuration :
264
- - `discriminator` : champ type (défaut `@type`)
265
- - `resourceTypes` : liste de strings à accepter (`['Conversation', 'Message']`, etc.)
266
-
267
- ```ts
268
- import {inject} from '@angular/core';
269
- import {BridgeFacade} from '__PACKAGE_NAME__';
270
- import type {Item} from '__PACKAGE_NAME__';
271
-
272
- type Conversation = Item & {title?: string | null};
273
- type Message = Item & {originalText?: string | null};
274
-
275
- type Registry = {
276
- Conversation: Conversation;
277
- Message: Message;
278
- };
279
-
280
- const bridge = inject(BridgeFacade);
281
-
282
- bridge.watchTypes$<Registry>(
283
- '/api/events/me',
284
- ['Conversation', 'Message'],
285
- {discriminator: '@type'},
286
- {newConnection: true} // isolation stricte du topic
287
- ).subscribe((evt) => {
288
- switch (evt.resourceType) {
289
- case 'Conversation':
290
- // evt.payload: Conversation
291
- break;
292
- case 'Message':
293
- // evt.payload: Message
294
- break;
295
- }
40
+ projects.watch$().subscribe((resource) => { // toute la ressource, en direct
41
+ if (isResourceDeletion(resource)) store.remove(resource['@id']);
42
+ else store.upsert(resource);
296
43
  });
297
44
  ```
298
45
 
299
- ## Models TypeScript
46
+ Pour un endpoint hors ressource ou un topic Mercure personnalisé : `BridgeFacade`.
300
47
 
301
- Les models générés (si présents) sont exportés au même niveau que le runtime :
302
-
303
- ```ts
304
- import type {Item} from '__PACKAGE_NAME__';
305
- // import type {Book} from '__PACKAGE_NAME__';
306
- ```
48
+ ## Documentation
307
49
 
308
- En JSON-LD (`application/ld+json`), l’IRI est typiquement dans `model['@id']`.
50
+ [Démarrage](https://github.com/obsidiane-lab/meridiane/blob/master/docs/demarrage.md) ·
51
+ [HTTP](https://github.com/obsidiane-lab/meridiane/blob/master/docs/http.md) ·
52
+ [Temps réel](https://github.com/obsidiane-lab/meridiane/blob/master/docs/temps-reel.md) ·
53
+ [Backend](https://github.com/obsidiane-lab/meridiane/blob/master/docs/backend.md) ·
54
+ [Recettes](https://github.com/obsidiane-lab/meridiane/blob/master/docs/recettes.md) ·
55
+ [Référence](https://github.com/obsidiane-lab/meridiane/blob/master/docs/reference.md) ·
56
+ [Dépannage](https://github.com/obsidiane-lab/meridiane/blob/master/docs/depannage.md)
@@ -2,8 +2,8 @@
2
2
  "name": "__LIB_NAME__",
3
3
  "version": "0.0.1",
4
4
  "peerDependencies": {
5
- "@angular/common": "^20.0.0 || ^21.0.0",
6
- "@angular/core": "^20.0.0 || ^21.0.0",
5
+ "@angular/common": "^20.0.0 || ^21.0.0 || ^22.0.0",
6
+ "@angular/core": "^20.0.0 || ^21.0.0 || ^22.0.0",
7
7
  "rxjs": "^7.8.0"
8
8
  },
9
9
  "dependencies": {
@@ -8,23 +8,14 @@ export function resolveHttpCallOptions(options: HttpCallOptions | undefined, wit
8
8
  };
9
9
  }
10
10
 
11
- export function buildHttpRequestOptions(
12
- req: HttpRequestConfig,
13
- {withCredentialsDefault}: {withCredentialsDefault: boolean}
14
- ): Record<string, unknown> {
15
- const {query, body, headers, responseType, withCredentials, options = {}} = req;
16
- const mergedOptions: Record<string, unknown> = {...options};
17
-
18
- if (headers) mergedOptions['headers'] = headers;
19
- if (query) mergedOptions['params'] = toHttpParams(query);
20
- if (body !== undefined) mergedOptions['body'] = body;
21
-
22
- const inheritedResponseType = mergedOptions['responseType'];
23
- const inheritedWithCredentials = mergedOptions['withCredentials'];
24
-
25
- mergedOptions['responseType'] = responseType ?? inheritedResponseType ?? 'json';
26
- mergedOptions['withCredentials'] = withCredentials ?? inheritedWithCredentials ?? withCredentialsDefault;
27
- mergedOptions['observe'] = 'body';
28
-
29
- return mergedOptions;
11
+ export function buildHttpRequestOptions(request: HttpRequestConfig, withCredentialsDefault: boolean) {
12
+ return {
13
+ headers: request.headers,
14
+ params: request.query ? toHttpParams(request.query) : undefined,
15
+ body: request.body,
16
+ // The caller types the response with `R`; `HttpClient.request` only infers it for `json`.
17
+ responseType: (request.responseType ?? 'json') as 'json',
18
+ withCredentials: request.withCredentials ?? withCredentialsDefault,
19
+ observe: 'body' as const,
20
+ };
30
21
  }