@supersoniks/concorde 4.2.1 → 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 +163 -0
- package/build-infos.json +1 -1
- package/concorde-core.bundle.js +175 -171
- package/concorde-core.es.js +2490 -2246
- package/dist/concorde-core.bundle.js +175 -171
- package/dist/concorde-core.es.js +2490 -2246
- package/package.json +22 -1
- package/php/get-challenge.php +34 -0
- package/php/some-service.php +42 -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/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 -1
- package/src/tsconfig.tsbuildinfo +1 -1
- package/src/utils.ts +8 -1
- package/vite.config.mts +11 -0
- 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
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DataProviderKey,
|
|
3
|
+
DataProviderKeyHost,
|
|
4
|
+
} from "../../utils/dataProviderKey";
|
|
5
|
+
import DataProvider from "../../utils/PublisherProxy";
|
|
6
|
+
import { ConnectedComponent, setSubscribable } from "./common";
|
|
7
|
+
import { extractDynamicDependencies, resolveDynamicPath } from "./dynamicPath";
|
|
8
|
+
import {
|
|
9
|
+
publishDynamicWatchKeys,
|
|
10
|
+
registerDynamicPropertyWatcher,
|
|
11
|
+
} from "./dynamicPropertyWatch";
|
|
12
|
+
import { getPublisherFromPath } from "./publisherPath";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Publishes property writes to a publisher path. Inverse of @subscribe.
|
|
16
|
+
* When the property is set, the value is written to the publisher (reflect-only, no subscription).
|
|
17
|
+
* The decorated property is typed as `T` (or optional / `| null` / `| undefined` for Lit / TS 5).
|
|
18
|
+
* Supports dynamic paths: use placeholders like "users.${userIndex}.email" in the DataProviderKey.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* const formKey = new DataProviderKey<FormData>("formData");
|
|
22
|
+
* @publish(formKey.email)
|
|
23
|
+
* @state()
|
|
24
|
+
* email = "";
|
|
25
|
+
*
|
|
26
|
+
* // Dynamic path + hôte typé via le 2ᵉ générique de la clé :
|
|
27
|
+
* @publish(new DataProviderKey<string, { userIndex: number }>("users.${userIndex}.email"))
|
|
28
|
+
* email = "";
|
|
29
|
+
*/
|
|
30
|
+
export function publish<T, U = any>(
|
|
31
|
+
key: DataProviderKey<T, U>,
|
|
32
|
+
): <K extends string>(
|
|
33
|
+
target: DataProviderKeyHost<U> & { [P in K]?: T | null | undefined },
|
|
34
|
+
propertyKey: K,
|
|
35
|
+
) => void {
|
|
36
|
+
const path = key.path;
|
|
37
|
+
const dynamicDependencies = extractDynamicDependencies(path);
|
|
38
|
+
|
|
39
|
+
return function (target: object, propertyKey: string) {
|
|
40
|
+
setSubscribable(target);
|
|
41
|
+
const publisherKey = `__publish_${propertyKey}_publisher__`;
|
|
42
|
+
const internalValueKey = `__publish_${propertyKey}_value__`;
|
|
43
|
+
|
|
44
|
+
const existingDescriptor = Object.getOwnPropertyDescriptor(
|
|
45
|
+
target as object,
|
|
46
|
+
propertyKey,
|
|
47
|
+
);
|
|
48
|
+
const initialValue =
|
|
49
|
+
existingDescriptor && !existingDescriptor.get && !existingDescriptor.set
|
|
50
|
+
? existingDescriptor.value
|
|
51
|
+
: undefined;
|
|
52
|
+
|
|
53
|
+
Object.defineProperty(target as object, propertyKey, {
|
|
54
|
+
get() {
|
|
55
|
+
if (existingDescriptor?.get) {
|
|
56
|
+
return existingDescriptor.get.call(this);
|
|
57
|
+
}
|
|
58
|
+
if (
|
|
59
|
+
!Object.prototype.hasOwnProperty.call(this, internalValueKey) &&
|
|
60
|
+
initialValue !== undefined
|
|
61
|
+
) {
|
|
62
|
+
(this as Record<string, unknown>)[internalValueKey] = initialValue;
|
|
63
|
+
}
|
|
64
|
+
return (this as Record<string, unknown>)[internalValueKey];
|
|
65
|
+
},
|
|
66
|
+
set(newValue: unknown) {
|
|
67
|
+
if (existingDescriptor?.set) {
|
|
68
|
+
existingDescriptor.set.call(this, newValue);
|
|
69
|
+
} else {
|
|
70
|
+
(this as Record<string, unknown>)[internalValueKey] = newValue;
|
|
71
|
+
}
|
|
72
|
+
const publisher = (this as Record<string, unknown>)[publisherKey] as
|
|
73
|
+
| DataProvider
|
|
74
|
+
| undefined;
|
|
75
|
+
if (publisher) {
|
|
76
|
+
publisher.set(newValue);
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
enumerable: existingDescriptor?.enumerable ?? true,
|
|
80
|
+
configurable: existingDescriptor?.configurable ?? true,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
(target as ConnectedComponent).__onConnected__((component) => {
|
|
84
|
+
const comp = component as Record<string, unknown>;
|
|
85
|
+
const stateKey = `__publish_state_${propertyKey}`;
|
|
86
|
+
type PublishState = { cleanupWatchers: Array<() => void> };
|
|
87
|
+
const state: PublishState =
|
|
88
|
+
(comp[stateKey] as PublishState) ||
|
|
89
|
+
((comp[stateKey] as PublishState) = {
|
|
90
|
+
cleanupWatchers: [],
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const updatePublisher = () => {
|
|
94
|
+
let resolvedPath: string | null;
|
|
95
|
+
if (dynamicDependencies.length) {
|
|
96
|
+
const resolution = resolveDynamicPath(component, path);
|
|
97
|
+
resolvedPath = resolution.ready ? resolution.path : null;
|
|
98
|
+
} else {
|
|
99
|
+
resolvedPath = path;
|
|
100
|
+
}
|
|
101
|
+
const publisher = resolvedPath
|
|
102
|
+
? getPublisherFromPath(resolvedPath)
|
|
103
|
+
: undefined;
|
|
104
|
+
comp[publisherKey] = publisher ?? null;
|
|
105
|
+
if (publisher && propertyKey in component) {
|
|
106
|
+
const currentValue = comp[propertyKey];
|
|
107
|
+
if (currentValue !== undefined) {
|
|
108
|
+
publisher.set(currentValue);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
state.cleanupWatchers.forEach((cleanup: () => void) => cleanup());
|
|
114
|
+
state.cleanupWatchers = [];
|
|
115
|
+
|
|
116
|
+
if (dynamicDependencies.length) {
|
|
117
|
+
for (const dependency of dynamicDependencies) {
|
|
118
|
+
state.cleanupWatchers.push(
|
|
119
|
+
registerDynamicPropertyWatcher(
|
|
120
|
+
publishDynamicWatchKeys.watcherStore,
|
|
121
|
+
publishDynamicWatchKeys.hooked,
|
|
122
|
+
comp,
|
|
123
|
+
dependency,
|
|
124
|
+
updatePublisher,
|
|
125
|
+
),
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
updatePublisher();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
(target as ConnectedComponent).__onDisconnected__((component) => {
|
|
134
|
+
const comp = component as Record<string, unknown>;
|
|
135
|
+
const stateKey = `__publish_state_${propertyKey}`;
|
|
136
|
+
const state = comp[stateKey] as
|
|
137
|
+
| { cleanupWatchers: Array<() => void> }
|
|
138
|
+
| undefined;
|
|
139
|
+
if (state?.cleanupWatchers) {
|
|
140
|
+
state.cleanupWatchers.forEach((cleanup: () => void) => cleanup());
|
|
141
|
+
}
|
|
142
|
+
comp[publisherKey] = undefined;
|
|
143
|
+
});
|
|
144
|
+
} as <K extends string>(
|
|
145
|
+
target: DataProviderKeyHost<U> & { [P in K]?: T | null | undefined },
|
|
146
|
+
propertyKey: K,
|
|
147
|
+
) => void;
|
|
148
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Objects } from "@supersoniks/concorde/utils";
|
|
2
|
+
import DataProvider, { PublisherManager } from "../../utils/PublisherProxy";
|
|
3
|
+
|
|
4
|
+
export function getPublisherFromPath(path: string): DataProvider | null {
|
|
5
|
+
const segments = path.split(".").filter((segment) => segment.length > 0);
|
|
6
|
+
if (segments.length === 0) return null;
|
|
7
|
+
const dataProvider = segments.shift() || "";
|
|
8
|
+
if (!dataProvider) return null;
|
|
9
|
+
let publisher = PublisherManager.get(dataProvider);
|
|
10
|
+
if (!publisher) return null;
|
|
11
|
+
publisher = Objects.traverse(publisher, segments);
|
|
12
|
+
return publisher as DataProvider | null;
|
|
13
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { DataProviderKey } from "../../utils/dataProviderKey";
|
|
3
|
+
import { subscribe } from "./subscribe";
|
|
4
|
+
|
|
5
|
+
type ControlStats = {
|
|
6
|
+
gauge: number;
|
|
7
|
+
checkedTickets: { id: string }[];
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const statsKey = new DataProviderKey<ControlStats>("stats");
|
|
11
|
+
|
|
12
|
+
describe("subscribe", () => {
|
|
13
|
+
it("accepts DataProviderKey and binds to path", () => {
|
|
14
|
+
class TestClass {
|
|
15
|
+
@subscribe(statsKey.gauge)
|
|
16
|
+
gauge = 0;
|
|
17
|
+
}
|
|
18
|
+
const instance = new TestClass();
|
|
19
|
+
expect(instance.gauge).toBe(0);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DataProviderKey,
|
|
3
|
+
DataProviderKeyHost,
|
|
4
|
+
} from "../../utils/dataProviderKey";
|
|
5
|
+
import { bind } from "./bind";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Read-only subscription to a publisher path via DataProviderKey<T>. No reflect.
|
|
9
|
+
* The decorated property is typed as `T` (or optional / `| null` / `| undefined` for Lit / TS 5).
|
|
10
|
+
* Supports dynamic paths: use placeholders like "users.${userIndex}" in the DataProviderKey.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* const dataKey = new DataProviderKey<Data>("data");
|
|
14
|
+
* @subscribe(dataKey.count)
|
|
15
|
+
* @state()
|
|
16
|
+
* count: number;
|
|
17
|
+
*
|
|
18
|
+
* // Dynamic path:
|
|
19
|
+
* @subscribe(new DataProviderKey<User>("users.${userIndex}"))
|
|
20
|
+
* user: User | null;
|
|
21
|
+
*/
|
|
22
|
+
export function subscribe<T, U = any>(
|
|
23
|
+
key: DataProviderKey<T, U>,
|
|
24
|
+
): <K extends string>(
|
|
25
|
+
target: DataProviderKeyHost<U> & { [P in K]?: T | null | undefined },
|
|
26
|
+
propertyKey: K,
|
|
27
|
+
) => void {
|
|
28
|
+
return bind(key) as <K extends string>(
|
|
29
|
+
target: DataProviderKeyHost<U> & { [P in K]?: T | null | undefined },
|
|
30
|
+
propertyKey: K,
|
|
31
|
+
) => void;
|
|
32
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type tests for subscribe — ensures property type must match DataProviderKey<T>.
|
|
3
|
+
* Excluded from build. To verify: temporarily remove from exclude and run build.
|
|
4
|
+
* Expected: InvalidUsage and InvalidUsage2 should produce type errors.
|
|
5
|
+
*/
|
|
6
|
+
import { DataProviderKey } from "../../utils/dataProviderKey";
|
|
7
|
+
import { subscribe } from "./subscribe";
|
|
8
|
+
|
|
9
|
+
type ControlStats = {
|
|
10
|
+
gauge: number;
|
|
11
|
+
label: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const statsKey = new DataProviderKey<ControlStats>("stats");
|
|
15
|
+
|
|
16
|
+
// OK: gauge is number, matches DataProviderKey<number>
|
|
17
|
+
class ValidUsage {
|
|
18
|
+
@subscribe(statsKey.gauge)
|
|
19
|
+
gauge = 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Should error: gauge should be number, not string
|
|
23
|
+
class InvalidUsage {
|
|
24
|
+
@subscribe(statsKey.gauge)
|
|
25
|
+
gauge: string = "";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Should error: label should be string, not number
|
|
29
|
+
class InvalidUsage2 {
|
|
30
|
+
@subscribe(statsKey.label)
|
|
31
|
+
label: number = 0;
|
|
32
|
+
}
|
package/src/core/utils/api.ts
CHANGED
|
@@ -36,6 +36,30 @@ export type APIResponse = {
|
|
|
36
36
|
http: Response;
|
|
37
37
|
processed: ResultTypeInterface;
|
|
38
38
|
};
|
|
39
|
+
|
|
40
|
+
/** Valeur assignée par `@get` : requête native, réponse `fetch` si HTTP, résultat métier typé `T`. */
|
|
41
|
+
export type ApiGetResult<T> = {
|
|
42
|
+
request: Request;
|
|
43
|
+
/** Absent / `undefined` pour les chemins `dataProvider(...)` (pas d’appel réseau). */
|
|
44
|
+
response?: Response;
|
|
45
|
+
/** Corps utile sans `_sonic_http_response_` sur l’objet racine. */
|
|
46
|
+
result: T;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** Extrait le corps typé depuis le résultat traité par `handleResult`. */
|
|
50
|
+
export function extractTypedApiResult<T>(
|
|
51
|
+
processed: ResultTypeInterface | null | undefined,
|
|
52
|
+
): T {
|
|
53
|
+
if (processed == null || typeof processed !== "object") {
|
|
54
|
+
return processed as T;
|
|
55
|
+
}
|
|
56
|
+
if (Array.isArray(processed)) {
|
|
57
|
+
return processed as T;
|
|
58
|
+
}
|
|
59
|
+
const copy = { ...(processed as Record<string, unknown>) };
|
|
60
|
+
delete copy._sonic_http_response_;
|
|
61
|
+
return copy as T;
|
|
62
|
+
}
|
|
39
63
|
class API {
|
|
40
64
|
/**
|
|
41
65
|
* Ce tableau static permet de ne pas appeler plusieurs fois le même service lors d'appel concurrents en GET.
|
|
@@ -149,7 +173,7 @@ class API {
|
|
|
149
173
|
}
|
|
150
174
|
async handleResult(
|
|
151
175
|
fetchResult: Response,
|
|
152
|
-
lastCall: APICall
|
|
176
|
+
lastCall: APICall,
|
|
153
177
|
): Promise<ResultTypeInterface> {
|
|
154
178
|
API.firstCallDoneFlags.set(this.serviceURL, "done");
|
|
155
179
|
this.lastResult = fetchResult;
|
|
@@ -177,14 +201,14 @@ class API {
|
|
|
177
201
|
if (lastCall.apiMethod === "get") {
|
|
178
202
|
result = await this[lastCall.apiMethod](
|
|
179
203
|
lastCall.path,
|
|
180
|
-
lastCall.additionalHeaders
|
|
204
|
+
lastCall.additionalHeaders,
|
|
181
205
|
);
|
|
182
206
|
} else {
|
|
183
207
|
result = await this[lastCall.apiMethod](
|
|
184
208
|
lastCall.path,
|
|
185
209
|
lastCall.data,
|
|
186
210
|
lastCall.method,
|
|
187
|
-
lastCall.additionalHeaders
|
|
211
|
+
lastCall.additionalHeaders,
|
|
188
212
|
);
|
|
189
213
|
}
|
|
190
214
|
}
|
|
@@ -192,7 +216,7 @@ class API {
|
|
|
192
216
|
/**
|
|
193
217
|
* Publication en global de la réponse de l'api
|
|
194
218
|
*/
|
|
195
|
-
const apiPublisher = dp<{lastResponse: APIResponse}>("sonic-api");
|
|
219
|
+
const apiPublisher = dp<{ lastResponse: APIResponse }>("sonic-api");
|
|
196
220
|
apiPublisher.lastResponse.set({
|
|
197
221
|
http: fetchResult,
|
|
198
222
|
processed: result,
|
|
@@ -218,7 +242,7 @@ class API {
|
|
|
218
242
|
Authorization:
|
|
219
243
|
"Basic " +
|
|
220
244
|
window.btoa(
|
|
221
|
-
unescape(encodeURIComponent(this.userName + ":" + this.password))
|
|
245
|
+
unescape(encodeURIComponent(this.userName + ":" + this.password)),
|
|
222
246
|
),
|
|
223
247
|
};
|
|
224
248
|
} else if (this.authToken) {
|
|
@@ -237,7 +261,7 @@ class API {
|
|
|
237
261
|
headers: headers,
|
|
238
262
|
credentials: this.credentials,
|
|
239
263
|
keepalive: this.keepAlive,
|
|
240
|
-
}
|
|
264
|
+
},
|
|
241
265
|
);
|
|
242
266
|
|
|
243
267
|
try {
|
|
@@ -344,7 +368,7 @@ class API {
|
|
|
344
368
|
const loop = () => {
|
|
345
369
|
if (
|
|
346
370
|
![undefined, "loading"].includes(
|
|
347
|
-
API.firstCallDoneFlags.get(this.serviceURL)
|
|
371
|
+
API.firstCallDoneFlags.get(this.serviceURL),
|
|
348
372
|
)
|
|
349
373
|
) {
|
|
350
374
|
resolve(true);
|
|
@@ -400,6 +424,50 @@ class API {
|
|
|
400
424
|
API.loadingGetPromises.delete(mapKey);
|
|
401
425
|
return result as T & ResultTypeInterface;
|
|
402
426
|
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* S’appuie sur `get()` puis reformate la réponse : `Request` équivalent à l’appel,
|
|
430
|
+
* `Response` renseignée par `lastResult` après un GET HTTP (via `handleResult`),
|
|
431
|
+
* `result` = corps métier typé `T` (sans `_sonic_http_response_` à la racine).
|
|
432
|
+
* Pour `dataProvider(...)`, pas de `response` (pas de fetch).
|
|
433
|
+
*/
|
|
434
|
+
async getDetailed<T>(
|
|
435
|
+
path: string,
|
|
436
|
+
additionalHeaders?: HeadersInit,
|
|
437
|
+
): Promise<ApiGetResult<T> | undefined> {
|
|
438
|
+
const isDataProvider = /dataProvider\((.*?)\)(.*?)$/.test(path);
|
|
439
|
+
|
|
440
|
+
const processed = await this.get<T>(path, additionalHeaders);
|
|
441
|
+
if (processed == null) {
|
|
442
|
+
return undefined;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const result = extractTypedApiResult<T>(processed as ResultTypeInterface);
|
|
446
|
+
const url = this.computeURL(path);
|
|
447
|
+
|
|
448
|
+
if (isDataProvider) {
|
|
449
|
+
return {
|
|
450
|
+
request: new Request(url, { method: "GET" }),
|
|
451
|
+
result,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const headers = await this.createHeaders(additionalHeaders);
|
|
456
|
+
const request = new Request(url, {
|
|
457
|
+
method: "GET",
|
|
458
|
+
headers: new Headers(headers as HeadersInit),
|
|
459
|
+
credentials: this.credentials,
|
|
460
|
+
cache: this.cache,
|
|
461
|
+
keepalive: this.keepAlive,
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
return {
|
|
465
|
+
request,
|
|
466
|
+
response: this.lastResult,
|
|
467
|
+
result,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
403
471
|
/**
|
|
404
472
|
* Création du header, avec authentification si besoin
|
|
405
473
|
* ajout du language via le header accept-language qui contient le langue du navigateur
|
|
@@ -440,7 +508,7 @@ class API {
|
|
|
440
508
|
path: string,
|
|
441
509
|
data: SendType,
|
|
442
510
|
method = "POST",
|
|
443
|
-
additionalHeaders?: HeadersInit
|
|
511
|
+
additionalHeaders?: HeadersInit,
|
|
444
512
|
) {
|
|
445
513
|
const lastCall: APICall = {
|
|
446
514
|
apiMethod: "send",
|
|
@@ -471,7 +539,7 @@ class API {
|
|
|
471
539
|
path: string,
|
|
472
540
|
data: SendType,
|
|
473
541
|
method = "POST",
|
|
474
|
-
additionalHeaders?: HeadersInit
|
|
542
|
+
additionalHeaders?: HeadersInit,
|
|
475
543
|
) {
|
|
476
544
|
const lastCall: APICall = {
|
|
477
545
|
apiMethod: "submitFormData",
|
|
@@ -502,7 +570,7 @@ class API {
|
|
|
502
570
|
async put<T, SendType = CoreJSType>(
|
|
503
571
|
path: string,
|
|
504
572
|
data: SendType,
|
|
505
|
-
additionalHeaders?: HeadersInit
|
|
573
|
+
additionalHeaders?: HeadersInit,
|
|
506
574
|
) {
|
|
507
575
|
return this.send<T, SendType>(path, data, "PUT", additionalHeaders);
|
|
508
576
|
}
|
|
@@ -513,7 +581,7 @@ class API {
|
|
|
513
581
|
async post<T, SendType = CoreJSType>(
|
|
514
582
|
path: string,
|
|
515
583
|
data: SendType,
|
|
516
|
-
additionalHeaders?: HeadersInit
|
|
584
|
+
additionalHeaders?: HeadersInit,
|
|
517
585
|
) {
|
|
518
586
|
return this.send<T, SendType>(path, data, "POST", additionalHeaders);
|
|
519
587
|
}
|
|
@@ -523,7 +591,7 @@ class API {
|
|
|
523
591
|
async patch<T, SendType = CoreJSType>(
|
|
524
592
|
path: string,
|
|
525
593
|
data: SendType,
|
|
526
|
-
additionalHeaders?: HeadersInit
|
|
594
|
+
additionalHeaders?: HeadersInit,
|
|
527
595
|
) {
|
|
528
596
|
return this.send<T, SendType>(path, data, "PATCH", additionalHeaders);
|
|
529
597
|
}
|
|
@@ -534,7 +602,7 @@ class API {
|
|
|
534
602
|
async delete<T, SendType = CoreJSType>(
|
|
535
603
|
path: string,
|
|
536
604
|
data: SendType,
|
|
537
|
-
additionalHeaders?: HeadersInit
|
|
605
|
+
additionalHeaders?: HeadersInit,
|
|
538
606
|
) {
|
|
539
607
|
return this.send<T, SendType>(path, data, "delete", additionalHeaders);
|
|
540
608
|
}
|
|
@@ -548,7 +616,7 @@ export default API;
|
|
|
548
616
|
let logIsConf = false;
|
|
549
617
|
let logEndPoint = "log";
|
|
550
618
|
let logConfiguration: APIConfiguration = HTML.getApiConfiguration(
|
|
551
|
-
document.body || document.documentElement
|
|
619
|
+
document.body || document.documentElement,
|
|
552
620
|
);
|
|
553
621
|
|
|
554
622
|
let logsToSend: any = [];
|
|
@@ -623,6 +691,6 @@ export const configLog = (newConfig: APIConfiguration, endPoint = "log") => {
|
|
|
623
691
|
timestamp: new Date().toISOString(),
|
|
624
692
|
};
|
|
625
693
|
log(event.reason?.message || "Unknown rejection reason.", errorDetails);
|
|
626
|
-
}
|
|
694
|
+
},
|
|
627
695
|
);
|
|
628
696
|
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { DataProviderKey } from "./dataProviderKey";
|
|
2
|
+
|
|
3
|
+
type LegacyCheckedTicket = { id: string };
|
|
4
|
+
type ControlRateType = "A" | "B";
|
|
5
|
+
type ControlStats = {
|
|
6
|
+
checkedTickets: LegacyCheckedTicket[];
|
|
7
|
+
gauge: number;
|
|
8
|
+
insidePeople: number;
|
|
9
|
+
soldTickets: number;
|
|
10
|
+
controlRateType: ControlRateType;
|
|
11
|
+
totalCheckedTickets: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
describe("DataProviderKey", () => {
|
|
15
|
+
it("construit le chemin cumulatif via les accès", () => {
|
|
16
|
+
const myKey = new DataProviderKey<ControlStats>(
|
|
17
|
+
"idDonneesDeStats",
|
|
18
|
+
).checkedTickets[0];
|
|
19
|
+
expect(myKey.toString()).toBe("idDonneesDeStats.checkedTickets.0");
|
|
20
|
+
expect(myKey.path).toBe("idDonneesDeStats.checkedTickets.0");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("permet la navigation type-safe sur les propriétés", () => {
|
|
24
|
+
const key = new DataProviderKey<ControlStats>("stats");
|
|
25
|
+
const gaugeKey = key.gauge;
|
|
26
|
+
expect(gaugeKey.path).toBe("stats.gauge");
|
|
27
|
+
expect(gaugeKey.toString()).toBe("stats.gauge");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("fonctionne avec new", () => {
|
|
31
|
+
const key = new DataProviderKey<ControlStats>("root");
|
|
32
|
+
expect(key.path).toBe("root");
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type-safe navigation through composite data structures (publisher paths).
|
|
3
|
+
* Each property or index access extends the path. Retrieve the final path via toString() or path.
|
|
4
|
+
* Supports dynamic paths: use placeholders like "users.${userIndex}" in the constructor.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* const myKey = new DataProviderKey<Data>("data").items[0];
|
|
8
|
+
* myKey.toString(); // "data.items.0"
|
|
9
|
+
* myKey.path; // same
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* // U = dépendances dynamiques sur l’hôte (voir DataProviderKeyHost) — propagé sur .foo.bar
|
|
13
|
+
* new DataProviderKey<User, { userIndex: number }>("demoUsers.${userIndex}");
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
type IsAny<T> = 0 extends 1 & T ? true : false;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Prototype de classe décorée : propriétés minimales attendues sur l’hôte quand la clé est
|
|
20
|
+
* `DataProviderKey<…, U>` (U renseigné à la construction). Avec `U` par défaut (`any`), pas de contrainte.
|
|
21
|
+
*/
|
|
22
|
+
export type DataProviderKeyHost<U> = IsAny<U> extends true
|
|
23
|
+
? object
|
|
24
|
+
: keyof U extends never
|
|
25
|
+
? object
|
|
26
|
+
: object & U;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* U : forme minimale du composant pour résoudre les placeholders `${…}` du path ; inchangée lors de la navigation.
|
|
30
|
+
*/
|
|
31
|
+
type DataProviderKeyProxy<T, U = any> = T extends object
|
|
32
|
+
? {
|
|
33
|
+
[K in keyof T as T[K] extends (...args: unknown[]) => unknown
|
|
34
|
+
? never
|
|
35
|
+
: K]: DataProviderKey<T[K], U>;
|
|
36
|
+
}
|
|
37
|
+
: object;
|
|
38
|
+
|
|
39
|
+
export type DataProviderKey<T, U = any> = DataProviderKeyImpl<T, U> &
|
|
40
|
+
DataProviderKeyProxy<T, U>;
|
|
41
|
+
|
|
42
|
+
class DataProviderKeyImpl<T, U = any> {
|
|
43
|
+
declare readonly _phantom?: T;
|
|
44
|
+
declare readonly _phantomDeps?: U;
|
|
45
|
+
|
|
46
|
+
constructor(public readonly path: string) {}
|
|
47
|
+
|
|
48
|
+
toString(): string {
|
|
49
|
+
return this.path;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function createDataProviderKeyProxy<T, U = any>(
|
|
54
|
+
key: DataProviderKeyImpl<T, U>,
|
|
55
|
+
): DataProviderKey<T, U> {
|
|
56
|
+
return new Proxy(key, {
|
|
57
|
+
get(target, prop: string | symbol) {
|
|
58
|
+
if (prop === "path") return target.path;
|
|
59
|
+
if (prop === "toString") return target.toString.bind(target);
|
|
60
|
+
if (prop === Symbol.toStringTag) return "DataProviderKey";
|
|
61
|
+
if (typeof prop === "symbol")
|
|
62
|
+
return (target as unknown as Record<symbol, unknown>)[prop];
|
|
63
|
+
const newPath = target.path
|
|
64
|
+
? `${target.path}.${String(prop)}`
|
|
65
|
+
: String(prop);
|
|
66
|
+
return createDataProviderKeyProxy(
|
|
67
|
+
new DataProviderKeyImpl<unknown, U>(newPath),
|
|
68
|
+
);
|
|
69
|
+
},
|
|
70
|
+
}) as DataProviderKey<T, U>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface DataProviderKeyConstructor {
|
|
74
|
+
new <T, U = any>(path: string): DataProviderKey<T, U>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
78
|
+
export const DataProviderKey: DataProviderKeyConstructor = function (
|
|
79
|
+
this: any,
|
|
80
|
+
path: string,
|
|
81
|
+
): DataProviderKey<unknown, any> {
|
|
82
|
+
if (!(this instanceof DataProviderKey)) {
|
|
83
|
+
return new (DataProviderKey as DataProviderKeyConstructor)(path);
|
|
84
|
+
}
|
|
85
|
+
return createDataProviderKeyProxy(new DataProviderKeyImpl(path));
|
|
86
|
+
} as any;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { Endpoint } from "./endpoint";
|
|
3
|
+
|
|
4
|
+
describe("Endpoint", () => {
|
|
5
|
+
it("expose le path normalisé (trim)", () => {
|
|
6
|
+
const e = new Endpoint<{ x: number }>(" communes?limit=1 ");
|
|
7
|
+
expect(e.path).toBe("communes?limit=1");
|
|
8
|
+
expect(e.toString()).toBe("communes?limit=1");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("refuse le path vide", () => {
|
|
12
|
+
expect(() => new Endpoint("")).toThrow(RangeError);
|
|
13
|
+
expect(() => new Endpoint(" ")).toThrow(RangeError);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("normalizePath et isNonEmpty", () => {
|
|
17
|
+
expect(Endpoint.normalizePath("a")).toBe("a");
|
|
18
|
+
expect(Endpoint.isNonEmpty(" x ")).toBe(true);
|
|
19
|
+
expect(Endpoint.isNonEmpty("")).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("normalizePath enlève le slash initial et les doubles slash (relatif)", () => {
|
|
23
|
+
expect(Endpoint.normalizePath("/users/1")).toBe("users/1");
|
|
24
|
+
expect(Endpoint.normalizePath("//users//1")).toBe("users/1");
|
|
25
|
+
expect(Endpoint.normalizePath("v1//users//x?limit=1")).toBe(
|
|
26
|
+
"v1/users/x?limit=1",
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("normalizePath pour URL absolue : pathname sans doubles slash", () => {
|
|
31
|
+
expect(
|
|
32
|
+
Endpoint.normalizePath("https://example.com/api//v1//x"),
|
|
33
|
+
).toBe("https://example.com/api/v1/x");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("looksLikeDataProviderPath", () => {
|
|
37
|
+
expect(Endpoint.looksLikeDataProviderPath("dataProvider(foo)")).toBe(true);
|
|
38
|
+
expect(Endpoint.looksLikeDataProviderPath(" DataProvider(x)")).toBe(true);
|
|
39
|
+
expect(Endpoint.looksLikeDataProviderPath("users/1")).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
});
|