@supersoniks/concorde 4.9.91 → 4.9.93
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/ai/cursor/rules/concorde.mdc +7 -0
- package/ai/jetbrains/rules/concorde.md +6 -0
- package/ai/skills/concorde/SKILL.md +48 -0
- package/build-infos.json +1 -1
- package/concorde-core.bundle.js +257 -257
- package/concorde-core.es.js +3174 -3086
- package/dist/concorde-core.bundle.js +257 -257
- package/dist/concorde-core.es.js +3174 -3086
- package/dist/js/core/decorators/api.js +168 -63
- package/dist/js/core/decorators/api.js.map +1 -1
- package/dist/js/core/utils/api.js +29 -8
- package/dist/js/core/utils/api.js.map +1 -1
- package/dist/types/core/decorators/api.d.ts +30 -16
- package/dist/types/core/decorators/api.d.ts.map +1 -1
- package/dist/types/core/utils/api.d.ts +1 -1
- package/dist/types/core/utils/api.d.ts.map +1 -1
- package/docs/assets/{index-BL6WVF7g.css → index-D2Q3JyQ6.css} +1 -1
- package/docs/assets/{index-DVn8AeyA.js → index-DURr6O71.js} +466 -568
- package/docs/crawl/docs/_decorators/get.html +35 -10
- package/docs/crawl/docs/_decorators/patch.html +13 -5
- package/docs/crawl/docs/_decorators/post.html +39 -10
- package/docs/crawl/docs/_decorators/put.html +13 -5
- package/docs/crawl/hubs/decorators.html +43 -40
- package/docs/crawl/hubs/remote-data.html +43 -40
- package/docs/index.html +2 -2
- package/docs/llms-full.txt +98 -23
- package/docs/llms.txt +1 -1
- package/docs/src/docs/_decorators/get.md +39 -5
- package/docs/src/docs/_decorators/patch.md +8 -3
- package/docs/src/docs/_decorators/post.md +39 -6
- package/docs/src/docs/_decorators/put.md +8 -3
- package/docs/src/docs/search/docs-search.json +103 -183
- package/docs/src/tsconfig.json +268 -268
- package/package.json +1 -1
- package/src/core/decorators/api.post.spec.ts +101 -6
- package/src/core/decorators/api.spec.ts +137 -6
- package/src/core/decorators/api.ts +304 -148
- package/src/core/utils/api.spec.ts +73 -0
- package/src/core/utils/api.ts +44 -11
- package/src/docs/_decorators/get.md +43 -5
- package/src/docs/_decorators/patch.md +16 -2
- package/src/docs/_decorators/post.md +23 -7
- package/src/docs/_decorators/put.md +16 -2
- package/src/docs/components/docs-demo-sources.ts +22 -8
- package/src/docs/example/decorators-demo-post.ts +70 -0
- package/src/docs/example/decorators-demo-subscribe-publish-get-demos.ts +61 -2
- package/src/docs/search/docs-search.json +122 -7
- package/src/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import API, { type APIConfiguration } from "./api";
|
|
3
|
+
|
|
4
|
+
const baseConfig: APIConfiguration = {
|
|
5
|
+
serviceURL: "https://api.example.test",
|
|
6
|
+
token: null,
|
|
7
|
+
userName: null,
|
|
8
|
+
password: null,
|
|
9
|
+
authToken: null,
|
|
10
|
+
tokenProvider: null,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
vi.restoreAllMocks();
|
|
15
|
+
vi.unstubAllGlobals();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("API GET synchronization", () => {
|
|
19
|
+
it("deduplicates fetch while sharing the response metadata", async () => {
|
|
20
|
+
const response = new Response(JSON.stringify({ items: [1, 2] }), {
|
|
21
|
+
status: 200,
|
|
22
|
+
headers: { "Content-Type": "application/json" },
|
|
23
|
+
});
|
|
24
|
+
const fetchMock = vi.fn().mockResolvedValue(response);
|
|
25
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
26
|
+
|
|
27
|
+
const firstApi = new API({
|
|
28
|
+
...baseConfig,
|
|
29
|
+
serviceURL: "https://api.example.test/concurrent",
|
|
30
|
+
});
|
|
31
|
+
const secondApi = new API({
|
|
32
|
+
...baseConfig,
|
|
33
|
+
serviceURL: "https://api.example.test/concurrent",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const [firstPayload, secondPayload] = await Promise.all([
|
|
37
|
+
firstApi.getDetailed<{ items: number[] }>("items"),
|
|
38
|
+
secondApi.getDetailed<{ items: number[] }>("items"),
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
42
|
+
expect(firstPayload?.response?.status).toBe(200);
|
|
43
|
+
expect(secondPayload?.response?.status).toBe(200);
|
|
44
|
+
expect(firstPayload?.result).toEqual({ items: [1, 2] });
|
|
45
|
+
expect(secondPayload?.result).toEqual({ items: [1, 2] });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("permet un nouvel appel après une erreur réseau", async () => {
|
|
49
|
+
const response = new Response(JSON.stringify({ ok: true }), {
|
|
50
|
+
status: 200,
|
|
51
|
+
headers: { "Content-Type": "application/json" },
|
|
52
|
+
});
|
|
53
|
+
const fetchMock = vi
|
|
54
|
+
.fn()
|
|
55
|
+
.mockRejectedValueOnce(new Error("offline"))
|
|
56
|
+
.mockResolvedValueOnce(response);
|
|
57
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
58
|
+
|
|
59
|
+
const api = new API({
|
|
60
|
+
...baseConfig,
|
|
61
|
+
serviceURL: "https://api.example.test/recover",
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await expect(api.getDetailed<{ ok: boolean }>("items")).resolves.toBe(
|
|
65
|
+
undefined,
|
|
66
|
+
);
|
|
67
|
+
const payload = await api.getDetailed<{ ok: boolean }>("items");
|
|
68
|
+
|
|
69
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
70
|
+
expect(payload?.response?.status).toBe(200);
|
|
71
|
+
expect(payload?.result).toEqual({ ok: true });
|
|
72
|
+
});
|
|
73
|
+
});
|
package/src/core/utils/api.ts
CHANGED
|
@@ -49,6 +49,11 @@ export type ApiResult<T> = {
|
|
|
49
49
|
/** @deprecated Utiliser `ApiResult`. */
|
|
50
50
|
export type ApiGetResult<T> = ApiResult<T>;
|
|
51
51
|
|
|
52
|
+
type SharedGetResult = {
|
|
53
|
+
processed: ResultTypeInterface;
|
|
54
|
+
response?: Response;
|
|
55
|
+
};
|
|
56
|
+
|
|
52
57
|
/** Extrait le corps typé depuis le résultat traité par `handleResult`. */
|
|
53
58
|
function extractTypedApiResult<T>(
|
|
54
59
|
processed: ResultTypeInterface | null | undefined,
|
|
@@ -67,7 +72,10 @@ class API {
|
|
|
67
72
|
/**
|
|
68
73
|
* Ce tableau static permet de ne pas appeler plusieurs fois le même service lors d'appel concurrents en GET.
|
|
69
74
|
*/
|
|
70
|
-
private static loadingGetPromises: Map<
|
|
75
|
+
private static loadingGetPromises: Map<
|
|
76
|
+
string,
|
|
77
|
+
Promise<SharedGetResult | null>
|
|
78
|
+
> = new Map();
|
|
71
79
|
/**
|
|
72
80
|
* L'url de base du service sans endpoint
|
|
73
81
|
*/
|
|
@@ -202,10 +210,14 @@ class API {
|
|
|
202
210
|
if (httpCode === 498 && !API.failledTokenUpdates.has(this.serviceURL)) {
|
|
203
211
|
this.handleInvalidToken(this.token);
|
|
204
212
|
if (lastCall.apiMethod === "get") {
|
|
205
|
-
|
|
213
|
+
const retryResult = await this.get(
|
|
206
214
|
lastCall.path,
|
|
207
215
|
lastCall.additionalHeaders,
|
|
208
216
|
);
|
|
217
|
+
if (retryResult == null) {
|
|
218
|
+
throw new Error("GET retry failed after token refresh");
|
|
219
|
+
}
|
|
220
|
+
result = retryResult;
|
|
209
221
|
} else {
|
|
210
222
|
result = await this[lastCall.apiMethod](
|
|
211
223
|
lastCall.path,
|
|
@@ -384,13 +396,16 @@ class API {
|
|
|
384
396
|
});
|
|
385
397
|
}
|
|
386
398
|
|
|
387
|
-
async get<T>(
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
}
|
|
399
|
+
async get<T>(
|
|
400
|
+
path: string,
|
|
401
|
+
additionalHeaders?: HeadersInit,
|
|
402
|
+
): Promise<(T & ResultTypeInterface) | null> {
|
|
392
403
|
const regExp = /dataProvider\((.*?)\)(.*?)$/;
|
|
393
404
|
if (regExp.test(path)) {
|
|
405
|
+
if (this.blockUntilDone) {
|
|
406
|
+
await this.firstCallDone();
|
|
407
|
+
API.firstCallDoneFlags.set(this.serviceURL, "loading");
|
|
408
|
+
}
|
|
394
409
|
const match = path.match(regExp);
|
|
395
410
|
if (!match) throw new Error("dataProvider path is not valid");
|
|
396
411
|
return await this.localGet<T>(match[1], match[2]);
|
|
@@ -405,9 +420,21 @@ class API {
|
|
|
405
420
|
const mapKey = JSON.stringify({
|
|
406
421
|
url: url,
|
|
407
422
|
headers: headers,
|
|
423
|
+
credentials: this.credentials,
|
|
424
|
+
cache: this.cache,
|
|
425
|
+
keepAlive: this.keepAlive,
|
|
408
426
|
});
|
|
427
|
+
|
|
428
|
+
// Les GET non bloquants doivent pouvoir rejoindre une requête déjà en vol.
|
|
429
|
+
// Sinon firstCallDone() sérialise les appels avant que la map de déduplication
|
|
430
|
+
// ne soit consultée, ce qui déclenche deux fetch pour deux consommateurs.
|
|
431
|
+
if (this.blockUntilDone) {
|
|
432
|
+
await this.firstCallDone();
|
|
433
|
+
API.firstCallDoneFlags.set(this.serviceURL, "loading");
|
|
434
|
+
}
|
|
435
|
+
|
|
409
436
|
if (!API.loadingGetPromises.has(mapKey)) {
|
|
410
|
-
const promise = new Promise(async (resolve) => {
|
|
437
|
+
const promise = new Promise<SharedGetResult | null>(async (resolve) => {
|
|
411
438
|
try {
|
|
412
439
|
const result = await fetch(url, {
|
|
413
440
|
headers: headers,
|
|
@@ -416,16 +443,22 @@ class API {
|
|
|
416
443
|
keepalive: this.keepAlive,
|
|
417
444
|
});
|
|
418
445
|
const handledResult = await this.handleResult(result, lastCall);
|
|
419
|
-
resolve(
|
|
446
|
+
resolve({
|
|
447
|
+
processed: handledResult,
|
|
448
|
+
response: this.lastResult,
|
|
449
|
+
});
|
|
420
450
|
} catch (e) {
|
|
451
|
+
API.firstCallDoneFlags.set(this.serviceURL, "error");
|
|
421
452
|
resolve(null);
|
|
422
453
|
}
|
|
423
454
|
});
|
|
424
455
|
API.loadingGetPromises.set(mapKey, promise);
|
|
425
456
|
}
|
|
426
|
-
const
|
|
457
|
+
const sharedResult = await API.loadingGetPromises.get(mapKey);
|
|
427
458
|
API.loadingGetPromises.delete(mapKey);
|
|
428
|
-
|
|
459
|
+
if (!sharedResult) return null;
|
|
460
|
+
this.lastResult = sharedResult.response;
|
|
461
|
+
return sharedResult.processed as T & ResultTypeInterface;
|
|
429
462
|
}
|
|
430
463
|
|
|
431
464
|
/**
|
|
@@ -17,6 +17,19 @@ const usersEndpoint = new Endpoint<User[]>("users");
|
|
|
17
17
|
payload?: ApiResult<User[]>;
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
+
The same decorator adapts to a method. The method receives the complete
|
|
21
|
+
`ApiResult<T>` payload after each GET:
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
@get(usersEndpoint)
|
|
25
|
+
handleUsers(payload?: ApiResult<User[]>) {
|
|
26
|
+
if (!payload) return;
|
|
27
|
+
|
|
28
|
+
console.log(payload.response?.status);
|
|
29
|
+
console.log(payload.result);
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
20
33
|
The first argument is always an
|
|
21
34
|
[`Endpoint<T>`](#docs/_misc/endpoint.md/endpoint). API configuration is read
|
|
22
35
|
from the HTML scope (`serviceURL`, token, etc.), so the component is usually
|
|
@@ -37,15 +50,36 @@ const usersGet = get(new Endpoint<User[]>("users"));
|
|
|
37
50
|
@state()
|
|
38
51
|
payload?: ApiResult<User[]>;
|
|
39
52
|
|
|
40
|
-
refresh() {
|
|
41
|
-
usersGet.refetch(this);
|
|
53
|
+
async refresh() {
|
|
54
|
+
const payload = await usersGet.refetch(this);
|
|
55
|
+
console.log(payload?.result);
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`refetch(component)` returns a `Promise<ApiResult<T> | undefined>` that resolves
|
|
60
|
+
after the GET has completed.
|
|
61
|
+
|
|
62
|
+
The named handle can also decorate a method and be reused for refetching:
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
const usersGet = get(new Endpoint<User[]>("users"));
|
|
66
|
+
|
|
67
|
+
@usersGet
|
|
68
|
+
handleUsers(payload?: ApiResult<User[]>) {
|
|
69
|
+
// ...
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@refetch(usersGet)
|
|
73
|
+
reloadUsers() {
|
|
74
|
+
// The GET runs and handleUsers receives its payload.
|
|
42
75
|
}
|
|
43
76
|
```
|
|
44
77
|
|
|
45
78
|
### From a method decorator
|
|
46
79
|
|
|
47
80
|
The refetch can decorate a local method. The default is `after`; if the
|
|
48
|
-
method returns a Promise, the GET waits for its resolution.
|
|
81
|
+
method returns a Promise, the GET waits for its resolution. The decorated method
|
|
82
|
+
call also waits for the refetch to complete.
|
|
49
83
|
|
|
50
84
|
```typescript
|
|
51
85
|
import { refetch } from "@supersoniks/concorde/decorators";
|
|
@@ -56,12 +90,12 @@ async reload() {
|
|
|
56
90
|
}
|
|
57
91
|
```
|
|
58
92
|
|
|
59
|
-
Use `{ when: "before" }` when the GET must
|
|
93
|
+
Use `{ when: "before" }` when the GET must complete before the method runs:
|
|
60
94
|
|
|
61
95
|
```typescript
|
|
62
96
|
@refetch(usersGet, { when: "before" })
|
|
63
97
|
reloadBefore() {
|
|
64
|
-
// The refetch
|
|
98
|
+
// The refetch has completed before this method runs.
|
|
65
99
|
}
|
|
66
100
|
```
|
|
67
101
|
|
|
@@ -76,6 +110,7 @@ configuration key is provided.
|
|
|
76
110
|
- `skipEmptyPlaceholder`: blocks the GET when `${prop}` is `""`.
|
|
77
111
|
- `refetchEveryMs`: automatically runs the GET again after this interval
|
|
78
112
|
(milliseconds). Omit the option, or use `0`, to disable polling.
|
|
113
|
+
- `triggerKey`: a publisher whose `invalidate()` runs the GET again.
|
|
79
114
|
|
|
80
115
|
With a configuration key, use:
|
|
81
116
|
|
|
@@ -88,6 +123,7 @@ With a configuration key, use:
|
|
|
88
123
|
- when the component connects and configuration is available;
|
|
89
124
|
- when a property used in `${...}` changes;
|
|
90
125
|
- when a configuration publisher changes;
|
|
126
|
+
- when the `triggerKey` publisher is invalidated;
|
|
91
127
|
- after each response when `refetchEveryMs` is set.
|
|
92
128
|
|
|
93
129
|
See [Dynamic path placeholders](#docs/_misc/dynamic-path.md/dynamic-path) for
|
|
@@ -109,6 +145,8 @@ import { Endpoint } from "@supersoniks/concorde/utils/endpoint";
|
|
|
109
145
|
<template>
|
|
110
146
|
<docs-demo-sources for="demo-api-get"></docs-demo-sources>
|
|
111
147
|
<demo-api-get></demo-api-get>
|
|
148
|
+
<docs-demo-sources for="demo-api-get-method"></docs-demo-sources>
|
|
149
|
+
<demo-api-get-method></demo-api-get-method>
|
|
112
150
|
<docs-demo-sources for="demo-api-get-configuration-key"></docs-demo-sources>
|
|
113
151
|
<demo-api-get-configuration-key></demo-api-get-configuration-key>
|
|
114
152
|
</template>
|
|
@@ -16,14 +16,27 @@ const patchUser = patch(new Endpoint<User>("users/42"), bodyKey);
|
|
|
16
16
|
payload?: ApiResult<User>;
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
The same decorator adapts to a method:
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
@patch(new Endpoint<User>("users/42"), bodyKey)
|
|
23
|
+
handlePatch(payload?: ApiResult<User>) {
|
|
24
|
+
if (payload?.response?.ok) console.log(payload.result);
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
19
28
|
To run the operation again from the component:
|
|
20
29
|
|
|
21
30
|
```typescript
|
|
22
|
-
savePatch() {
|
|
23
|
-
patchUser.send(this);
|
|
31
|
+
async savePatch() {
|
|
32
|
+
await patchUser.send(this);
|
|
24
33
|
}
|
|
25
34
|
```
|
|
26
35
|
|
|
36
|
+
`send(component)` returns a Promise resolved after the PATCH completes. See
|
|
37
|
+
[@post](#docs/_decorators/post.md/post) for the method-decorator form and the
|
|
38
|
+
`when: "before"` option.
|
|
39
|
+
|
|
27
40
|
The PATCH is also sent automatically when the body mutates, unless
|
|
28
41
|
`autoPostOnBodyMutation: false` is set. The available options are described in
|
|
29
42
|
[@post](#docs/_decorators/post.md/post):
|
|
@@ -32,6 +45,7 @@ The PATCH is also sent automatically when the body mutates, unless
|
|
|
32
45
|
- `skipIfBodyMissing`
|
|
33
46
|
- `refetchEveryMs`
|
|
34
47
|
- `skipEmptyPlaceholder`
|
|
48
|
+
- `triggerKey`
|
|
35
49
|
|
|
36
50
|
Pass explicit API configuration as the third argument:
|
|
37
51
|
|
|
@@ -17,6 +17,15 @@ const createUser = post(new Endpoint<User>("users"), userBodyKey);
|
|
|
17
17
|
payload?: ApiResult<User>;
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
+
The same decorator adapts to a method and receives the complete payload:
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
@post(new Endpoint<User>("users"), userBodyKey)
|
|
24
|
+
handleCreate(payload?: ApiResult<User>) {
|
|
25
|
+
if (payload?.response?.ok) console.log(payload.result);
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
20
29
|
The current body is read from `userBodyKey`. By default, the POST is sent when
|
|
21
30
|
the component connects and after each body mutation. Mutations in the same
|
|
22
31
|
frame are coalesced into one request.
|
|
@@ -32,18 +41,21 @@ Keep the decorator to access `send(component)`. The body is read again when the
|
|
|
32
41
|
operation runs.
|
|
33
42
|
|
|
34
43
|
```typescript
|
|
35
|
-
saveAgain() {
|
|
36
|
-
createUser.send(this);
|
|
44
|
+
async saveAgain() {
|
|
45
|
+
const payload = await createUser.send(this);
|
|
46
|
+
console.log(payload?.result);
|
|
37
47
|
}
|
|
38
48
|
```
|
|
39
49
|
|
|
40
|
-
`send()`
|
|
41
|
-
|
|
50
|
+
`send()` returns a `Promise<ApiResult<T> | undefined>` that resolves after the
|
|
51
|
+
operation completes. It is useful for “Save” or “Retry” buttons and can repeat
|
|
52
|
+
an operation with side effects.
|
|
42
53
|
|
|
43
54
|
### From a method decorator
|
|
44
55
|
|
|
45
56
|
The send action can decorate a local method. It runs after the method by
|
|
46
|
-
default
|
|
57
|
+
default, waits for an eventual Promise to resolve, and the decorated method
|
|
58
|
+
call waits for the send to complete:
|
|
47
59
|
|
|
48
60
|
```typescript
|
|
49
61
|
import { send } from "@supersoniks/concorde/decorators";
|
|
@@ -54,12 +66,12 @@ async save() {
|
|
|
54
66
|
}
|
|
55
67
|
```
|
|
56
68
|
|
|
57
|
-
Use `{ when: "before" }` when the
|
|
69
|
+
Use `{ when: "before" }` when the send must complete before the method runs:
|
|
58
70
|
|
|
59
71
|
```typescript
|
|
60
72
|
@send(createUser, { when: "before" })
|
|
61
73
|
saveBefore() {
|
|
62
|
-
// The POST
|
|
74
|
+
// The POST has completed before this method runs.
|
|
63
75
|
}
|
|
64
76
|
```
|
|
65
77
|
|
|
@@ -80,6 +92,7 @@ is provided.
|
|
|
80
92
|
repeating the operation is intended.
|
|
81
93
|
- `skipEmptyPlaceholder`: block the request when a `${...}` placeholder is
|
|
82
94
|
`""`.
|
|
95
|
+
- `triggerKey`: a publisher whose `invalidate()` sends the POST again.
|
|
83
96
|
|
|
84
97
|
With a configuration key:
|
|
85
98
|
|
|
@@ -96,6 +109,7 @@ dynamic path details.
|
|
|
96
109
|
- after a body mutation, unless `autoPostOnBodyMutation: false`;
|
|
97
110
|
- when a property used in `${...}` changes;
|
|
98
111
|
- when a configuration publisher changes;
|
|
112
|
+
- when the `triggerKey` publisher is invalidated;
|
|
99
113
|
- when `send()` is called;
|
|
100
114
|
- after each response when `refetchEveryMs` is set.
|
|
101
115
|
|
|
@@ -115,6 +129,8 @@ import { Endpoint } from "@supersoniks/concorde/utils/endpoint";
|
|
|
115
129
|
<template>
|
|
116
130
|
<docs-demo-sources for="demo-api-post"></docs-demo-sources>
|
|
117
131
|
<demo-api-post></demo-api-post>
|
|
132
|
+
<docs-demo-sources for="demo-api-post-method"></docs-demo-sources>
|
|
133
|
+
<demo-api-post-method></demo-api-post-method>
|
|
118
134
|
<docs-demo-sources for="demo-api-post-dynamic"></docs-demo-sources>
|
|
119
135
|
<demo-api-post-dynamic></demo-api-post-dynamic>
|
|
120
136
|
</template>
|
|
@@ -16,14 +16,27 @@ const updateUser = put(new Endpoint<User>("users/42"), bodyKey);
|
|
|
16
16
|
payload?: ApiResult<User>;
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
The same decorator adapts to a method:
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
@put(new Endpoint<User>("users/42"), bodyKey)
|
|
23
|
+
handleUpdate(payload?: ApiResult<User>) {
|
|
24
|
+
if (payload?.response?.ok) console.log(payload.result);
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
19
28
|
To run the operation again from the component:
|
|
20
29
|
|
|
21
30
|
```typescript
|
|
22
|
-
update() {
|
|
23
|
-
updateUser.send(this);
|
|
31
|
+
async update() {
|
|
32
|
+
await updateUser.send(this);
|
|
24
33
|
}
|
|
25
34
|
```
|
|
26
35
|
|
|
36
|
+
`send(component)` returns a Promise resolved after the PUT completes. See
|
|
37
|
+
[@post](#docs/_decorators/post.md/post) for the method-decorator form and the
|
|
38
|
+
`when: "before"` option.
|
|
39
|
+
|
|
27
40
|
The PUT is also sent automatically when the body mutates, unless
|
|
28
41
|
`autoPostOnBodyMutation: false` is set. See
|
|
29
42
|
[@post](#docs/_decorators/post.md/post) for the option details:
|
|
@@ -32,6 +45,7 @@ The PUT is also sent automatically when the body mutates, unless
|
|
|
32
45
|
- `skipIfBodyMissing`
|
|
33
46
|
- `refetchEveryMs`
|
|
34
47
|
- `skipEmptyPlaceholder`
|
|
48
|
+
- `triggerKey`
|
|
35
49
|
|
|
36
50
|
Pass explicit API configuration as the third argument:
|
|
37
51
|
|
|
@@ -222,7 +222,7 @@ export const DOCS_DEMO_SOURCE_REGISTRY: Record<string, DocsSource[]> = {
|
|
|
222
222
|
"demo-publish": [
|
|
223
223
|
{
|
|
224
224
|
path: "src/docs/example/decorators-demo-subscribe-publish-get-demos.ts",
|
|
225
|
-
line:
|
|
225
|
+
line: 32,
|
|
226
226
|
label: "demo-publish",
|
|
227
227
|
},
|
|
228
228
|
{ path: "src/docs/example/decorators-demo-init.ts", label: "demo data" },
|
|
@@ -230,7 +230,7 @@ export const DOCS_DEMO_SOURCE_REGISTRY: Record<string, DocsSource[]> = {
|
|
|
230
230
|
"demo-subscribe-dynamic": [
|
|
231
231
|
{
|
|
232
232
|
path: "src/docs/example/decorators-demo-subscribe-publish-get-demos.ts",
|
|
233
|
-
line:
|
|
233
|
+
line: 269,
|
|
234
234
|
label: "demo-subscribe-dynamic",
|
|
235
235
|
},
|
|
236
236
|
{ path: "src/docs/example/decorators-demo-init.ts", label: "demo data" },
|
|
@@ -238,7 +238,7 @@ export const DOCS_DEMO_SOURCE_REGISTRY: Record<string, DocsSource[]> = {
|
|
|
238
238
|
"demo-sub-template": [
|
|
239
239
|
{
|
|
240
240
|
path: "src/docs/example/decorators-demo-subscribe-publish-get-demos.ts",
|
|
241
|
-
line:
|
|
241
|
+
line: 238,
|
|
242
242
|
label: "demo-sub-template",
|
|
243
243
|
},
|
|
244
244
|
{ path: "src/docs/example/decorators-demo-init.ts", label: "demo data" },
|
|
@@ -246,16 +246,23 @@ export const DOCS_DEMO_SOURCE_REGISTRY: Record<string, DocsSource[]> = {
|
|
|
246
246
|
"demo-api-get": [
|
|
247
247
|
{
|
|
248
248
|
path: "src/docs/example/decorators-demo-subscribe-publish-get-demos.ts",
|
|
249
|
-
line:
|
|
249
|
+
line: 86,
|
|
250
250
|
label: "demo-api-get",
|
|
251
251
|
},
|
|
252
252
|
{ path: "src/docs/example/decorators-demo-geo.ts", label: "geo API" },
|
|
253
253
|
{ path: "src/docs/mock-api/router.ts", label: "mock router" },
|
|
254
254
|
],
|
|
255
|
+
"demo-api-get-method": [
|
|
256
|
+
{
|
|
257
|
+
path: "src/docs/example/decorators-demo-subscribe-publish-get-demos.ts",
|
|
258
|
+
label: "demo-api-get-method",
|
|
259
|
+
},
|
|
260
|
+
{ path: "src/docs/example/decorators-demo-geo.ts", label: "geo API" },
|
|
261
|
+
],
|
|
255
262
|
"demo-api-get-configuration-key": [
|
|
256
263
|
{
|
|
257
264
|
path: "src/docs/example/decorators-demo-subscribe-publish-get-demos.ts",
|
|
258
|
-
line:
|
|
265
|
+
line: 127,
|
|
259
266
|
label: "demo-api-get-configuration-key",
|
|
260
267
|
},
|
|
261
268
|
{ path: "src/docs/example/decorators-demo-geo.ts", label: "geo API" },
|
|
@@ -263,7 +270,7 @@ export const DOCS_DEMO_SOURCE_REGISTRY: Record<string, DocsSource[]> = {
|
|
|
263
270
|
"demo-api-get-publish-subscribe": [
|
|
264
271
|
{
|
|
265
272
|
path: "src/docs/example/decorators-demo-subscribe-publish-get-demos.ts",
|
|
266
|
-
line:
|
|
273
|
+
line: 178,
|
|
267
274
|
label: "demo-api-get-publish-subscribe",
|
|
268
275
|
},
|
|
269
276
|
{ path: "src/docs/example/decorators-demo-geo.ts", label: "geo API" },
|
|
@@ -276,6 +283,13 @@ export const DOCS_DEMO_SOURCE_REGISTRY: Record<string, DocsSource[]> = {
|
|
|
276
283
|
},
|
|
277
284
|
{ path: "src/docs/mock-api/router.ts", label: "mock router" },
|
|
278
285
|
],
|
|
286
|
+
"demo-api-post-method": [
|
|
287
|
+
{
|
|
288
|
+
path: "src/docs/example/decorators-demo-post.ts",
|
|
289
|
+
label: "demo-api-post-method",
|
|
290
|
+
},
|
|
291
|
+
{ path: "src/docs/mock-api/router.ts", label: "mock router" },
|
|
292
|
+
],
|
|
279
293
|
"demo-api-post-dynamic": [
|
|
280
294
|
{
|
|
281
295
|
path: "src/docs/example/decorators-demo-post.ts",
|
|
@@ -295,7 +309,7 @@ export const DOCS_DEMO_SOURCE_REGISTRY: Record<string, DocsSource[]> = {
|
|
|
295
309
|
"demo-handle": [
|
|
296
310
|
{
|
|
297
311
|
path: "src/docs/example/decorators-demo-subscribe-publish-get-demos.ts",
|
|
298
|
-
line:
|
|
312
|
+
line: 316,
|
|
299
313
|
label: "demo-handle",
|
|
300
314
|
},
|
|
301
315
|
{ path: "src/docs/example/decorators-demo-init.ts", label: "demo data" },
|
|
@@ -303,7 +317,7 @@ export const DOCS_DEMO_SOURCE_REGISTRY: Record<string, DocsSource[]> = {
|
|
|
303
317
|
"demo-handle-dynamic": [
|
|
304
318
|
{
|
|
305
319
|
path: "src/docs/example/decorators-demo-subscribe-publish-get-demos.ts",
|
|
306
|
-
line:
|
|
320
|
+
line: 351,
|
|
307
321
|
label: "demo-handle-dynamic",
|
|
308
322
|
},
|
|
309
323
|
{ path: "src/docs/example/decorators-demo-init.ts", label: "demo data" },
|
|
@@ -65,6 +65,12 @@ export const docsDemoPostPublishRequestKey = new DataProviderKey<RegisterRequest
|
|
|
65
65
|
export const docsDemoPostTriggerKey = new DataProviderKey<number>(
|
|
66
66
|
"docsDemoPostTrigger",
|
|
67
67
|
);
|
|
68
|
+
export const docsDemoPostMethodRequestKey = new DataProviderKey<RegisterRequest>(
|
|
69
|
+
"docsDemoPostMethodRequest",
|
|
70
|
+
);
|
|
71
|
+
export const docsDemoPostMethodTriggerKey = new DataProviderKey<number>(
|
|
72
|
+
"docsDemoPostMethodTrigger",
|
|
73
|
+
);
|
|
68
74
|
export const docsDemoPostDynamicRequestKey = new DataProviderKey<SessionSyncRequest>(
|
|
69
75
|
"docsDemoPostDynamicRequest",
|
|
70
76
|
);
|
|
@@ -140,6 +146,70 @@ export class DemoApiPost extends LitElement {
|
|
|
140
146
|
}
|
|
141
147
|
}
|
|
142
148
|
|
|
149
|
+
@customElement("demo-api-post-method")
|
|
150
|
+
export class DemoApiPostMethod extends LitElement {
|
|
151
|
+
static styles = [tailwind];
|
|
152
|
+
|
|
153
|
+
@state()
|
|
154
|
+
private payload?: ApiResult<RegisterResult>;
|
|
155
|
+
|
|
156
|
+
connectedCallback() {
|
|
157
|
+
super.connectedCallback();
|
|
158
|
+
set(docsDemoPostMethodRequestKey, {
|
|
159
|
+
email: "method@concorde.local",
|
|
160
|
+
password: "method-password",
|
|
161
|
+
});
|
|
162
|
+
set(docsDemoPostMethodTriggerKey, 0);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
@post(
|
|
166
|
+
registerPostEndpoint,
|
|
167
|
+
docsDemoPostMethodRequestKey,
|
|
168
|
+
docsDemoReqresApiConfigurationKey,
|
|
169
|
+
{ triggerKey: docsDemoPostMethodTriggerKey },
|
|
170
|
+
)
|
|
171
|
+
handlePayload(payload?: ApiResult<RegisterResult>) {
|
|
172
|
+
this.payload = payload;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private updateBody() {
|
|
176
|
+
set(docsDemoPostMethodRequestKey, {
|
|
177
|
+
email: `method-${Date.now()}@concorde.local`,
|
|
178
|
+
password: "method-password",
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private resend() {
|
|
183
|
+
dp(docsDemoPostMethodTriggerKey).invalidate();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
render() {
|
|
187
|
+
const result = this.payload?.result;
|
|
188
|
+
return html`
|
|
189
|
+
<div class="space-y-3">
|
|
190
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
191
|
+
<span class="text-sm font-medium">✉️ POST function mode</span>
|
|
192
|
+
<sonic-button size="sm" @click=${this.updateBody}>
|
|
193
|
+
Update body & POST
|
|
194
|
+
</sonic-button>
|
|
195
|
+
<sonic-button size="sm" variant="secondary" @click=${this.resend}>
|
|
196
|
+
Invalidate trigger
|
|
197
|
+
</sonic-button>
|
|
198
|
+
</div>
|
|
199
|
+
<p class="text-sm text-neutral-600 dark:text-neutral-400">
|
|
200
|
+
The <code>@post</code> decorator calls
|
|
201
|
+
<code>handlePayload</code> with the complete API payload.
|
|
202
|
+
</p>
|
|
203
|
+
<p class="text-xs text-neutral-500">
|
|
204
|
+
HTTP status:
|
|
205
|
+
<strong>${this.payload?.response?.status ?? "loading…"}</strong>
|
|
206
|
+
· email: <strong>${result?.email ?? "—"}</strong>
|
|
207
|
+
</p>
|
|
208
|
+
</div>
|
|
209
|
+
`;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
143
213
|
@customElement("demo-api-post-dynamic")
|
|
144
214
|
export class DemoApiPostDynamic extends LitElement {
|
|
145
215
|
static styles = [tailwind];
|