@obsidiane/meridiane 2.0.13 → 3.0.1
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 +33 -66
- package/cli.js +1 -10
- package/package.json +1 -1
- package/src/application/bridge-workflow.js +23 -66
- package/src/application/generate-bridge.js +32 -36
- package/src/application/run-build.js +8 -30
- package/src/application/run-dev.js +8 -63
- package/src/application/run-generate.js +8 -47
- package/src/domain/bridge-options.js +10 -63
- package/src/generators/bridge/write-bridge-files.js +11 -29
- package/src/generators/models/interface-schema.js +3 -20
- package/src/generators/models/model-selection.js +67 -106
- package/src/generators/models/oas-contract.js +1 -108
- package/src/generators/models/oas-schema-name.js +1 -19
- package/src/generators/models/openapi-to-models.js +51 -42
- package/src/generators/models/schema-utils.js +0 -34
- package/src/generators/models/type-resolver.js +2 -20
- package/src/generators/models/utils.js +0 -11
- package/src/generators/models/write-model-files.js +5 -15
- package/src/infrastructure/dev-project.js +0 -33
- package/src/infrastructure/files.js +15 -0
- package/src/infrastructure/local-package-install.js +2 -2
- package/src/infrastructure/logger.js +0 -2
- package/templates/angular-bridge/README.md +29 -281
- package/templates/angular-bridge/package.json +2 -2
- package/templates/angular-bridge/src/lib/bridge/rest/http-request.options.ts +10 -19
- package/templates/angular-bridge/src/lib/bridge/sse/mercure-hub.ts +449 -0
- package/templates/angular-bridge/src/lib/bridge/sse/mercure-url.ts +28 -0
- package/templates/angular-bridge/src/lib/bridge/sse/resource-topic.ts +83 -0
- package/templates/angular-bridge/src/lib/bridge-options.ts +21 -17
- package/templates/angular-bridge/src/lib/bridge.types.ts +22 -33
- package/templates/angular-bridge/src/lib/facades/bridge.facade.ts +58 -134
- package/templates/angular-bridge/src/lib/facades/facade.factory.ts +25 -25
- package/templates/angular-bridge/src/lib/facades/facade.interface.ts +4 -9
- package/templates/angular-bridge/src/lib/facades/resource.facade.ts +38 -61
- package/templates/angular-bridge/src/lib/interceptors/bearer-auth.interceptor.ts +8 -12
- package/templates/angular-bridge/src/lib/interceptors/bridge-defaults.interceptor.ts +19 -11
- package/templates/angular-bridge/src/lib/interceptors/content-type.interceptor.ts +5 -17
- package/templates/angular-bridge/src/lib/interceptors/singleflight.interceptor.ts +28 -24
- package/templates/angular-bridge/src/lib/ports/realtime.port.ts +34 -53
- package/templates/angular-bridge/src/lib/ports/resource-repository.port.ts +31 -21
- package/templates/angular-bridge/src/lib/provide-bridge.ts +14 -38
- package/templates/angular-bridge/src/lib/tokens.ts +4 -15
- package/templates/angular-bridge/src/lib/utils/request-url.ts +4 -6
- package/templates/angular-bridge/src/lib/utils/url.ts +32 -9
- package/templates/angular-bridge/src/public-api.ts +0 -1
- package/templates/angular-bridge/tsconfig.lib.json +0 -3
- package/src/README.md +0 -9
- package/src/infrastructure/json.js +0 -71
- package/templates/angular-bridge/src/lib/bridge/rest/api-platform.adapter.ts +0 -82
- package/templates/angular-bridge/src/lib/bridge/sse/eventsource-wrapper.ts +0 -70
- package/templates/angular-bridge/src/lib/bridge/sse/mercure-connections.ts +0 -255
- package/templates/angular-bridge/src/lib/bridge/sse/mercure-payload.ts +0 -82
- package/templates/angular-bridge/src/lib/bridge/sse/mercure-topic.mapper.ts +0 -48
- package/templates/angular-bridge/src/lib/bridge/sse/mercure-url.builder.ts +0 -39
- package/templates/angular-bridge/src/lib/bridge/sse/mercure.adapter.ts +0 -358
- package/templates/angular-bridge/src/lib/bridge/sse/ref-count-topic.registry.ts +0 -36
- package/templates/angular-bridge/src/lib/bridge/sse/subscription-claims.ts +0 -76
- 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:
|
|
105
|
-
type:
|
|
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,
|
|
11
|
+
export async function writeModelFiles({ projectDir, spec, include, exclude, log }) {
|
|
12
12
|
const outputDir = path.resolve(projectDir, 'src', 'models');
|
|
13
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
74
|
+
log.info(`cache Angular/Vite invalidé (${removedFiles} fichier(s)) pour ${packageName}`);
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
@@ -1,308 +1,56 @@
|
|
|
1
1
|
# __PACKAGE_NAME__
|
|
2
2
|
|
|
3
|
-
Bridge Angular
|
|
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
|
-
|
|
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
|
-
##
|
|
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: '
|
|
38
|
-
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
32
|
+
const projects = inject(FacadeFactory).create<Project>({url: '/api/projects'});
|
|
246
33
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
46
|
+
Pour un endpoint hors ressource ou un topic Mercure personnalisé : `BridgeFacade`.
|
|
300
47
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
```ts
|
|
304
|
-
import type {Item} from '__PACKAGE_NAME__';
|
|
305
|
-
// import type {Book} from '__PACKAGE_NAME__';
|
|
306
|
-
```
|
|
48
|
+
## Documentation
|
|
307
49
|
|
|
308
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
):
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
}
|