From e6a701e9124f6baeff64afb2df7cad7e8fa35501 Mon Sep 17 00:00:00 2001
From: Perreal Guillaume <guillaume.perreal@irstea.fr>
Date: Thu, 16 May 2019 12:48:26 +0200
Subject: [PATCH] Ajout d'une partie du typescript et configuration des outils.

---
 .gitignore                                |   1 +
 composer.json                             |   3 +
 package-lock.json                         | Bin 76840 -> 127521 bytes
 package.json                              |  60 +++-
 rollup.config.js                          |  16 +
 src/ts/cache.service.spec.ts              | 374 ----------------------
 src/ts/cache.service.ts                   | 178 ----------
 src/ts/index.ts                           |   3 +
 src/ts/metadata/index.ts                  |   3 +
 src/ts/metadata/iri.metadata.ts           |  40 +++
 src/ts/metadata/registry.ts               |  68 ++++
 src/ts/metadata/resource.metadata.ts      |  56 ++++
 src/ts/service/abstract-repository.ts     |  42 +++
 src/ts/service/abstract-resource-cache.ts |  63 ++++
 src/ts/service/api.ts                     | 128 ++++++++
 src/ts/service/index.ts                   |   3 +
 src/ts/types/collection.ts                |  39 +++
 src/ts/types/date-time.ts                 |   4 +
 src/ts/types/index.ts                     |   4 +
 src/ts/types/resource.ts                  |  53 +++
 src/ts/types/uuid.ts                      |  16 +
 tsconfig.declaration.json                 |   9 +
 tsconfig.json                             |  32 +-
 23 files changed, 617 insertions(+), 578 deletions(-)
 create mode 100644 rollup.config.js
 delete mode 100644 src/ts/cache.service.spec.ts
 delete mode 100644 src/ts/cache.service.ts
 create mode 100644 src/ts/index.ts
 create mode 100644 src/ts/metadata/index.ts
 create mode 100644 src/ts/metadata/iri.metadata.ts
 create mode 100644 src/ts/metadata/registry.ts
 create mode 100644 src/ts/metadata/resource.metadata.ts
 create mode 100644 src/ts/service/abstract-repository.ts
 create mode 100644 src/ts/service/abstract-resource-cache.ts
 create mode 100644 src/ts/service/api.ts
 create mode 100644 src/ts/service/index.ts
 create mode 100644 src/ts/types/collection.ts
 create mode 100644 src/ts/types/date-time.ts
 create mode 100644 src/ts/types/index.ts
 create mode 100644 src/ts/types/resource.ts
 create mode 100644 src/ts/types/uuid.ts
 create mode 100644 tsconfig.declaration.json

diff --git a/.gitignore b/.gitignore
index afdcfdb..3113a65 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
 /vendor
 /composer.lock
 /dist
+/out
 /node_modules
 .php_cs.*cache
 .idea
diff --git a/composer.json b/composer.json
index 794c815..51d0020 100644
--- a/composer.json
+++ b/composer.json
@@ -44,6 +44,9 @@
         "sebastian/phpcpd": "^4.0"
     },
     "scripts": {
+      "post-install-cmd": "@nodejs-build",
+      "post-update-cmd": "@nodejs-build",
+      "nodejs-build": "npm install && npm run-script build || exit 0",
       "fix-cs": "@php vendor/bin/php-cs-fixer fix --verbose",
       "phploc": "@php vendor/bin/phploc --no-interaction src/php tests",
       "test": [
diff --git a/package-lock.json b/package-lock.json
index 7b82f5b2cce22f6c310df1d259770bfc2287ecce..db7ae56286e6dcef07e1a416641a736007c43014 100644
GIT binary patch
delta 25585
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
Qa)y5Vkymb}QzujZF9i|~djJ3c

delta 2877
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

diff --git a/package.json b/package.json
index 7466024..cbe80b8 100644
--- a/package.json
+++ b/package.json
@@ -2,10 +2,16 @@
   "name": "irstea-ng-model",
   "version": "1.0.0",
   "description": "Runtime libray for the composer package irstea/ng-model-generator-bundle.",
+  "types": "dist/index.d.ts",
   "main": "dist/index.js",
+  "module": "dist/index.esm.js",
   "directories": {},
   "scripts": {
-    "test": "echo \"Error: no test specified\" && exit 1"
+    "test": "echo \"Error: no test specified\" && exit 1",
+    "prebuild": "rm -rf dist/ && tsc -p ./tsconfig.declaration.json",
+    "build": "tsc -p . && rollup -c",
+    "postbuild": "rm -rf out",
+    "clean": "rm -rf dist/* out/*"
   },
   "repository": {
     "type": "git",
@@ -18,26 +24,54 @@
   "author": "Irstea - pôle IS",
   "license": "LGPL-3.0-or-later",
   "peerDependencies": {
-    "@angular/common": "^7.2.9",
-    "@angular/core": "^7.2.9"
+    "@angular/common": "^7.2.15"
   },
   "dependencies": {
     "lodash": "^4.17.11",
-    "rxjs": "^6.4.0",
-    "rxjs-etc": "^9.4.0"
+    "rxjs": "^6.5.2",
+    "rxjs-etc": "^9.5.0",
+    "tslib": "^1.9.3"
   },
   "devDependencies": {
-    "@angular/common": "^7.2.9",
-    "@angular/core": "^7.2.9",
-    "irstea-typescript-config": "^1.0.3",
-    "prettier": "^1.16.4",
+    "@angular/common": "^7.2.15",
+    "@angular/compiler": "^7.2.15",
+    "@angular/core": "^7.2.15",
+    "codelyzer": "^5.0.1",
+    "husky": "^2.3.0",
+    "irstea-typescript-config": "^1.0.6",
+    "lint-staged": "^8.1.7",
+    "prettier": "^1.17.1",
     "prettier-tslint": "^0.4.2",
-    "rxjs-marbles": "^5.0.0",
-    "rxjs-tslint-rules": "^4.18.2",
-    "tslint": "^5.14.0",
+    "rollup": "^1.12.1",
+    "rollup-plugin-node-resolve": "^5.0.0",
+    "rxjs-marbles": "^5.0.2",
+    "rxjs-tslint-rules": "^4.23.1",
+    "tslint": "^5.16.0",
     "tslint-config-prettier": "^1.18.0",
+    "tslint-defocus": "^2.0.6",
     "tslint-plugin-prettier": "^2.0.1",
-    "typescript": "^3.3.3333",
+    "typescript": "^3.4.5",
     "zone.js": "~0.8.26"
+  },
+  "husky": {
+    "hooks": {
+      "pre-commit": "lint-staged"
+    }
+  },
+  "lint-staged": {
+    "src/**/*.{json,md}": [
+      "prettier --write",
+      "git add"
+    ],
+    "src/**/*.ts": [
+      "prettier-tslint fix",
+      "git add"
+    ]
+  },
+  "prettier": {
+    "printWidth": 80,
+    "semi": true,
+    "singleQuote": true,
+    "trailingComma": "es5"
   }
 }
