@supersoniks/concorde 4.2.0 → 4.3.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.
- package/README.md +0 -0
- package/build-infos.json +1 -1
- package/concorde-core.bundle.js +175 -171
- package/concorde-core.es.js +2493 -2247
- package/dist/concorde-core.bundle.js +175 -171
- package/dist/concorde-core.es.js +2493 -2247
- package/docs/assets/{index-BbnRiebQ.js → index-B0IJ9I_B.js} +268 -242
- package/docs/assets/{index-BBv9CZqo.css → index-B3QHEJTV.css} +1 -1
- package/docs/index.html +2 -2
- package/docs/src/docs/_misc/bind.md +74 -0
- package/docs/src/docs/_misc/key.md +135 -0
- package/docs/src/docs/search/docs-search.json +310 -0
- package/docs/src/tsconfig-model.json +1 -1
- package/docs/src/tsconfig.json +322 -306
- package/package.json +22 -4
- package/php/get-challenge.php +0 -0
- package/php/some-service.php +0 -0
- package/scripts/pre-build.mjs +4 -0
- package/src/core/_types/endpoint.ts +4 -0
- package/src/core/_types/key.ts +1 -0
- package/src/core/components/functional/example/example.ts +38 -6
- package/src/core/components/ui/captcha/captcha.ts +12 -6
- package/src/core/decorators/Subscriber.ts +2 -0
- package/src/core/decorators/api.spec.ts +150 -0
- package/src/core/decorators/api.ts +244 -0
- package/src/core/decorators/subscriber/bind.ts +57 -145
- package/src/core/decorators/subscriber/dynamicPath.ts +77 -0
- package/src/core/decorators/subscriber/dynamicPropertyWatch.ts +105 -0
- package/src/core/decorators/subscriber/onAssign.ts +11 -147
- package/src/core/decorators/subscriber/publish.spec.ts +21 -0
- package/src/core/decorators/subscriber/publish.ts +148 -0
- package/src/core/decorators/subscriber/publisherPath.ts +13 -0
- package/src/core/decorators/subscriber/subscribe.spec.ts +21 -0
- package/src/core/decorators/subscriber/subscribe.ts +32 -0
- package/src/core/decorators/subscriber/subscribe.type-test.ts +32 -0
- package/src/core/utils/api.ts +83 -15
- package/src/core/utils/dataProviderKey.spec.ts +34 -0
- package/src/core/utils/dataProviderKey.ts +86 -0
- package/src/core/utils/endpoint.spec.ts +41 -0
- package/src/core/utils/endpoint.ts +87 -0
- package/src/decorators.ts +14 -0
- package/src/docs/{_misc → _decorators}/ancestor-attribute.md +15 -31
- package/src/docs/_decorators/bind.md +164 -0
- package/src/docs/_decorators/get.md +65 -0
- package/src/docs/_decorators/publish.md +54 -0
- package/src/docs/_decorators/subscribe.md +36 -0
- package/src/docs/_misc/dataProviderKey.md +135 -0
- package/src/docs/_misc/endpoint.md +42 -0
- package/src/docs/example/decorators-demo-bind-demos.ts +210 -0
- package/src/docs/example/decorators-demo-geo.ts +45 -0
- package/src/docs/example/decorators-demo-init.ts +228 -0
- package/src/docs/example/decorators-demo-subscribe-publish-get-demos.ts +324 -0
- package/src/docs/example/decorators-demo.ts +12 -459
- package/src/docs/navigation/navigation.ts +27 -10
- package/src/docs/search/docs-search.json +1059 -609
- package/src/tsconfig-model.json +1 -1
- package/src/tsconfig.json +65 -10
- package/src/tsconfig.tsbuildinfo +1 -1
- package/src/utils.ts +8 -1
- package/vite.config.mts +11 -0
- package/src/core/components/ui/modal/modal.stories.ts +0 -140
- package/src/docs/_misc/bind.md +0 -362
- /package/src/docs/{_misc → _decorators}/auto-subscribe.md +0 -0
- /package/src/docs/{_misc → _decorators}/on-assign.md +0 -0
- /package/src/docs/{_misc → _decorators}/wait-for-ancestors.md +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supersoniks/concorde",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "",
|
|
@@ -17,9 +17,12 @@
|
|
|
17
17
|
"exports": {
|
|
18
18
|
"./core/components/ui/icon/icons.json": "./src/core/components/ui/icon/icons.json",
|
|
19
19
|
"./core/components/functional/sdui/default-library.json": "./src/core/components/functional/sdui/default-library.json",
|
|
20
|
+
"./dataProviderKey": "./src/core/utils/dataProviderKey.ts",
|
|
20
21
|
"./*": "./src/*.ts",
|
|
21
22
|
"./vite-config": "./vite/config.js",
|
|
22
23
|
"./rollup-plugin-*": "./rollup/plugin-*.js",
|
|
24
|
+
"./_types/endpoint": "./src/core/_types/endpoint.ts",
|
|
25
|
+
"./_types/key": "./src/core/_types/key.ts",
|
|
23
26
|
"./_types/types": "./src/core/_types/types.ts",
|
|
24
27
|
"./date": "./src/core/components/functional/date/date.ts",
|
|
25
28
|
"./functional/date": "./src/core/components/functional/date/date.ts",
|
|
@@ -217,9 +220,6 @@
|
|
|
217
220
|
"./modal-title": "./src/core/components/ui/modal/modal-title.ts",
|
|
218
221
|
"./ui/modal-title": "./src/core/components/ui/modal/modal-title.ts",
|
|
219
222
|
"./ui/modal/modal-title": "./src/core/components/ui/modal/modal-title.ts",
|
|
220
|
-
"./modal.stories": "./src/core/components/ui/modal/modal.stories.ts",
|
|
221
|
-
"./ui/modal.stories": "./src/core/components/ui/modal/modal.stories.ts",
|
|
222
|
-
"./ui/modal/modal.stories": "./src/core/components/ui/modal/modal.stories.ts",
|
|
223
223
|
"./modal": "./src/core/components/ui/modal/modal.ts",
|
|
224
224
|
"./ui/modal": "./src/core/components/ui/modal/modal.ts",
|
|
225
225
|
"./pop": "./src/core/components/ui/pop/pop.ts",
|
|
@@ -274,13 +274,23 @@
|
|
|
274
274
|
"./ui/ui": "./src/core/components/ui/ui.ts",
|
|
275
275
|
"./core": "./src/core/core.ts",
|
|
276
276
|
"./decorators/Subscriber": "./src/core/decorators/Subscriber.ts",
|
|
277
|
+
"./decorators/api.spec": "./src/core/decorators/api.spec.ts",
|
|
278
|
+
"./decorators/api": "./src/core/decorators/api.ts",
|
|
277
279
|
"./decorators/lifecycle": "./src/core/decorators/lifecycle.ts",
|
|
278
280
|
"./decorators/subscriber/ancestorAttribute": "./src/core/decorators/subscriber/ancestorAttribute.ts",
|
|
279
281
|
"./decorators/subscriber/autoFill": "./src/core/decorators/subscriber/autoFill.ts",
|
|
280
282
|
"./decorators/subscriber/autoSubscribe": "./src/core/decorators/subscriber/autoSubscribe.ts",
|
|
281
283
|
"./decorators/subscriber/bind": "./src/core/decorators/subscriber/bind.ts",
|
|
282
284
|
"./decorators/subscriber/common": "./src/core/decorators/subscriber/common.ts",
|
|
285
|
+
"./decorators/subscriber/dynamicPath": "./src/core/decorators/subscriber/dynamicPath.ts",
|
|
286
|
+
"./decorators/subscriber/dynamicPropertyWatch": "./src/core/decorators/subscriber/dynamicPropertyWatch.ts",
|
|
283
287
|
"./decorators/subscriber/onAssign": "./src/core/decorators/subscriber/onAssign.ts",
|
|
288
|
+
"./decorators/subscriber/publish.spec": "./src/core/decorators/subscriber/publish.spec.ts",
|
|
289
|
+
"./decorators/subscriber/publish": "./src/core/decorators/subscriber/publish.ts",
|
|
290
|
+
"./decorators/subscriber/publisherPath": "./src/core/decorators/subscriber/publisherPath.ts",
|
|
291
|
+
"./decorators/subscriber/subscribe.spec": "./src/core/decorators/subscriber/subscribe.spec.ts",
|
|
292
|
+
"./decorators/subscriber/subscribe": "./src/core/decorators/subscriber/subscribe.ts",
|
|
293
|
+
"./decorators/subscriber/subscribe.type-test": "./src/core/decorators/subscriber/subscribe.type-test.ts",
|
|
284
294
|
"./directives/DataProvider": "./src/core/directives/DataProvider.ts",
|
|
285
295
|
"./directives/Wording": "./src/core/directives/Wording.ts",
|
|
286
296
|
"./mixins/Fetcher": "./src/core/mixins/Fetcher.ts",
|
|
@@ -301,11 +311,19 @@
|
|
|
301
311
|
"./utils/Utils": "./src/core/utils/Utils.ts",
|
|
302
312
|
"./utils/aesCrypto": "./src/core/utils/aesCrypto.ts",
|
|
303
313
|
"./utils/api": "./src/core/utils/api.ts",
|
|
314
|
+
"./utils/dataProviderKey.spec": "./src/core/utils/dataProviderKey.spec.ts",
|
|
315
|
+
"./utils/dataProviderKey": "./src/core/utils/dataProviderKey.ts",
|
|
316
|
+
"./utils/endpoint.spec": "./src/core/utils/endpoint.spec.ts",
|
|
317
|
+
"./utils/endpoint": "./src/core/utils/endpoint.ts",
|
|
304
318
|
"./utils/route.spec": "./src/core/utils/route.spec.ts",
|
|
305
319
|
"./utils/route": "./src/core/utils/route.ts",
|
|
306
320
|
"./utils/url-pattern": "./src/core/utils/url-pattern.ts",
|
|
307
321
|
"./code": "./src/docs/code.ts",
|
|
308
322
|
"./docs": "./src/docs/docs.ts",
|
|
323
|
+
"./example/decorators-demo-bind-demos": "./src/docs/example/decorators-demo-bind-demos.ts",
|
|
324
|
+
"./example/decorators-demo-geo": "./src/docs/example/decorators-demo-geo.ts",
|
|
325
|
+
"./example/decorators-demo-init": "./src/docs/example/decorators-demo-init.ts",
|
|
326
|
+
"./example/decorators-demo-subscribe-publish-get-demos": "./src/docs/example/decorators-demo-subscribe-publish-get-demos.ts",
|
|
309
327
|
"./example/decorators-demo": "./src/docs/example/decorators-demo.ts",
|
|
310
328
|
"./example/users": "./src/docs/example/users.ts",
|
|
311
329
|
"./header/header": "./src/docs/header/header.ts",
|
package/php/get-challenge.php
CHANGED
|
File without changes
|
package/php/some-service.php
CHANGED
|
File without changes
|
package/scripts/pre-build.mjs
CHANGED
|
@@ -99,6 +99,9 @@ jsonString = jsonString;
|
|
|
99
99
|
|
|
100
100
|
const tsConfig = JSON.parse(jsonString);
|
|
101
101
|
tsConfig.compilerOptions.paths = { ...shortPathMapping };
|
|
102
|
+
tsConfig.compilerOptions.paths["@supersoniks/concorde/dataProviderKey"] = [
|
|
103
|
+
path.resolve(__dirname, "../src/core/utils/dataProviderKey.ts"),
|
|
104
|
+
];
|
|
102
105
|
tsConfig.compilerOptions.paths["@supersoniks/concorde/*"] = ["./*"];
|
|
103
106
|
tsConfig.compilerOptions.paths["@concorde/*"] = ["./*"];
|
|
104
107
|
fs.writeFileSync(tsConfigPath, JSON.stringify(tsConfig, null, 2));
|
|
@@ -113,6 +116,7 @@ const pExports = {
|
|
|
113
116
|
"./src/core/components/ui/icon/icons.json",
|
|
114
117
|
"./core/components/functional/sdui/default-library.json":
|
|
115
118
|
"./src/core/components/functional/sdui/default-library.json",
|
|
119
|
+
"./dataProviderKey": "./src/core/utils/dataProviderKey.ts",
|
|
116
120
|
"./*": "./src/*.ts",
|
|
117
121
|
"./vite-config": "./vite/config.js",
|
|
118
122
|
"./rollup-plugin-*": "./rollup/plugin-*.js",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DataProviderKey } from "../utils/dataProviderKey";
|
|
@@ -1,11 +1,43 @@
|
|
|
1
|
-
import {html, LitElement} from "lit";
|
|
2
|
-
import {customElement, property} from "lit/decorators.js";
|
|
3
|
-
import
|
|
1
|
+
import { html, LitElement } from "lit";
|
|
2
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
3
|
+
import { Endpoint } from "@supersoniks/concorde/utils";
|
|
4
|
+
import { DataProviderKey } from "@supersoniks/concorde/core/utils/dataProviderKey";
|
|
5
|
+
import {
|
|
6
|
+
get,
|
|
7
|
+
publish,
|
|
8
|
+
subscribe,
|
|
9
|
+
ApiGetResult,
|
|
10
|
+
} from "@supersoniks/concorde/decorators";
|
|
11
|
+
|
|
12
|
+
/** Same API as the queue demo: https://geo.api.gouv.fr/ */
|
|
13
|
+
type City = { nom: string; code: string };
|
|
14
|
+
|
|
15
|
+
const endpoint = new Endpoint<City[], { limit: number }>(
|
|
16
|
+
"communes?limit=$limit&fields=nom,code",
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
/** Même path que l’endpoint ; type = charge utile `@get` pour `@publish`. */
|
|
20
|
+
const dpKey = new DataProviderKey<ApiGetResult<City[]>>(endpoint.path);
|
|
21
|
+
|
|
4
22
|
const tagName = "sonic-example"; // For Astro.build
|
|
5
23
|
@customElement(tagName)
|
|
6
|
-
export class SonicComponent extends
|
|
7
|
-
@property(
|
|
24
|
+
export class SonicComponent extends LitElement {
|
|
25
|
+
@property({ type: Number })
|
|
26
|
+
limit = 5;
|
|
27
|
+
|
|
28
|
+
@get(endpoint)
|
|
29
|
+
@publish(dpKey)
|
|
30
|
+
geoCommunesPayload?: ApiGetResult<City[]>;
|
|
31
|
+
|
|
32
|
+
@state()
|
|
33
|
+
@subscribe(dpKey.result)
|
|
34
|
+
geoCommunesResult?: City[];
|
|
35
|
+
|
|
8
36
|
render() {
|
|
9
|
-
return html
|
|
37
|
+
return html` <span part="api-get-demo">
|
|
38
|
+
· get: ${JSON.stringify(this.geoCommunesPayload?.result ?? null)} · HTTP
|
|
39
|
+
${this.geoCommunesPayload?.response?.status ?? "—"} · subscribe:
|
|
40
|
+
${JSON.stringify(this.geoCommunesResult ?? null)}</span
|
|
41
|
+
>`;
|
|
10
42
|
}
|
|
11
43
|
}
|
|
@@ -3,7 +3,10 @@ import { dp, HTML } from "@supersoniks/concorde/utils";
|
|
|
3
3
|
import { css, html, LitElement, nothing } from "lit";
|
|
4
4
|
import { customElement, property } from "lit/decorators.js";
|
|
5
5
|
import { ConcordeWindow } from "@supersoniks/concorde/core/_types/types";
|
|
6
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
DataProvider,
|
|
8
|
+
Publisher,
|
|
9
|
+
} from "@supersoniks/concorde/core/utils/PublisherProxy";
|
|
7
10
|
import {
|
|
8
11
|
generateKey,
|
|
9
12
|
encryptToBase64,
|
|
@@ -72,7 +75,8 @@ export class Captcha extends Subscriber(LitElement) {
|
|
|
72
75
|
script.type = "module";
|
|
73
76
|
this.setAttribute("async", "");
|
|
74
77
|
this.setAttribute("defer", "");
|
|
75
|
-
script.src =
|
|
78
|
+
script.src =
|
|
79
|
+
"https://cdn.jsdelivr.net/gh/altcha-org/altcha/dist/altcha.min.js";
|
|
76
80
|
scriptAdded = true;
|
|
77
81
|
document.head.appendChild(script);
|
|
78
82
|
}
|
|
@@ -84,7 +88,7 @@ export class Captcha extends Subscriber(LitElement) {
|
|
|
84
88
|
captchaToken: string;
|
|
85
89
|
}>(
|
|
86
90
|
this.getAncestorAttributeValue("headersDataProvider") ??
|
|
87
|
-
this.getAncestorAttributeValue("formDataProvider")
|
|
91
|
+
this.getAncestorAttributeValue("formDataProvider"),
|
|
88
92
|
);
|
|
89
93
|
|
|
90
94
|
if (
|
|
@@ -93,14 +97,14 @@ export class Captcha extends Subscriber(LitElement) {
|
|
|
93
97
|
) {
|
|
94
98
|
this.formPublisher.needsCaptchaValidation.set(true);
|
|
95
99
|
(this.formPublisher.captchaToken as Publisher<string>).onAssign(
|
|
96
|
-
this.onCaptchaTokenChanged
|
|
100
|
+
this.onCaptchaTokenChanged,
|
|
97
101
|
);
|
|
98
102
|
}
|
|
99
103
|
}
|
|
100
104
|
disconnectedCallback(): void {
|
|
101
105
|
if (this.formPublisher) {
|
|
102
106
|
(this.formPublisher.captchaToken as Publisher<string>).offAssign(
|
|
103
|
-
this.onCaptchaTokenChanged
|
|
107
|
+
this.onCaptchaTokenChanged,
|
|
104
108
|
);
|
|
105
109
|
this.formPublisher.captchaToken.set("");
|
|
106
110
|
this.formPublisher.needsCaptchaValidation.set(false);
|
|
@@ -115,7 +119,9 @@ export class Captcha extends Subscriber(LitElement) {
|
|
|
115
119
|
if (!form) return;
|
|
116
120
|
const formData = new FormData(form);
|
|
117
121
|
this.formPublisher.captchaKey.set(this.key);
|
|
118
|
-
this.formPublisher.captchaToken.set(
|
|
122
|
+
this.formPublisher.captchaToken.set(
|
|
123
|
+
formData.get("altcha")?.toString() || "",
|
|
124
|
+
);
|
|
119
125
|
}
|
|
120
126
|
|
|
121
127
|
async generateEncryptedKey() {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { bind } from "./subscriber/bind";
|
|
2
|
+
export { publish } from "./subscriber/publish";
|
|
3
|
+
export { subscribe } from "./subscriber/subscribe";
|
|
2
4
|
export { onAssign } from "./subscriber/onAssign";
|
|
3
5
|
export { autoSubscribe } from "./subscriber/autoSubscribe";
|
|
4
6
|
export { autoFill } from "./subscriber/autoFill";
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { Endpoint } from "../utils/endpoint";
|
|
3
|
+
import { DataProviderKey } from "../utils/dataProviderKey";
|
|
4
|
+
import API, {
|
|
5
|
+
APIConfiguration,
|
|
6
|
+
type ApiGetResult,
|
|
7
|
+
} from "@supersoniks/concorde/core/utils/api";
|
|
8
|
+
import { PublisherManager } from "@supersoniks/concorde/core/utils/PublisherProxy";
|
|
9
|
+
import { get } from "./api";
|
|
10
|
+
|
|
11
|
+
const staticConfig: APIConfiguration = {
|
|
12
|
+
serviceURL: "https://api.example.test",
|
|
13
|
+
token: null,
|
|
14
|
+
userName: null,
|
|
15
|
+
password: null,
|
|
16
|
+
authToken: null,
|
|
17
|
+
tokenProvider: null,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
async function flushGetMicrotasks() {
|
|
21
|
+
await Promise.resolve();
|
|
22
|
+
await Promise.resolve();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function mockPayload<T>(result: T) {
|
|
26
|
+
return {
|
|
27
|
+
request: new Request("https://api.example.test/items/1"),
|
|
28
|
+
response: new Response(JSON.stringify(result), {
|
|
29
|
+
status: 200,
|
|
30
|
+
headers: { "Content-Type": "application/json" },
|
|
31
|
+
}),
|
|
32
|
+
result,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe("get", () => {
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
vi.spyOn(API.prototype, "getDetailed").mockResolvedValue(
|
|
39
|
+
mockPayload({ id: "loaded" }) as never,
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
vi.restoreAllMocks();
|
|
45
|
+
document.body.replaceChildren();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("assigne ApiGetResult avec configurationKey (publisher)", async () => {
|
|
49
|
+
const pubId = "getSpecPub1";
|
|
50
|
+
PublisherManager.get(pubId).set(staticConfig);
|
|
51
|
+
const confKey = new DataProviderKey<APIConfiguration>(pubId);
|
|
52
|
+
|
|
53
|
+
const itemEp = new Endpoint<{ id: string }>("items/1");
|
|
54
|
+
const tag = "test-api-get-config-key";
|
|
55
|
+
class C extends HTMLElement {
|
|
56
|
+
@get(itemEp, confKey)
|
|
57
|
+
payload: ApiGetResult<{ id: string }> | null = null;
|
|
58
|
+
}
|
|
59
|
+
if (!customElements.get(tag)) {
|
|
60
|
+
customElements.define(tag, C);
|
|
61
|
+
}
|
|
62
|
+
const el = document.createElement(tag) as InstanceType<typeof C>;
|
|
63
|
+
document.body.appendChild(el);
|
|
64
|
+
await flushGetMicrotasks();
|
|
65
|
+
expect(el.payload?.result).toEqual({ id: "loaded" });
|
|
66
|
+
expect(el.payload?.response?.ok).toBe(true);
|
|
67
|
+
expect(el.payload?.request.url).toContain("api.example.test");
|
|
68
|
+
expect(API.prototype.getDetailed).toHaveBeenCalledWith("items/1");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("endpoint dynamique avec configurationKey", async () => {
|
|
72
|
+
const pubId = "getSpecPub2";
|
|
73
|
+
PublisherManager.get(pubId).set(staticConfig);
|
|
74
|
+
const confKey = new DataProviderKey<APIConfiguration>(pubId);
|
|
75
|
+
const rowEp = new Endpoint<{ id: string }>("rows/${rowId}");
|
|
76
|
+
|
|
77
|
+
class TestRow extends HTMLElement {
|
|
78
|
+
rowId = "42";
|
|
79
|
+
|
|
80
|
+
@get(rowEp, confKey)
|
|
81
|
+
payload: ApiGetResult<{ id: string }> | null = null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const tag = "test-api-get-row";
|
|
85
|
+
if (!customElements.get(tag)) {
|
|
86
|
+
customElements.define(tag, TestRow);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const el = document.createElement(tag) as InstanceType<typeof TestRow>;
|
|
90
|
+
document.body.appendChild(el);
|
|
91
|
+
await flushGetMicrotasks();
|
|
92
|
+
|
|
93
|
+
expect(API.prototype.getDetailed).toHaveBeenCalledWith("rows/42");
|
|
94
|
+
expect(el.payload?.result).toEqual({ id: "loaded" });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("mode scoped lit la configuration sur les ancêtres (getApiConfiguration)", async () => {
|
|
98
|
+
const dataEp = new Endpoint<{ id: string }>("scoped-endpoint");
|
|
99
|
+
|
|
100
|
+
class TestScoped extends HTMLElement {
|
|
101
|
+
@get(dataEp)
|
|
102
|
+
payload: ApiGetResult<{ id: string }> | null = null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const tag = "test-api-get-scoped";
|
|
106
|
+
if (!customElements.get(tag)) {
|
|
107
|
+
customElements.define(tag, TestScoped);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const wrap = document.createElement("div");
|
|
111
|
+
wrap.setAttribute("serviceURL", "https://from-ancestor.test");
|
|
112
|
+
const el = document.createElement(tag) as InstanceType<typeof TestScoped>;
|
|
113
|
+
wrap.appendChild(el);
|
|
114
|
+
document.body.appendChild(wrap);
|
|
115
|
+
|
|
116
|
+
await flushGetMicrotasks();
|
|
117
|
+
|
|
118
|
+
expect(API.prototype.getDetailed).toHaveBeenCalledWith("scoped-endpoint");
|
|
119
|
+
expect(el.payload?.result).toEqual({ id: "loaded" });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("relance le GET quand le publisher de config mute (onInternalMutation)", async () => {
|
|
123
|
+
const pubId = "getSpecPubMut";
|
|
124
|
+
const pub = PublisherManager.get(pubId);
|
|
125
|
+
pub.set(staticConfig);
|
|
126
|
+
const confKey = new DataProviderKey<APIConfiguration>(pubId);
|
|
127
|
+
const itemEp = new Endpoint<{ id: string }>("items/1");
|
|
128
|
+
const tag = "test-api-get-mutation";
|
|
129
|
+
|
|
130
|
+
class C extends HTMLElement {
|
|
131
|
+
@get(itemEp, confKey)
|
|
132
|
+
payload: ApiGetResult<{ id: string }> | null = null;
|
|
133
|
+
}
|
|
134
|
+
if (!customElements.get(tag)) {
|
|
135
|
+
customElements.define(tag, C);
|
|
136
|
+
}
|
|
137
|
+
const el = document.createElement(tag) as InstanceType<typeof C>;
|
|
138
|
+
document.body.appendChild(el);
|
|
139
|
+
await flushGetMicrotasks();
|
|
140
|
+
expect(API.prototype.getDetailed).toHaveBeenCalledTimes(1);
|
|
141
|
+
|
|
142
|
+
pub.set({
|
|
143
|
+
...staticConfig,
|
|
144
|
+
serviceURL: "https://api.example.test/v2",
|
|
145
|
+
});
|
|
146
|
+
await flushGetMicrotasks();
|
|
147
|
+
expect(API.prototype.getDetailed).toHaveBeenCalledTimes(2);
|
|
148
|
+
expect(el.payload?.result).toEqual({ id: "loaded" });
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DataProviderKey,
|
|
3
|
+
DataProviderKeyHost,
|
|
4
|
+
} from "../utils/dataProviderKey";
|
|
5
|
+
import { Endpoint } from "../utils/endpoint";
|
|
6
|
+
import HTML, {
|
|
7
|
+
SearchableDomElement,
|
|
8
|
+
} from "@supersoniks/concorde/core/utils/HTML";
|
|
9
|
+
import API, {
|
|
10
|
+
APIConfiguration,
|
|
11
|
+
type ApiGetResult,
|
|
12
|
+
} from "@supersoniks/concorde/core/utils/api";
|
|
13
|
+
import DataProvider from "../utils/PublisherProxy";
|
|
14
|
+
import { ConnectedComponent, setSubscribable } from "./subscriber/common";
|
|
15
|
+
import {
|
|
16
|
+
extractDynamicDependencies,
|
|
17
|
+
resolveDynamicPath,
|
|
18
|
+
} from "./subscriber/dynamicPath";
|
|
19
|
+
import {
|
|
20
|
+
getDynamicWatchKeys,
|
|
21
|
+
registerDynamicPropertyWatcher,
|
|
22
|
+
} from "./subscriber/dynamicPropertyWatch";
|
|
23
|
+
import { getPublisherFromPath } from "./subscriber/publisherPath";
|
|
24
|
+
|
|
25
|
+
function asSearchableHost(component: unknown): SearchableDomElement | null {
|
|
26
|
+
if (component instanceof HTMLElement || component instanceof ShadowRoot) {
|
|
27
|
+
return component;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readApiConfigurationFromPublisher(
|
|
33
|
+
publisher: DataProvider | null,
|
|
34
|
+
): APIConfiguration | null {
|
|
35
|
+
if (!publisher || typeof publisher.get !== "function") return null;
|
|
36
|
+
const raw = publisher.get();
|
|
37
|
+
if (!raw || typeof raw !== "object") return null;
|
|
38
|
+
if (!("serviceURL" in (raw as object))) return null;
|
|
39
|
+
return raw as APIConfiguration;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function resolveScopedConfiguration(
|
|
43
|
+
component: unknown,
|
|
44
|
+
): APIConfiguration | null {
|
|
45
|
+
const host = asSearchableHost(component);
|
|
46
|
+
if (!host) return null;
|
|
47
|
+
return HTML.getApiConfiguration(host);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type ApiGetState = {
|
|
51
|
+
cleanupWatchers: Array<() => void>;
|
|
52
|
+
requestGeneration: number;
|
|
53
|
+
configPublisher: DataProvider | null;
|
|
54
|
+
configMutationHandler: (() => void) | null;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function detachConfigPublisher(state: ApiGetState): void {
|
|
58
|
+
if (state.configPublisher && state.configMutationHandler) {
|
|
59
|
+
state.configPublisher.offInternalMutation(state.configMutationHandler);
|
|
60
|
+
}
|
|
61
|
+
state.configPublisher = null;
|
|
62
|
+
state.configMutationHandler = null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Décorateur **`@get`** : charge des données via `API.getDetailed` et assigne un
|
|
67
|
+
* `ApiGetResult<T>` (`request`, `response`, `result` typé `T`) ou `null`.
|
|
68
|
+
* Le path est un `Endpoint<T, Ue>` ; les placeholders `${nomPropriété}` sont résolus sur l'instance (`Ue` contraint l’hôte).
|
|
69
|
+
*
|
|
70
|
+
* **Scoped (défaut)** : `HTML.getApiConfiguration(host)` avec `host` = l’élément connecté
|
|
71
|
+
* (`HTMLElement` / `ShadowRoot`). Sans hôte DOM valide, la propriété reste inchangée jusqu’à connexion.
|
|
72
|
+
*
|
|
73
|
+
* **Deuxième paramètre** : `DataProviderKey<APIConfiguration>` — la config est lue via
|
|
74
|
+
* `PublisherManager` sur le chemin résolu (même syntaxe dynamique que `@subscribe`).
|
|
75
|
+
* Toute mutation interne du publisher (`onInternalMutation`) relance le GET.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* @get(new Endpoint<User, { userId: string }>("users/${userId}"))
|
|
79
|
+
* payload?: ApiGetResult<User>;
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* const apiConf = new DataProviderKey<APIConfiguration>("myApiConf");
|
|
83
|
+
* PublisherManager.get("myApiConf").set({ serviceURL: "...", token: null, ... });
|
|
84
|
+
* @get(new Endpoint<Thing, { id: string }>("things/${id}"), apiConf)
|
|
85
|
+
* payload?: ApiGetResult<Thing>;
|
|
86
|
+
*/
|
|
87
|
+
export function get<T, Ue = any>(
|
|
88
|
+
endpoint: Endpoint<T, Ue>,
|
|
89
|
+
): <K extends string>(
|
|
90
|
+
target: DataProviderKeyHost<Ue> & {
|
|
91
|
+
[P in K]?: ApiGetResult<T> | null | undefined;
|
|
92
|
+
},
|
|
93
|
+
propertyKey: K,
|
|
94
|
+
) => void;
|
|
95
|
+
export function get<T, Ue = any, Uk = any>(
|
|
96
|
+
endpoint: Endpoint<T, Ue>,
|
|
97
|
+
configurationKey: DataProviderKey<APIConfiguration, Uk>,
|
|
98
|
+
): <K extends string>(
|
|
99
|
+
target: DataProviderKeyHost<Ue> &
|
|
100
|
+
DataProviderKeyHost<Uk> & {
|
|
101
|
+
[P in K]?: ApiGetResult<T> | null | undefined;
|
|
102
|
+
},
|
|
103
|
+
propertyKey: K,
|
|
104
|
+
) => void;
|
|
105
|
+
export function get<T, Ue = any, Uk = any>(
|
|
106
|
+
endpoint: Endpoint<T, Ue>,
|
|
107
|
+
configurationKey?: DataProviderKey<APIConfiguration, Uk>,
|
|
108
|
+
): <K extends string>(
|
|
109
|
+
target: object & { [P in K]?: ApiGetResult<T> | null | undefined },
|
|
110
|
+
propertyKey: K,
|
|
111
|
+
) => void {
|
|
112
|
+
const pathTemplate = endpoint.path;
|
|
113
|
+
const configurationKeyPath = configurationKey?.path;
|
|
114
|
+
const endpointDynamicDependencies = extractDynamicDependencies(pathTemplate);
|
|
115
|
+
const configKeyDynamicDependencies = configurationKeyPath
|
|
116
|
+
? extractDynamicDependencies(configurationKeyPath)
|
|
117
|
+
: [];
|
|
118
|
+
const mergedDynamicDependencies = [
|
|
119
|
+
...new Set([
|
|
120
|
+
...endpointDynamicDependencies,
|
|
121
|
+
...configKeyDynamicDependencies,
|
|
122
|
+
]),
|
|
123
|
+
];
|
|
124
|
+
const isDynamicPath = endpointDynamicDependencies.length > 0;
|
|
125
|
+
const usesPublisherConfig = Boolean(configurationKeyPath);
|
|
126
|
+
|
|
127
|
+
return function (target: object, propertyKey: string) {
|
|
128
|
+
if (!target) return;
|
|
129
|
+
setSubscribable(target);
|
|
130
|
+
const stateKey = `__get_state_${propertyKey}`;
|
|
131
|
+
|
|
132
|
+
(target as ConnectedComponent).__onConnected__((component) => {
|
|
133
|
+
const comp = component as Record<string, unknown>;
|
|
134
|
+
let state = comp[stateKey] as ApiGetState | undefined;
|
|
135
|
+
if (!state) {
|
|
136
|
+
state = {
|
|
137
|
+
cleanupWatchers: [],
|
|
138
|
+
requestGeneration: 0,
|
|
139
|
+
configPublisher: null,
|
|
140
|
+
configMutationHandler: null,
|
|
141
|
+
};
|
|
142
|
+
comp[stateKey] = state;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
state.cleanupWatchers.forEach((cleanup) => cleanup());
|
|
146
|
+
state.cleanupWatchers = [];
|
|
147
|
+
state.requestGeneration++;
|
|
148
|
+
|
|
149
|
+
const runFetch = () => {
|
|
150
|
+
const resolution = isDynamicPath
|
|
151
|
+
? resolveDynamicPath(component, pathTemplate)
|
|
152
|
+
: { ready: true, path: pathTemplate };
|
|
153
|
+
if (!resolution.ready || !resolution.path) {
|
|
154
|
+
comp[propertyKey] = undefined;
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
let config: APIConfiguration | null = null;
|
|
158
|
+
if (usesPublisherConfig && configurationKeyPath) {
|
|
159
|
+
const configRes = resolveDynamicPath(component, configurationKeyPath);
|
|
160
|
+
if (!configRes.ready || !configRes.path) {
|
|
161
|
+
comp[propertyKey] = undefined;
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const configPublisher = getPublisherFromPath(configRes.path);
|
|
165
|
+
config = readApiConfigurationFromPublisher(configPublisher);
|
|
166
|
+
} else {
|
|
167
|
+
config = resolveScopedConfiguration(component);
|
|
168
|
+
}
|
|
169
|
+
if (!config) {
|
|
170
|
+
comp[propertyKey] = undefined;
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const generation = ++state.requestGeneration;
|
|
174
|
+
const api = new API(config);
|
|
175
|
+
void api
|
|
176
|
+
.getDetailed<T>(resolution.path)
|
|
177
|
+
.then((payload?: ApiGetResult<T>) => {
|
|
178
|
+
if (generation !== state.requestGeneration) return;
|
|
179
|
+
comp[propertyKey] = payload;
|
|
180
|
+
});
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const rebindPublisherConfig = () => {
|
|
184
|
+
if (!usesPublisherConfig || !configurationKeyPath) return;
|
|
185
|
+
detachConfigPublisher(state);
|
|
186
|
+
const configRes = resolveDynamicPath(component, configurationKeyPath);
|
|
187
|
+
if (!configRes.ready || !configRes.path) {
|
|
188
|
+
comp[propertyKey] = undefined;
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const publisher = getPublisherFromPath(configRes.path);
|
|
192
|
+
if (!publisher) {
|
|
193
|
+
comp[propertyKey] = undefined;
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const mutationHandler = () => {
|
|
197
|
+
runFetch();
|
|
198
|
+
};
|
|
199
|
+
publisher.onInternalMutation(mutationHandler);
|
|
200
|
+
state.configPublisher = publisher;
|
|
201
|
+
state.configMutationHandler = mutationHandler;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
if (usesPublisherConfig) {
|
|
205
|
+
for (const dependency of mergedDynamicDependencies) {
|
|
206
|
+
const unsubscribe = registerDynamicPropertyWatcher(
|
|
207
|
+
getDynamicWatchKeys.watcherStore,
|
|
208
|
+
getDynamicWatchKeys.hooked,
|
|
209
|
+
component,
|
|
210
|
+
dependency,
|
|
211
|
+
() => rebindPublisherConfig(),
|
|
212
|
+
);
|
|
213
|
+
state.cleanupWatchers.push(unsubscribe);
|
|
214
|
+
}
|
|
215
|
+
rebindPublisherConfig();
|
|
216
|
+
} else {
|
|
217
|
+
if (isDynamicPath) {
|
|
218
|
+
for (const dependency of endpointDynamicDependencies) {
|
|
219
|
+
const unsubscribe = registerDynamicPropertyWatcher(
|
|
220
|
+
getDynamicWatchKeys.watcherStore,
|
|
221
|
+
getDynamicWatchKeys.hooked,
|
|
222
|
+
component,
|
|
223
|
+
dependency,
|
|
224
|
+
() => runFetch(),
|
|
225
|
+
);
|
|
226
|
+
state.cleanupWatchers.push(unsubscribe);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
runFetch();
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
(target as ConnectedComponent).__onDisconnected__((component) => {
|
|
234
|
+
const comp = component as Record<string, unknown>;
|
|
235
|
+
const state = comp[stateKey] as ApiGetState | undefined;
|
|
236
|
+
if (!state) return;
|
|
237
|
+
detachConfigPublisher(state);
|
|
238
|
+
state.cleanupWatchers.forEach((cleanup) => cleanup());
|
|
239
|
+
state.cleanupWatchers = [];
|
|
240
|
+
state.requestGeneration++;
|
|
241
|
+
comp[propertyKey] = undefined;
|
|
242
|
+
});
|
|
243
|
+
};
|
|
244
|
+
}
|