diff --git a/rollup.config.js b/rollup.config.js
new file mode 100644
index 0000000..5911d85
--- /dev/null
+++ b/rollup.config.js
@@ -0,0 +1,16 @@
+import resolve from 'rollup-plugin-node-resolve';
+
+import pkg from './package.json';
+
+export default [
+  // CommonJS (for Node) and ES module (for bundlers) build.
+  {
+    input: 'out/index.js',
+    external: Object.keys(pkg.dependencies).concat(Object.keys(pkg.peerDependencies)),
+    output: [
+      { file: pkg.main, format: 'cjs' },
+      { file: pkg.module, format: 'es' },
+    ],
+    plugins: [resolve()],
+  },
+];
diff --git a/src/ts/cache.service.spec.ts b/src/ts/cache.service.spec.ts
deleted file mode 100644
index 4b23a8b..0000000
--- a/src/ts/cache.service.spec.ts
+++ /dev/null
@@ -1,374 +0,0 @@
-import { HttpHeaderResponse } from '@angular/common/http';
-import { inject, TestBed } from '@angular/core/testing';
-import * as _ from 'lodash';
-import { forkJoin, Observable } from 'rxjs';
-import { catchError, map, mergeMap } from 'rxjs/operators';
-
-import { MarbleTestScheduler } from '../../../testing/marbles';
-import { Collection, IRI, IRI_PROPERTY, Resource } from '../../shared/models';
-import { safeForkJoin } from '../../shared/rxjs';
-
-import { IRIMismatchError, MissingIRIError, ResourceCache, ValueHolder } from './cache.service';
-
-describe('cache.service', () => {
-  interface MyResource extends Resource {
-    readonly '@id': IRI<MyResource>;
-    readonly '@type': 'MyResource';
-    value: string;
-    value2?: string;
-  }
-
-  function iri(x: string): IRI<MyResource> {
-    return x as any;
-  }
-
-  const MY_IRI = iri('/bla/a');
-  const OTHER_IRI = iri('/bla/B');
-  const VALUES: { [name: string]: MyResource } = {
-    a: { '@id': MY_IRI, '@type': 'MyResource', value: 'foo' },
-    b: { '@id': MY_IRI, '@type': 'MyResource', value: 'bar' },
-    c: { '@id': MY_IRI, '@type': 'MyResource', value: 'bar', value2: 'quz' },
-    d: { '@id': OTHER_IRI, '@type': 'MyResource', value: 'zig' },
-  };
-  let scheduler: MarbleTestScheduler<any>;
-  beforeEach(() => {
-    scheduler = MarbleTestScheduler.create(VALUES, 'error');
-  });
-
-  describe('ValueHolder', () => {
-    let holder: ValueHolder<any>;
-
-    beforeEach(() => {
-      holder = new ValueHolder<MyResource>(iri('/bla/a'));
-    });
-
-    it('should be created', () => {
-      expect(holder).toBeTruthy();
-    });
-
-    describe('.set()', () => {
-      function testSet({ value, error, SET_M }: any) {
-        scheduler.withError(error).run(({ expectObservable }) => {
-          expectObservable(holder.set(value)).toBe(SET_M);
-        });
-      }
-
-      it('should provide the value', () =>
-        testSet({
-          value: VALUES.a,
-          SET_M: '(a|)',
-        }));
-
-      it(`should refuse value without ${IRI_PROPERTY}`, () =>
-        testSet({
-          value: {},
-          error: new MissingIRIError(),
-          SET_M: '#',
-        }));
-
-      it('should refuse value with different @id', () =>
-        testSet({
-          value: { '@id': iri('bar') },
-          error: new IRIMismatchError(MY_IRI, iri('bar')),
-          SET_M: '#',
-        }));
-
-      it('should always points to the same instance', () => {
-        forkJoin(holder.set(VALUES.a), holder.set(VALUES.b)).subscribe(([a, b]: MyResource[]) => {
-          expect(a).toBe(b);
-        });
-      });
-    });
-
-    describe('.update()', () => {
-      it('should provide the value from the server', () =>
-        scheduler.run(({ cold, expectObservable }) => {
-          const REQ_M = '---a|';
-          const UPD_M = '--(a|) ';
-
-          const request$ = cold(REQ_M);
-          expectObservable(holder.update(request$)).toBe(UPD_M);
-        }));
-
-      it('should cancel pending requests', () => {
-        const LOCAL_VALUES = {
-          a: VALUES.a,
-          b: VALUES.b,
-          j: [VALUES.b, VALUES.b],
-        };
-        const REQ1_M = '---a|';
-        const REQ2_M = 'b|   ';
-
-        const UPDA_M = '(j|) ';
-        const REQ1_S = '(^!) ';
-        const REQ2_S = '(^!) ';
-
-        scheduler.withValues(LOCAL_VALUES).run(({ cold, expectObservable, expectSubscriptions }) => {
-          const request1$ = cold(REQ1_M);
-          const request2$ = cold(REQ2_M);
-
-          expectObservable(
-            safeForkJoin([
-              //
-              holder.update(request1$),
-              holder.update(request2$),
-            ])
-          ).toBe(UPDA_M);
-          expectSubscriptions(request1$.subscriptions).toBe(REQ1_S);
-          expectSubscriptions(request2$.subscriptions).toBe(REQ2_S);
-        });
-      });
-
-      it('should propagate errors', () =>
-        scheduler.run(({ cold, expectObservable }) => {
-          const REQ_M = '#';
-          const UPD_M = '#';
-
-          const request$ = cold(REQ_M);
-          expectObservable(holder.update(request$)).toBe(UPD_M);
-        }));
-
-      it('should restart on errors', () =>
-        scheduler.run(({ cold, expectObservable }) => {
-          const REQ1_M = '#';
-          const UPD1_M = '#';
-
-          const obs$ = holder.update(cold(REQ1_M));
-          expectObservable(obs$).toBe(UPD1_M);
-
-          const REQ2_M = '-a|';
-          const UPD2_M = '-a|';
-
-          const obs2$ = holder.update(cold(REQ2_M));
-          expectObservable(obs2$).toBe(UPD2_M);
-        }));
-    });
-
-    describe('.listen()', () => {
-      function testListen({ REQUEST_M, LISTEN_M, initial }: any) {
-        scheduler.run(({ cold, expectObservable }) => {
-          if (initial) {
-            holder.set(initial);
-          }
-          expectObservable(holder.listen(() => cold(REQUEST_M))).toBe(LISTEN_M);
-        });
-      }
-
-      it('should provide the value', () =>
-        testListen({
-          initial: VALUES.a,
-          REQUEST_M: /**/ '    ',
-          LISTEN_M: /***/ '(a|)',
-        }));
-
-      it('should cache the value', () =>
-        testListen({
-          initial: VALUES.a,
-          REQUEST_M: /**/ 'b|  ',
-          LISTEN_M: /***/ '(a|)',
-        }));
-
-      it('should propagate errors', () =>
-        testListen({
-          REQUEST_M: /**/ '#',
-          LISTEN_M: /***/ '#',
-        }));
-    });
-
-    it('.invalidate() should cause the value to be requested again', () =>
-      scheduler.run(({ cold, expectObservable }) => {
-        const REQUEST_M = /**/ '(a|)';
-        const LISTEN_M = /***/ '(a|)';
-        const requestFactory = jasmine.createSpy('requestFactory');
-        requestFactory.and.returnValue(cold(REQUEST_M));
-
-        return holder
-          .set(VALUES.a)
-          .toPromise()
-          .then(() => holder.invalidate())
-          .then(() => expectObservable(holder.listen(requestFactory)).toBe(LISTEN_M))
-          .then(() => expect(requestFactory).toHaveBeenCalled());
-      }));
-  });
-
-  describe('ResourceCache', () => {
-    beforeEach(() =>
-      TestBed.configureTestingModule({
-        providers: [ResourceCache],
-      })
-    );
-
-    it('should be created', inject([ResourceCache], (service: ResourceCache) => {
-      expect(service).toBeDefined();
-    }));
-
-    describe('.get()', () => {
-      it('should provide the value', inject([ResourceCache], (service: ResourceCache) =>
-        scheduler.run(({ cold, expectObservable }) => {
-          const getQuery$ = cold('a|');
-
-          expectObservable(service.get(MY_IRI, () => getQuery$)).toBe('(a|)');
-        })
-      ));
-
-      it('should cache the value', inject([ResourceCache], (service: ResourceCache) =>
-        scheduler.run(({ cold, expectObservable }) => {
-          const getQuery$ = cold('a|');
-          const getQuery2$ = cold('b|');
-          // tslint:disable-next-line:rxjs-finnish
-          const queries$ = cold('ab|', { a: getQuery$, b: getQuery2$ });
-
-          expectObservable(queries$.pipe(mergeMap(query$ => service.get(MY_IRI, () => query$)))).toBe('aa|');
-        })
-      ));
-
-      it('should propagate errors', inject([ResourceCache], (service: ResourceCache) =>
-        scheduler.run(({ cold, expectObservable }) => {
-          const getQuery$ = cold('#');
-
-          expectObservable(service.get(MY_IRI, () => getQuery$)).toBe('#');
-        })
-      ));
-    });
-
-    describe('.put()', () => {
-      it('should provide the value', inject([ResourceCache], (service: ResourceCache) => {
-        const putRequest$ = scheduler.createColdObservable('a|');
-
-        scheduler.expectObservable(service.put(MY_IRI, putRequest$)).toBe('a|');
-      }));
-
-      it('should not cache the value', inject([ResourceCache], (service: ResourceCache) =>
-        scheduler.run(({ cold, expectObservable }) => {
-          const putRequest$ = cold('a|');
-          const putRequest2$ = cold('b|');
-          // tslint:disable-next-line:rxjs-finnish
-          const requests$ = cold('ab|', { a: putRequest$, b: putRequest2$ });
-
-          expectObservable(
-            requests$.pipe(
-              mergeMap((request$: Observable<MyResource>) => service.put(MY_IRI, request$)),
-              map(x => _.clone(x))
-            )
-          ).toBe('ab|');
-        })
-      ));
-
-      it('should propagate errors', inject([ResourceCache], (service: ResourceCache) =>
-        scheduler.run(({ cold, expectObservable }) => {
-          const putRequest$ = cold('#');
-
-          expectObservable(service.put(MY_IRI, putRequest$)).toBe('#');
-        })
-      ));
-    });
-
-    describe('.post()', () => {
-      it('should provide the value', inject([ResourceCache], (service: ResourceCache) =>
-        scheduler.run(({ cold, expectObservable }) => {
-          const postRequest$ = cold('a|');
-
-          expectObservable(service.post(postRequest$)).toBe('a|');
-        })
-      ));
-
-      it('should propagate errors', inject([ResourceCache], (service: ResourceCache) =>
-        scheduler.run(({ cold, expectObservable }) => {
-          const postRequest$ = cold('#');
-
-          expectObservable(service.post(postRequest$)).toBe('#');
-        })
-      ));
-    });
-
-    describe('.delete()', () => {
-      it('should clear the cache on successful fetch', inject([ResourceCache], (service: ResourceCache) => {
-        const response = new HttpHeaderResponse({ status: 200 });
-        const values = { r: response, a: VALUES.a, b: VALUES.b };
-        const sched = scheduler.withValues(values);
-
-        sched.run(async ({ cold, expectObservable }) => {
-          await service.get(MY_IRI, () => cold('a|')).toPromise();
-          await service.delete(MY_IRI, cold('r|')).toPromise();
-
-          return expectObservable(service.get(MY_IRI, () => cold('b|'))).toBe('(b|)');
-        });
-      }));
-
-      it('should propagate errors', inject([ResourceCache], (service: ResourceCache) =>
-        scheduler.run(({ cold, expectObservable }) => {
-          const deleteRequest$ = cold('#');
-
-          expectObservable(service.delete(MY_IRI, deleteRequest$)).toBe('#');
-        })
-      ));
-
-      it('should not clear the cache on error', inject([ResourceCache], (service: ResourceCache) => {
-        const error = new HttpHeaderResponse({ status: 500 });
-        const values = { a: VALUES.a, b: VALUES.b, e: error };
-        scheduler
-          .withValues(values)
-          .withError(error)
-          .run(async ({ cold, expectObservable }) => {
-            await service.get(MY_IRI, () => cold('a|')).toPromise();
-            await service
-              .delete(MY_IRI, cold('#'))
-              .pipe(catchError(e => e))
-              .toPromise();
-
-            return expectObservable(service.get(MY_IRI, () => cold('b|'))).toBe('(a|)');
-          });
-      }));
-    });
-
-    describe('.getAll()', () => {
-      it('should provide the returned value', inject([ResourceCache], (service: ResourceCache) => {
-        const values = {
-          a: { 'hydra:member': [VALUES.a, VALUES.d], 'hydra:totalItems': 2 },
-        };
-        scheduler.withValues(values).run(({ cold, expectObservable }) => {
-          const getAllRequest$ = cold('a|');
-          expectObservable(service.getAll(getAllRequest$)).toBe('a|');
-        });
-      }));
-
-      it('should nicely handle empty collections', inject([ResourceCache], (service: ResourceCache) => {
-        const values = {
-          a: { 'hydra:member': [] as any, 'hydra:totalItems': 0 },
-        };
-        scheduler.withValues(values).run(({ cold, expectObservable }) => {
-          const getAllRequest$ = cold('a|');
-          expectObservable(service.getAll(getAllRequest$)).toBe('a|');
-        });
-      }));
-
-      it('should propagate errors', inject([ResourceCache], (service: ResourceCache) =>
-        scheduler.run(({ cold, expectObservable }) => {
-          const getAllRequest$ = cold<Collection<MyResource>>('#');
-          expectObservable(service.getAll(getAllRequest$)).toBe('#');
-        })
-      ));
-
-      it('should populate the cache', inject([ResourceCache], (service: ResourceCache) => {
-        const values = {
-          a: VALUES.a,
-          b: VALUES.d,
-          h: {
-            'hydra:member': [VALUES.a, VALUES.d],
-            'hydra:totalItems': 2,
-          },
-        };
-        scheduler.withValues(values).run(({ cold, expectObservable }) => {
-          const getAllRequest$ = cold('h|');
-          const getRequest$ = cold('b|');
-          const requests$ = cold<() => Observable<any>>('ab|', {
-            a: () => service.getAll(getAllRequest$),
-            b: () => service.get(MY_IRI, () => getRequest$),
-          });
-
-          expectObservable(requests$.pipe(mergeMap((sendRequest: () => Observable<any>) => sendRequest()))).toBe('ha|');
-        });
-      }));
-    });
-  });
-});
diff --git a/src/ts/cache.service.ts b/src/ts/cache.service.ts
deleted file mode 100644
index 1ed095f..0000000
--- a/src/ts/cache.service.ts
+++ /dev/null
@@ -1,178 +0,0 @@
-import { HttpResponseBase } from '@angular/common/http';
-import * as _ from 'lodash';
-import { Observable, of, race, Subject, throwError } from 'rxjs';
-import { map, switchMap, take, tap } from 'rxjs/operators';
-
-import {
-  AbstractResourceCache,
-  Collection,
-  COLLECTION_MEMBERS,
-  getCollectionMembers,
-  IRI,
-  IRI_PROPERTY,
-  Resource,
-} from '../../shared/models';
-import { safeForkJoin } from '../../shared/rxjs';
-
-export class APICacheError extends Error {}
-
-export class MissingIRIError extends APICacheError {
-  public constructor() {
-    super(`resource must have an ${IRI_PROPERTY} property`);
-  }
-}
-
-export class IRIMismatchError extends APICacheError {
-  public constructor(expected: any, actual: any) {
-    super(`${IRI_PROPERTY}s mismatch: ${actual} !== ${expected}`);
-  }
-}
-
-/**
- * ValueHolder gère les requêtes d'une seule ressource.
- *
- * @internal
- */
-export class ValueHolder<R extends Resource> {
-  private readonly value$ = new Subject<R>();
-
-  private readonly value = {} as R;
-  private version = 0;
-
-  constructor(private readonly iri: IRI<R>) {}
-
-  public set(value: R): Observable<R> {
-    if (!(IRI_PROPERTY in value)) {
-      return throwError(new MissingIRIError());
-    }
-    if (value[IRI_PROPERTY] !== this.iri) {
-      return throwError(new IRIMismatchError(this.iri, value[IRI_PROPERTY]));
-    }
-
-    _.assign(this.value, value);
-    _(this.value)
-      .keys()
-      .difference(_.keys(value))
-      .forEach(key => delete this.value[key]);
-    this.version++;
-    this.value$.next(this.value);
-
-    return of(this.value);
-  }
-
-  public listen(queryFactory: () => Observable<R>): Observable<R> {
-    if (this.version > 0) {
-      return of(this.value);
-    }
-    return this.update(queryFactory());
-  }
-
-  public update(request$: Observable<R>): Observable<R> {
-    return race(this.value$.pipe(take(1)), request$.pipe(switchMap((item: R) => this.set(item))));
-  }
-
-  public invalidate(): void {
-    this.version = 0;
-  }
-
-  public delete(): void {
-    this.value$.complete();
-  }
-}
-
-/**
- * Implémentation d'un cache de resource.
- *
- * Cette implémentation met en place une queue de requête ainsi qu'un observable pour chaque ressource.
- *
- * La queue de requête permet de mettre à jour une ressource en cache. switchMap est utilisé pour prendre en compte
- * les valeurs des dernières requêtes.
- *
- * L'Observable s'assure de retourner toujours la même référence d'objet tout au long de la vie
- * de la ressource dans le cache, il permet aussi de faire suivre tout mise à jour à d'eventuels subscribers.
- */
-export class ResourceCache extends AbstractResourceCache {
-  private readonly holders = new Map<IRI<Resource>, ValueHolder<Resource>>();
-
-  /**
-   * Retourne la ressource identifiée par l'IRI donné.
-   *
-   * Effectue une requête si on le connait pas.
-   */
-  public get<R extends Resource>(iri: IRI<R>, requestFactory: () => Observable<R>): Observable<R> {
-    return this.getHolder(iri).listen(requestFactory);
-  }
-
-  /**
-   * Envoie une requête de mise à jour puis met à jour le cache avec la réponse du serveur.
-   */
-  public put<R extends Resource>(iri: IRI<R>, query$: Observable<R>): Observable<R> {
-    return this.getHolder(iri).update(query$);
-  }
-
-  /**
-   * Envoie une requête de création puis met à jour le chache avec la réponse.
-   */
-  public post<R extends Resource>(query$: Observable<R>): Observable<R> {
-    return query$.pipe(switchMap(item => this.received(item)));
-  }
-
-  /**
-   * Supprime une ressource sur le serveur puis en cache.
-   */
-  public delete<R extends Resource>(iri: IRI<R>, query$: Observable<HttpResponseBase>): Observable<HttpResponseBase> {
-    return query$.pipe(
-      tap(() => {
-        if (!this.holders.has(iri)) {
-          return;
-        }
-        this.holders.get(iri).delete();
-        this.holders.delete(iri);
-      })
-    );
-  }
-
-  /**
-   * Fait une requête pour plusieurs ressources puis les mets en cache.
-   */
-  public getAll<R extends Resource>(query$: Observable<Collection<R>>): Observable<Collection<R>> {
-    return query$.pipe(
-      switchMap((coll: Collection<R>) => {
-        const members = getCollectionMembers(coll);
-        const memberObservables$ = members.map(item => this.received(item));
-        return safeForkJoin(memberObservables$).pipe(
-          map(items => Object.assign({} as Collection<R>, coll, { [COLLECTION_MEMBERS]: items }))
-        );
-      })
-    );
-  }
-
-  /**
-   * Invalide la valeur d'une IRI pour forcer une mise-à-jour.
-   */
-  public invalidate<R extends Resource>(iri: IRI<R>): void {
-    if (!this.holders.has(iri)) {
-      return;
-    }
-    this.holders.get(iri).invalidate();
-  }
-
-  /**
-   * Retourne le ValueHolder d'une IRI, ou le crée si nécessaire.
-   */
-  private getHolder<R extends Resource>(iri: IRI<R>): ValueHolder<R> {
-    let holder = this.holders.get(iri) as ValueHolder<R>;
-    if (!holder) {
-      holder = new ValueHolder<R>(iri);
-      this.holders.set(iri, holder);
-    }
-    return holder;
-  }
-
-  /**
-   * Retourne le ValueHolder d'une IRI, ou le crée si nécessaire.
-   */
-  private received<R extends Resource>(item: R): Observable<R> {
-    return this.getHolder(item[IRI_PROPERTY]).set(item);
-  }
-}
diff --git a/src/ts/index.ts b/src/ts/index.ts
new file mode 100644
index 0000000..2616010
--- /dev/null
+++ b/src/ts/index.ts
@@ -0,0 +1,3 @@
+export * from './metadata';
+export * from './service';
+export * from './types';
diff --git a/src/ts/metadata/index.ts b/src/ts/metadata/index.ts
new file mode 100644
index 0000000..93f3e06
--- /dev/null
+++ b/src/ts/metadata/index.ts
@@ -0,0 +1,3 @@
+export * from './iri.metadata';
+export * from './resource.metadata';
+export * from './registry';
diff --git a/src/ts/metadata/iri.metadata.ts b/src/ts/metadata/iri.metadata.ts
new file mode 100644
index 0000000..1f34dd3
--- /dev/null
+++ b/src/ts/metadata/iri.metadata.ts
@@ -0,0 +1,40 @@
+import { IRI } from '../types';
+
+/**
+ * Type des paramètres
+ */
+export type IRIParameters = string[] | string;
+
+/**
+ * Informations sur l'IRI d'une resource.
+ *
+ * P: types des paramètres.
+ */
+export class IRIMetadata<P extends IRIParameters> {
+  public constructor(
+    private readonly testPattern: RegExp,
+    private readonly capturePattern: RegExp,
+    private readonly template: (parameters: P) => string
+  ) {}
+
+  public validate(path: string): boolean {
+    return this.testPattern.test(path);
+  }
+
+  public generate(parameters: P): IRI<any> {
+    return this.template(parameters) as any;
+  }
+
+  public parse(path: string): P {
+    const matches = this.capturePattern.exec(path);
+    if (!matches) {
+      throw new Error(
+        `Invalid path: ${path} does not match ${this.capturePattern}`
+      );
+    }
+    if (matches.length == 2) {
+      return matches[1] as any;
+    }
+    return matches.slice(1) as any;
+  }
+}
diff --git a/src/ts/metadata/registry.ts b/src/ts/metadata/registry.ts
new file mode 100644
index 0000000..9c01077
--- /dev/null
+++ b/src/ts/metadata/registry.ts
@@ -0,0 +1,68 @@
+import { AbstractRepository } from '../service';
+import { Resource } from '../types';
+
+import { IRIParameters } from './iri.metadata';
+import { ResourceMetadata } from './resource.metadata';
+
+/**
+ * Sur-type encadrant des API.
+ */
+export interface APIMeta {
+  [type: string]: {
+    resource: Resource;
+    metadata: ResourceMetadata<any, any, IRIParameters>;
+    repository: AbstractRepository<any, any, IRIParameters>;
+    iriParameters: IRIParameters;
+  };
+}
+
+/**
+ * Classe abstraite d'un registre des metadonnées des ressources.
+ */
+export interface APIMetadataRegistry<API extends APIMeta> {
+  /**
+   * Vérifie si on a des métadonnées pour le type de ressourcce indiqué.
+   */
+  has<T extends keyof API>(type: T): type is T;
+
+  /**
+   * (Construit et) retourne l'instance de métadonnées pour le type de resource 'type'.
+   */
+  get<T extends keyof API>(type: T): API[T]['metadata'];
+}
+
+/**
+ * Registre de métadonnées qui construit les instances à la demande (ce qui permet de gérer les
+ * dépendances entre métadonnées).
+ */
+export class LazyMetadataRegistry<API extends APIMeta>
+  implements APIMetadataRegistry<API> {
+  private readonly instances = {} as { [T in keyof API]: API[T]['metadata'] };
+
+  protected constructor(
+    private readonly builders: {
+      readonly [T in keyof API]: (
+        r?: APIMetadataRegistry<API>
+      ) => API[T]['metadata']
+    }
+  ) {}
+
+  public has<T extends keyof API>(type: T): type is T {
+    return typeof type === 'string' && type in this.builders;
+  }
+
+  public get<T extends keyof API>(type: T): API[T]['metadata'] {
+    if (!this.has(type)) {
+      throw new Error(`Invalid resource type: ${type}`);
+    }
+    return this.getOrCreate(type);
+  }
+
+  protected getOrCreate<T extends keyof API>(type: T): API[T]['metadata'] {
+    let metadata = this.instances[type];
+    if (!metadata) {
+      metadata = this.instances[type] = this.builders[type](this);
+    }
+    return metadata;
+  }
+}
diff --git a/src/ts/metadata/resource.metadata.ts b/src/ts/metadata/resource.metadata.ts
new file mode 100644
index 0000000..ed75e53
--- /dev/null
+++ b/src/ts/metadata/resource.metadata.ts
@@ -0,0 +1,56 @@
+import { getResourceType, IRI, Resource } from '../types';
+
+import { IRIMetadata, IRIParameters } from './iri.metadata';
+
+/**
+ * Metadonnées d'une ressource.
+ *
+ * R : type de resource, e.g. Person
+ * T : valeur de la propriété '@type', e.g. 'Person'.
+ */
+export class ResourceMetadata<
+  R extends Resource,
+  T extends string,
+  P extends IRIParameters
+> {
+  public constructor(
+    public readonly type: T,
+    public readonly iri: IRIMetadata<P>,
+    private readonly requiredProperties: Array<keyof R>,
+    private readonly types: Array<ResourceMetadata<any, any, P>> = []
+  ) {
+    this.types.unshift(this);
+  }
+
+  /**
+   * Vérifie si l'argument représente une ressource R ou dérivée.
+   */
+  public isResource(that: unknown): that is R {
+    const type = getResourceType(that);
+    if (!type) {
+      return false;
+    }
+    return this.types.some(t => t.type === type);
+  }
+
+  /**
+   * Vérifie si une propriété est obligatoire dans la resource R.
+   */
+  public isRequired(property: keyof R): boolean {
+    return this.requiredProperties.includes(property);
+  }
+
+  /**
+   * Génère une IRI à partir de ses paramètres.
+   */
+  public generateIRI(parameters: P): IRI<R> {
+    return this.iri.generate(parameters);
+  }
+
+  /**
+   * Extrait les paramètres d'une IRI.
+   */
+  public getIRIParameters(iri: IRI<R>): P {
+    return this.iri.parse(iri as any);
+  }
+}
diff --git a/src/ts/service/abstract-repository.ts b/src/ts/service/abstract-repository.ts
new file mode 100644
index 0000000..0620ded
--- /dev/null
+++ b/src/ts/service/abstract-repository.ts
@@ -0,0 +1,42 @@
+import { HttpClient } from '@angular/common/http';
+
+import { APIMeta, IRIParameters, ResourceMetadata } from '../metadata';
+import { IRI, Resource } from '../types';
+
+import { AbstractResourceCache } from './abstract-resource-cache';
+
+/**
+ * Classe de base d'un repository.
+ */
+export abstract class AbstractRepository<
+  R extends Resource,
+  T extends string,
+  P extends IRIParameters
+> {
+  public constructor(
+    public readonly metadata: ResourceMetadata<R, T, P>,
+    protected readonly client: HttpClient,
+    protected readonly cache: AbstractResourceCache
+  ) {}
+
+  /**
+   * Génère une IRI à partir de ses paramètres.
+   */
+  public generateIRI(parameters: P): IRI<R> {
+    return this.metadata.generateIRI(parameters);
+  }
+
+  /**
+   * Extrait les paramètres d'une IRI.
+   */
+  public getIRIParameters(iri: IRI<R>): P {
+    return this.metadata.getIRIParameters(iri);
+  }
+}
+
+/**
+ * Classe abstraite de la façade de l'API.
+ */
+export interface APIRepositoryRegistry<API extends APIMeta> {
+  get<T extends keyof API>(type: T): API[T]['repository'];
+}
diff --git a/src/ts/service/abstract-resource-cache.ts b/src/ts/service/abstract-resource-cache.ts
new file mode 100644
index 0000000..558a87e
--- /dev/null
+++ b/src/ts/service/abstract-resource-cache.ts
@@ -0,0 +1,63 @@
+import { HttpResponseBase } from '@angular/common/http';
+import { Observable } from 'rxjs';
+
+import { Collection, IRI, Resource } from '../types';
+
+/**
+ * AbstractResourceCache est une classe abstraite qui définit l'interface d'un cache de ressources.
+ *
+ * Elle doit être implémentée puis être fournie en provider d'un module core. Par exemple:
+ *
+ *      final class ResourceCache<T extends Resource> extends AbstractResourceCache<T> {
+ *        // Implémentation
+ *      }
+ *
+ *      providers: [
+ *        [ provider: AbstractResourceCache, useClass: ResourceCache ],
+ *      ]
+ */
+export abstract class AbstractResourceCache {
+  /**
+   * Récupère une ressource par son IRI. N'exécute la requête requestFactory que si on ne dispose
+   * pas d'une version en cache.
+   */
+  public abstract get<R extends Resource>(
+    iri: IRI<R>,
+    requestFactory: () => Observable<R>
+  ): Observable<R>;
+
+  /**
+   * Met à jour une ressource existante, rafraîchit le cache local avec la réponse.
+   */
+  public abstract put<R extends Resource>(
+    iri: IRI<R>,
+    request: Observable<R>
+  ): Observable<R>;
+
+  /**
+   * Crée une nouvelle ressource et met la ressource créée dans le cache.
+   */
+  public abstract post<R extends Resource>(
+    request: Observable<R>
+  ): Observable<R>;
+
+  /**
+   * Supprime une ressource en distant et dans le cache.
+   */
+  public abstract delete<R extends Resource>(
+    iri: IRI<R>,
+    request: Observable<HttpResponseBase>
+  ): Observable<HttpResponseBase>;
+
+  /**
+   * Effectue une recherche et met en cache toutes les ressources récupérées.
+   */
+  public abstract getAll<R extends Resource>(
+    request: Observable<Collection<R>>
+  ): Observable<Collection<R>>;
+
+  /**
+   * Invalide une ressource en cache.
+   */
+  public abstract invalidate<R extends Resource>(iri: IRI<R>): void;
+}
diff --git a/src/ts/service/api.ts b/src/ts/service/api.ts
new file mode 100644
index 0000000..19e9d12
--- /dev/null
+++ b/src/ts/service/api.ts
@@ -0,0 +1,128 @@
+import { HttpClient, HttpHeaders } from '@angular/common/http';
+import { forkJoin, Observable } from 'rxjs';
+
+import { APIMeta, APIMetadataRegistry } from '../metadata';
+import { IRI, Resource } from '../types';
+
+import { APIRepositoryRegistry } from './abstract-repository';
+import { AbstractResourceCache } from './abstract-resource-cache';
+
+/**
+ * Options supplémentaires pour les requêtes.
+ */
+export interface RequestOptions {
+  body?: any;
+  headers?:
+    | HttpHeaders
+    | {
+        [header: string]: string | string[];
+      };
+  params?: { [param: string]: string | string[] };
+}
+
+/**
+ * Service permettant d'accéder à l'API.
+ */
+export interface APIService<API extends APIMeta> {
+  /**
+   * Métadonnées de l'API.
+   */
+  readonly metadata: APIMetadataRegistry<API>;
+
+  /**
+   * Repositories de l'API
+   */
+  readonly repositories: APIRepositoryRegistry<API>;
+
+  /**
+   * Récupère une ressource par son IRI ou par son type et les paramètres de son IRI.
+   */
+  get<R extends Resource>(iri: IRI<R>, options?: RequestOptions): Observable<R>;
+  get<T extends keyof API>(
+    type: T,
+    parameters: API[T]['iriParameters'],
+    options?: RequestOptions
+  ): Observable<API[T]['resource']>;
+
+  /**
+   * Récupère des ressources par leurs IRIs.
+   */
+  getMany<R extends Resource>(
+    iris: Array<IRI<R>>,
+    options?: RequestOptions
+  ): Observable<R[]>;
+
+  /**
+   * Génère l'IRI d'une resource à partir de son type et des paramètres d'IRI.
+   */
+  generateIRI<T extends keyof API, P extends string[]>(
+    type: T,
+    parameters: P
+  ): IRI<any>;
+
+  /**
+   * Invalide le cache pour une IRI.
+   */
+  invalidate<R extends Resource>(iri: IRI<R>): void;
+}
+
+/**
+ * Implémentation de base d'une api
+ */
+export abstract class AbstractAPIService<
+  API extends APIMeta,
+  MR extends APIMetadataRegistry<API>,
+  RR extends APIRepositoryRegistry<API>
+> implements APIService<API> {
+  public constructor(
+    public readonly metadata: MR,
+    public readonly repositories: RR,
+    private readonly cache: AbstractResourceCache,
+    private readonly client: HttpClient
+  ) {}
+
+  public get<R extends Resource>(
+    iri: IRI<R>,
+    options?: RequestOptions
+  ): Observable<R>;
+  public get<T extends keyof API>(
+    type: T,
+    parameters: API[T]['iriParameters'],
+    options?: RequestOptions
+  ): Observable<API[T]['resource']>;
+
+  public get<T extends keyof API, R extends Resource = API[T]['resource']>(
+    typeOrIRI: T | IRI<R>,
+    parametersOrOptions?: API[T]['iriParameters'] | RequestOptions,
+    options?: RequestOptions
+  ): Observable<R> {
+    let iri: IRI<R>;
+    if (this.metadata.has(typeOrIRI as string)) {
+      iri = this.metadata
+        .get(typeOrIRI as string)
+        .generateIRI(parametersOrOptions as API[T]['iriParameters']);
+    } else {
+      iri = typeOrIRI as IRI<R>;
+      options = parametersOrOptions as RequestOptions;
+    }
+    return this.cache.get(iri, () => this.client.get<R>(iri as any, options));
+  }
+
+  public getMany<R extends Resource>(
+    iris: Array<IRI<R>>,
+    options?: RequestOptions
+  ): Observable<R[]> {
+    return forkJoin(iris.map(iri => this.get(iri, options)));
+  }
+
+  public generateIRI<T extends keyof API>(
+    type: T,
+    parameters: API[T]['iriParameters']
+  ): IRI<API[T]['resource']> {
+    return this.metadata.get(type).generateIRI(parameters);
+  }
+
+  public invalidate<R extends Resource>(iri: IRI<R>): void {
+    this.cache.invalidate(iri);
+  }
+}
diff --git a/src/ts/service/index.ts b/src/ts/service/index.ts
new file mode 100644
index 0000000..269a008
--- /dev/null
+++ b/src/ts/service/index.ts
@@ -0,0 +1,3 @@
+export * from './abstract-repository';
+export * from './abstract-resource-cache';
+export * from './api';
diff --git a/src/ts/types/collection.ts b/src/ts/types/collection.ts
new file mode 100644
index 0000000..3c28689
--- /dev/null
+++ b/src/ts/types/collection.ts
@@ -0,0 +1,39 @@
+import { Resource } from './resource';
+
+/**
+ * Nom de la propriété contenant les resource d'une collection.
+ */
+export const COLLECTION_MEMBERS = 'hydra:member';
+
+/**
+ * Nom de la propriété contenant le nombre total d'objet d'une collection.
+ */
+export const COLLECTION_TOTAL_COUNT = 'hydra:totalItems';
+
+/**
+ * Collection représente une collection de respoucres JSON-LD pour un type T donné.
+ */
+export interface Collection<R extends Resource> {
+  [COLLECTION_MEMBERS]: R[];
+  [COLLECTION_TOTAL_COUNT]: number;
+  [property: string]: any;
+}
+
+/**
+ * Retourne les membres d'une collection.
+ */
+export function getCollectionMembers<R extends Resource>(
+  collection: Collection<R>
+): R[] {
+  return collection[COLLECTION_MEMBERS];
+}
+
+/**
+ * Retourne le nombre total d'items
+ * @param collection
+ */
+export function getCollectionTotalCount<R extends Resource>(
+  collection: Collection<R>
+): number {
+  return collection[COLLECTION_TOTAL_COUNT];
+}
diff --git a/src/ts/types/date-time.ts b/src/ts/types/date-time.ts
new file mode 100644
index 0000000..f404731
--- /dev/null
+++ b/src/ts/types/date-time.ts
@@ -0,0 +1,4 @@
+/**
+ * Full DateTime in ISO-8601 format.
+ */
+export type DateTime = string;
diff --git a/src/ts/types/index.ts b/src/ts/types/index.ts
new file mode 100644
index 0000000..c0568a2
--- /dev/null
+++ b/src/ts/types/index.ts
@@ -0,0 +1,4 @@
+export * from './collection';
+export * from './date-time';
+export * from './resource';
+export * from './uuid';
diff --git a/src/ts/types/resource.ts b/src/ts/types/resource.ts
new file mode 100644
index 0000000..1848919
--- /dev/null
+++ b/src/ts/types/resource.ts
@@ -0,0 +1,53 @@
+/**
+ * IRI typé.
+ *
+ * Internationalized Resource Identifier - RFC 3987
+ */
+const IRI = Symbol('IRI');
+
+/* Les IRI sont en fait des chaînes mais pour forcer un typage fort on les définit
+ * comme un type "opaque". De cette façon, il est impossible de mélanger
+ * IRI et chaînes, et le type générique R permet d'interdire les assignations entre
+ * IRI de resources différentes.
+ */
+export interface IRI<R extends Resource> extends String {
+  readonly [IRI]?: R;
+}
+
+/**
+ * Nom de la propriété d'une resource contenant son IRI.
+ */
+export const IRI_PROPERTY = '@id';
+
+/**
+ * Nom de la propriété d'une resource contenant son type.
+ */
+export const TYPE_PROPERTY = '@type';
+
+/**
+ * Resource
+ */
+export interface Resource {
+  readonly [IRI_PROPERTY]: IRI<any>;
+  readonly [TYPE_PROPERTY]: string;
+  [property: string]: any;
+}
+
+/**
+ * Vérifie que l'argument est une ressource.
+ */
+export function isResource(what: unknown): what is Resource {
+  return (
+    typeof what === 'object' &&
+    what !== null &&
+    TYPE_PROPERTY in what &&
+    typeof (what as any)[TYPE_PROPERTY] === 'string'
+  );
+}
+
+/**
+ * Retourne le type d'une ressource, ou null
+ */
+export function getResourceType(that: unknown): string | null {
+  return isResource(that) ? that[TYPE_PROPERTY] : null;
+}
diff --git a/src/ts/types/uuid.ts b/src/ts/types/uuid.ts
new file mode 100644
index 0000000..8dda7a7
--- /dev/null
+++ b/src/ts/types/uuid.ts
@@ -0,0 +1,16 @@
+/**
+ * Universally Unique IDentifier - RFC 4122
+ */
+export type UUID = string;
+
+/**
+ * Teste si une donnée (chaîne) est formatée selon un UUID.
+ */
+export function isUUID(data: unknown): data is UUID {
+  return (
+    typeof data === 'string' &&
+    /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu.test(
+      data
+    )
+  );
+}
diff --git a/tsconfig.declaration.json b/tsconfig.declaration.json
new file mode 100644
index 0000000..ef88678
--- /dev/null
+++ b/tsconfig.declaration.json
@@ -0,0 +1,9 @@
+{
+  "extends": "./tsconfig.json",
+  "include": ["src/ts/index.ts"],
+  "compilerOptions": {
+    "outFile": "dist/index.d.ts",
+    "declaration": true,
+    "emitDeclarationOnly": true
+  }
+}
diff --git a/tsconfig.json b/tsconfig.json
index 48c1e49..4764da3 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,23 +1,29 @@
 {
   "extends": "./node_modules/irstea-typescript-config/tsconfig.json",
   "compileOnSave": false,
+  "include": ["src/ts/**/*"],
+  "exclude": ["src/ts/**/*.spec.ts"],
   "compilerOptions": {
-    "outDir": "./dist/out-tsc",
-    "sourceMap": false,
-    "inlineSourceMap": false,
-    "declaration": false,
+    "baseUrl": "./",
+    "paths": {},
+    "strict": true,
+    "lib": ["dom", "es2017"],
+    "typeRoots": ["node_modules/@types"],
+    "outDir": "out/",
+    "target": "es5",
+    "module": "esnext",
     "moduleResolution": "node",
+    "declaration": false,
+    "importHelpers": true,
+    "noEmitHelpers": true,
+    "noEmitOnError": true,
+    "pretty": true,
+    "removeComments": true,
+    "sourceMap": true,
+    "stripInternal": true,
+    "inlineSourceMap": false,
     "emitDecoratorMetadata": true,
     "experimentalDecorators": true,
-    "importHelpers": true,
-    "target": "es5",
-    "typeRoots": [
-      "node_modules/@types"
-    ],
-    "lib": [
-      "es2017",
-      "dom"
-    ],
     "locale": "fr"
   }
 }
-- 
GitLab