@supersoniks/concorde 4.9.92 → 4.9.94
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 +299 -292
- package/concorde-core.es.js +10139 -6035
- package/dist/concorde-core.bundle.js +299 -292
- package/dist/concorde-core.es.js +10139 -6035
- package/dist/docs-mock-api-sw.js +110 -0
- package/dist/docs-mock-api-sw.js.map +3 -3
- package/dist/js/core/components/functional/functional.js +1 -0
- package/dist/js/core/components/functional/functional.js.map +1 -1
- package/dist/js/core/components/functional/jsonata/jsonata.js +119 -0
- package/dist/js/core/components/functional/jsonata/jsonata.js.map +1 -0
- package/dist/js/core/components/ui/button/button.js +1 -1
- package/dist/js/core/components/ui/button/button.js.map +1 -1
- package/dist/js/core/decorators/Subscriber.js +1 -0
- package/dist/js/core/decorators/Subscriber.js.map +1 -1
- package/dist/js/core/decorators/api.js +108 -16
- package/dist/js/core/decorators/api.js.map +1 -1
- package/dist/js/core/decorators/subscriber/derive.js +60 -0
- package/dist/js/core/decorators/subscriber/derive.js.map +1 -0
- package/dist/js/core/utils/api.js +29 -8
- package/dist/js/core/utils/api.js.map +1 -1
- package/dist/js/core/utils/derive.js +246 -0
- package/dist/js/core/utils/derive.js.map +1 -0
- package/dist/js/core/utils/jsonataHelpers.js +69 -0
- package/dist/js/core/utils/jsonataHelpers.js.map +1 -0
- package/dist/js/core/utils/jsonataRuntime.js +75 -0
- package/dist/js/core/utils/jsonataRuntime.js.map +1 -0
- package/dist/js/utils.js +2 -0
- package/dist/js/utils.js.map +1 -1
- package/dist/types/core/components/functional/functional.d.ts +1 -0
- package/dist/types/core/components/functional/functional.d.ts.map +1 -1
- package/dist/types/core/components/functional/jsonata/jsonata.d.ts +29 -0
- package/dist/types/core/components/functional/jsonata/jsonata.d.ts.map +1 -0
- package/dist/types/core/components/ui/button/button.d.ts +1 -1
- package/dist/types/core/components/ui/button/button.d.ts.map +1 -1
- package/dist/types/core/components/ui/form/checkbox/checkbox.d.ts +3 -3
- package/dist/types/core/components/ui/form/input/input.d.ts +3 -3
- package/dist/types/core/components/ui/form/input-autocomplete/input-autocomplete.d.ts +3 -3
- package/dist/types/core/components/ui/form/textarea/textarea.d.ts +3 -3
- package/dist/types/core/decorators/Subscriber.d.ts +2 -0
- package/dist/types/core/decorators/Subscriber.d.ts.map +1 -1
- package/dist/types/core/decorators/api.d.ts +21 -12
- package/dist/types/core/decorators/api.d.ts.map +1 -1
- package/dist/types/core/decorators/subscriber/derive.d.ts +30 -0
- package/dist/types/core/decorators/subscriber/derive.d.ts.map +1 -0
- package/dist/types/core/utils/api.d.ts +1 -1
- package/dist/types/core/utils/api.d.ts.map +1 -1
- package/dist/types/core/utils/derive.d.ts +40 -0
- package/dist/types/core/utils/derive.d.ts.map +1 -0
- package/dist/types/core/utils/jsonataHelpers.d.ts +15 -0
- package/dist/types/core/utils/jsonataHelpers.d.ts.map +1 -0
- package/dist/types/core/utils/jsonataRuntime.d.ts +13 -0
- package/dist/types/core/utils/jsonataRuntime.d.ts.map +1 -0
- package/dist/types/mixins.d.ts +3 -3
- package/dist/types/utils.d.ts +2 -0
- package/dist/types/utils.d.ts.map +1 -1
- package/docs/assets/{index-DURr6O71.js → index-DpIkksht.js} +652 -479
- package/docs/crawl/docs/_decorators/get.html +27 -6
- package/docs/crawl/docs/_decorators/patch.html +6 -0
- package/docs/crawl/docs/_decorators/post.html +8 -0
- package/docs/crawl/docs/_decorators/put.html +6 -0
- package/docs/crawl/hubs/decorators.html +36 -31
- package/docs/crawl/hubs/remote-data.html +36 -31
- package/docs/index.html +1 -1
- package/docs/llms-full.txt +47 -3
- package/docs/src/docs/_decorators/get.md +31 -0
- package/docs/src/docs/_decorators/patch.md +9 -0
- package/docs/src/docs/_decorators/post.md +11 -0
- package/docs/src/docs/_decorators/put.md +9 -0
- package/docs/src/docs/search/docs-search.json +120 -5
- package/docs/src/tsconfig.json +268 -268
- package/package.json +28 -3
- package/public/docs-mock-api-sw.js +110 -0
- package/public/docs-mock-api-sw.js.map +3 -3
- package/src/core/components/functional/functional.ts +1 -0
- package/src/core/components/functional/jsonata/jsonata.md +56 -0
- package/src/core/components/functional/jsonata/jsonata.spec.ts +202 -0
- package/src/core/components/functional/jsonata/jsonata.ts +116 -0
- package/src/core/components/ui/button/button.ts +2 -2
- package/src/core/decorators/Subscriber.ts +2 -0
- package/src/core/decorators/api.post.spec.ts +99 -4
- package/src/core/decorators/api.spec.ts +90 -0
- package/src/core/decorators/api.ts +212 -92
- package/src/core/decorators/subscriber/derive.ts +95 -0
- package/src/core/utils/api.spec.ts +73 -0
- package/src/core/utils/api.ts +44 -11
- package/src/core/utils/derive.spec.ts +190 -0
- package/src/core/utils/derive.ts +357 -0
- package/src/core/utils/jsonataHelpers.ts +100 -0
- package/src/core/utils/jsonataRuntime.ts +88 -0
- package/src/docs/_core-concept/dataFlow.md +9 -0
- package/src/docs/_decorators/get.md +31 -0
- package/src/docs/_decorators/patch.md +9 -0
- package/src/docs/_decorators/post.md +11 -0
- package/src/docs/_decorators/put.md +9 -0
- package/src/docs/_misc/docs-mock-api.md +3 -0
- package/src/docs/components/docs-demo-sources.ts +40 -0
- package/src/docs/docs.ts +1 -0
- package/src/docs/example/decorators-demo-post.ts +70 -0
- package/src/docs/example/decorators-demo-subscribe-publish-get-demos.ts +59 -0
- package/src/docs/example/docs-jsonata-demos.ts +128 -0
- package/src/docs/mock-api/fixtures-jsonata.ts +131 -0
- package/src/docs/mock-api/router.ts +26 -0
- package/src/docs/navigation/navigation.ts +4 -0
- package/src/docs/search/docs-search.json +300 -5
- package/src/tsconfig.json +18 -0
- package/src/tsconfig.tsbuildinfo +1 -1
- package/src/utils.ts +13 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import TestUtils from "@supersoniks/concorde/test-utils/TestUtils";
|
|
3
|
+
import { DataProviderKey } from "@supersoniks/concorde/core/utils/dataProviderKey";
|
|
4
|
+
import { deriveStateKey } from "@supersoniks/concorde/core/utils/derive";
|
|
5
|
+
import { clearJsonataCache } from "@supersoniks/concorde/core/utils/jsonataRuntime";
|
|
6
|
+
import { dp, set } from "@supersoniks/concorde/utils";
|
|
7
|
+
import "./jsonata";
|
|
8
|
+
import "../states/states";
|
|
9
|
+
import {
|
|
10
|
+
DOCS_COFFEE_BAGS,
|
|
11
|
+
DOCS_SPACE_CAT_MISSIONS,
|
|
12
|
+
} from "../../../../docs/mock-api/fixtures-jsonata";
|
|
13
|
+
|
|
14
|
+
const missionsKey = new DataProviderKey<typeof DOCS_SPACE_CAT_MISSIONS>(
|
|
15
|
+
"jsonata-test-missions",
|
|
16
|
+
);
|
|
17
|
+
const formKey = new DataProviderKey<{ top: number | string }>(
|
|
18
|
+
"jsonata-test-form",
|
|
19
|
+
);
|
|
20
|
+
const boardKey = new DataProviderKey<
|
|
21
|
+
Array<{ name: string; points: number }>
|
|
22
|
+
>("jsonata-test-board");
|
|
23
|
+
|
|
24
|
+
function wait(ms: number) {
|
|
25
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
clearJsonataCache();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("sonic-jsonata", () => {
|
|
33
|
+
it("calcule un leaderboard (groupby + top N) en live", async () => {
|
|
34
|
+
set(missionsKey, DOCS_SPACE_CAT_MISSIONS);
|
|
35
|
+
set(formKey, { top: 2 });
|
|
36
|
+
|
|
37
|
+
const [elt] = TestUtils.bootstrap(`
|
|
38
|
+
<sonic-jsonata
|
|
39
|
+
inputProvider="jsonata-test-missions"
|
|
40
|
+
outputProvider="jsonata-test-board"
|
|
41
|
+
formDataProvider="jsonata-test-form"
|
|
42
|
+
expression='$input{name: $sum(points)} ~> $each(function($v, $k) {{"name": $k, "points": $v}}) ^(>points)[[0..$number($top)-1]]'
|
|
43
|
+
></sonic-jsonata>
|
|
44
|
+
`);
|
|
45
|
+
|
|
46
|
+
await wait(80);
|
|
47
|
+
const result = dp(boardKey).get();
|
|
48
|
+
expect(result).toHaveLength(2);
|
|
49
|
+
expect(result[0].name).toBe("Nebula Paws");
|
|
50
|
+
expect(result[0].points).toBe(75);
|
|
51
|
+
expect(result[1].name).toBe("Captain Whiskers");
|
|
52
|
+
expect(dp(deriveStateKey(boardKey)).get().status).toBe("ready");
|
|
53
|
+
|
|
54
|
+
// Comme sonic-input / sonic-select : mutation profonde sur le form DP
|
|
55
|
+
dp(formKey).top.set(1);
|
|
56
|
+
await wait(80);
|
|
57
|
+
const top1 = dp(boardKey).get();
|
|
58
|
+
const rows = Array.isArray(top1) ? top1 : [top1];
|
|
59
|
+
expect(rows).toHaveLength(1);
|
|
60
|
+
expect(rows[0].name).toBe("Nebula Paws");
|
|
61
|
+
|
|
62
|
+
(elt as HTMLElement).remove();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("agrège le stock café par origine avec filtre roast", async () => {
|
|
66
|
+
const bagsKey = new DataProviderKey<typeof DOCS_COFFEE_BAGS>(
|
|
67
|
+
"jsonata-test-bags",
|
|
68
|
+
);
|
|
69
|
+
const coffeeForm = new DataProviderKey<{ roast: string }>(
|
|
70
|
+
"jsonata-test-coffee-form",
|
|
71
|
+
);
|
|
72
|
+
const stockKey = new DataProviderKey<Array<{ origin: string; kg: number }>>(
|
|
73
|
+
"jsonata-test-stock",
|
|
74
|
+
);
|
|
75
|
+
set(bagsKey, DOCS_COFFEE_BAGS);
|
|
76
|
+
set(coffeeForm, { roast: "light" });
|
|
77
|
+
|
|
78
|
+
const [elt] = TestUtils.bootstrap(`
|
|
79
|
+
<sonic-jsonata
|
|
80
|
+
inputProvider="jsonata-test-bags"
|
|
81
|
+
outputProvider="jsonata-test-stock"
|
|
82
|
+
formDataProvider="jsonata-test-coffee-form"
|
|
83
|
+
expression='($roast = "all" ? $input : $input[roast=$roast]){origin: $sum(kg)} ~> $each(function($v, $k) {{"origin": $k, "kg": $v}}) ^(>kg)'
|
|
84
|
+
></sonic-jsonata>
|
|
85
|
+
`);
|
|
86
|
+
|
|
87
|
+
await wait(80);
|
|
88
|
+
const stock = dp(stockKey).get();
|
|
89
|
+
expect(stock.length).toBeGreaterThan(0);
|
|
90
|
+
expect(stock[0].origin).toBe("Ethiopia");
|
|
91
|
+
expect(stock[0].kg).toBe(16);
|
|
92
|
+
expect(stock.map((r) => r.origin)).not.toContain("Brazil");
|
|
93
|
+
|
|
94
|
+
dp(coffeeForm).roast.set("all");
|
|
95
|
+
await wait(80);
|
|
96
|
+
const all = dp(stockKey).get();
|
|
97
|
+
expect(all.map((r) => r.origin)).toContain("Brazil");
|
|
98
|
+
|
|
99
|
+
(elt as HTMLElement).remove();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("expose status error sur expression invalide", async () => {
|
|
103
|
+
const err = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
104
|
+
set(missionsKey, DOCS_SPACE_CAT_MISSIONS);
|
|
105
|
+
|
|
106
|
+
const [elt] = TestUtils.bootstrap(`
|
|
107
|
+
<sonic-jsonata
|
|
108
|
+
inputProvider="jsonata-test-missions"
|
|
109
|
+
outputProvider="jsonata-test-board"
|
|
110
|
+
expression='$input ~> $$$$invalid'
|
|
111
|
+
></sonic-jsonata>
|
|
112
|
+
`);
|
|
113
|
+
|
|
114
|
+
await wait(80);
|
|
115
|
+
const state = dp(deriveStateKey(boardKey)).get();
|
|
116
|
+
expect(state.status).toBe("error");
|
|
117
|
+
expect(state.error).toBeTruthy();
|
|
118
|
+
err.mockRestore();
|
|
119
|
+
(elt as HTMLElement).remove();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("branche sonic-states sur le DP /_state_", async () => {
|
|
123
|
+
set(missionsKey, DOCS_SPACE_CAT_MISSIONS);
|
|
124
|
+
set(formKey, { top: 3 });
|
|
125
|
+
|
|
126
|
+
const [wrap] = TestUtils.bootstrap(`
|
|
127
|
+
<div>
|
|
128
|
+
<sonic-jsonata
|
|
129
|
+
inputProvider="jsonata-test-missions"
|
|
130
|
+
outputProvider="jsonata-test-board"
|
|
131
|
+
formDataProvider="jsonata-test-form"
|
|
132
|
+
expression='$input[0].name'
|
|
133
|
+
></sonic-jsonata>
|
|
134
|
+
<sonic-states dataProvider="jsonata-test-board/_state_" data-path="status">
|
|
135
|
+
<template data-value="ready"><span id="ok">prêt</span></template>
|
|
136
|
+
<template data-value="error"><span id="ko">erreur</span></template>
|
|
137
|
+
</sonic-states>
|
|
138
|
+
</div>
|
|
139
|
+
`);
|
|
140
|
+
|
|
141
|
+
await wait(80);
|
|
142
|
+
expect(wrap.querySelector("#ok")?.textContent).toBe("prêt");
|
|
143
|
+
(wrap as HTMLElement).remove();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("calcule total + moyenne par ligne d’un tableau", async () => {
|
|
147
|
+
const linesKey = new DataProviderKey<
|
|
148
|
+
Array<{ qty: number; unitPrice: number }>
|
|
149
|
+
>("jsonata-test-lines");
|
|
150
|
+
const taxForm = new DataProviderKey<{ taxPercent: number | string }>(
|
|
151
|
+
"jsonata-test-tax-form",
|
|
152
|
+
);
|
|
153
|
+
const totalsKey = new DataProviderKey<{
|
|
154
|
+
count: number;
|
|
155
|
+
subtotal: number;
|
|
156
|
+
average: number;
|
|
157
|
+
tax: number;
|
|
158
|
+
grandTotal: number;
|
|
159
|
+
}>("jsonata-test-totals");
|
|
160
|
+
|
|
161
|
+
set(linesKey, [
|
|
162
|
+
{ qty: 4, unitPrice: 3.5 },
|
|
163
|
+
{ qty: 2, unitPrice: 6 },
|
|
164
|
+
{ qty: 1, unitPrice: 8.5 },
|
|
165
|
+
]);
|
|
166
|
+
set(taxForm, { taxPercent: 10 });
|
|
167
|
+
|
|
168
|
+
const [elt] = TestUtils.bootstrap(`
|
|
169
|
+
<sonic-jsonata
|
|
170
|
+
inputProvider="jsonata-test-lines"
|
|
171
|
+
outputProvider="jsonata-test-totals"
|
|
172
|
+
formDataProvider="jsonata-test-tax-form"
|
|
173
|
+
expression='(
|
|
174
|
+
$lines := $input.(qty * unitPrice);
|
|
175
|
+
{
|
|
176
|
+
"count": $count($input),
|
|
177
|
+
"subtotal": $round($sum($lines), 2),
|
|
178
|
+
"average": $round($average($lines), 2),
|
|
179
|
+
"tax": $round($sum($lines) * $number($taxPercent) / 100, 2),
|
|
180
|
+
"grandTotal": $round($sum($lines) * (1 + $number($taxPercent) / 100), 2)
|
|
181
|
+
}
|
|
182
|
+
)'
|
|
183
|
+
></sonic-jsonata>
|
|
184
|
+
`);
|
|
185
|
+
|
|
186
|
+
await wait(80);
|
|
187
|
+
expect(dp(totalsKey).get()).toMatchObject({
|
|
188
|
+
count: 3,
|
|
189
|
+
subtotal: 34.5,
|
|
190
|
+
average: 11.5,
|
|
191
|
+
tax: 3.45,
|
|
192
|
+
grandTotal: 37.95,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
dp(taxForm).taxPercent.set(20);
|
|
196
|
+
await wait(80);
|
|
197
|
+
expect(dp(totalsKey).get().tax).toBe(6.9);
|
|
198
|
+
expect(dp(totalsKey).get().grandTotal).toBe(41.4);
|
|
199
|
+
|
|
200
|
+
(elt as HTMLElement).remove();
|
|
201
|
+
});
|
|
202
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { LitElement, html, css } from "lit";
|
|
2
|
+
import { customElement, property } from "lit/decorators.js";
|
|
3
|
+
import {
|
|
4
|
+
derive,
|
|
5
|
+
type DeriveHandle,
|
|
6
|
+
} from "@supersoniks/concorde/core/utils/derive";
|
|
7
|
+
import {
|
|
8
|
+
compileJsonata,
|
|
9
|
+
validateOutputSchema,
|
|
10
|
+
} from "@supersoniks/concorde/core/utils/jsonataRuntime";
|
|
11
|
+
import { DataProviderKey } from "@supersoniks/concorde/core/utils/dataProviderKey";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Adaptateur JSONata de `derive` : 1 entrée → expression → 1 sortie (readonly).
|
|
15
|
+
*
|
|
16
|
+
* Contexte d’évaluation :
|
|
17
|
+
* - `$input` = contenu de `inputProvider`
|
|
18
|
+
* - champs du `formDataProvider` optionnel = `$top`, `$roast`, …
|
|
19
|
+
*/
|
|
20
|
+
@customElement("sonic-jsonata")
|
|
21
|
+
export default class SonicJsonata extends LitElement {
|
|
22
|
+
static styles = [
|
|
23
|
+
css`
|
|
24
|
+
:host {
|
|
25
|
+
display: contents;
|
|
26
|
+
}
|
|
27
|
+
`,
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/** DataProvider d’entrée (source). */
|
|
31
|
+
@property({ type: String })
|
|
32
|
+
inputProvider = "";
|
|
33
|
+
|
|
34
|
+
/** DataProvider de sortie (agrégat, readonly). */
|
|
35
|
+
@property({ type: String })
|
|
36
|
+
outputProvider = "";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Form / variables optionnelles (`$top`, `$roast`, …).
|
|
40
|
+
* Même id que le `formDataProvider` du formulaire — recalcule sans refetch.
|
|
41
|
+
*/
|
|
42
|
+
@property({ type: String })
|
|
43
|
+
formDataProvider = "";
|
|
44
|
+
|
|
45
|
+
@property({ type: String })
|
|
46
|
+
expression = "";
|
|
47
|
+
|
|
48
|
+
@property({ type: Object })
|
|
49
|
+
outputSchema: Record<string, unknown> | null = null;
|
|
50
|
+
|
|
51
|
+
private handle: DeriveHandle | null = null;
|
|
52
|
+
|
|
53
|
+
connectedCallback(): void {
|
|
54
|
+
super.connectedCallback();
|
|
55
|
+
this.restart();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
disconnectedCallback(): void {
|
|
59
|
+
this.handle?.stop();
|
|
60
|
+
this.handle = null;
|
|
61
|
+
super.disconnectedCallback();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
protected updated(
|
|
65
|
+
changed: Map<string | number | symbol, unknown>,
|
|
66
|
+
): void {
|
|
67
|
+
if (
|
|
68
|
+
changed.has("expression") ||
|
|
69
|
+
changed.has("inputProvider") ||
|
|
70
|
+
changed.has("outputProvider") ||
|
|
71
|
+
changed.has("formDataProvider") ||
|
|
72
|
+
changed.has("outputSchema")
|
|
73
|
+
) {
|
|
74
|
+
this.restart();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private restart(): void {
|
|
79
|
+
this.handle?.stop();
|
|
80
|
+
this.handle = null;
|
|
81
|
+
if (!this.isConnected) return;
|
|
82
|
+
if (!this.inputProvider || !this.outputProvider) return;
|
|
83
|
+
if (!this.expression?.trim()) return;
|
|
84
|
+
|
|
85
|
+
const expressionSource = this.expression;
|
|
86
|
+
const schema = this.outputSchema;
|
|
87
|
+
|
|
88
|
+
this.handle = derive({
|
|
89
|
+
inputs: { input: new DataProviderKey(this.inputProvider) },
|
|
90
|
+
output: new DataProviderKey(this.outputProvider),
|
|
91
|
+
variables: this.formDataProvider
|
|
92
|
+
? new DataProviderKey(this.formDataProvider)
|
|
93
|
+
: undefined,
|
|
94
|
+
compute: async ({ input }, vars) => {
|
|
95
|
+
const expr = compileJsonata(expressionSource);
|
|
96
|
+
const bindings: Record<string, unknown> = {
|
|
97
|
+
...((vars ?? {}) as Record<string, unknown>),
|
|
98
|
+
input,
|
|
99
|
+
};
|
|
100
|
+
let result: unknown;
|
|
101
|
+
try {
|
|
102
|
+
result = await expr.evaluate({}, bindings);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
105
|
+
throw new Error(`[sonic-jsonata] Évaluation : ${message}`);
|
|
106
|
+
}
|
|
107
|
+
validateOutputSchema(result, schema);
|
|
108
|
+
return result;
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
render() {
|
|
114
|
+
return html`<slot></slot>`;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -506,8 +506,8 @@ export class Button extends FormCheckable(FormElement(Subscriber(LitElement))) {
|
|
|
506
506
|
* L'url
|
|
507
507
|
*/
|
|
508
508
|
private _href: Route | string = "";
|
|
509
|
-
@property({ type: String }) set href(value: Route | string) {
|
|
510
|
-
this._href = value;
|
|
509
|
+
@property({ type: String }) set href(value: Route | string | null) {
|
|
510
|
+
this._href = value ?? "";
|
|
511
511
|
const str = this._href.toString();
|
|
512
512
|
if (str && str.indexOf("http") != 0) {
|
|
513
513
|
LocationHandler.onChange(this);
|
|
@@ -4,6 +4,8 @@ export { subscribe } from "./subscriber/subscribe";
|
|
|
4
4
|
export { onAssign } from "./subscriber/onAssign";
|
|
5
5
|
export { handle, Skip } from "./subscriber/handle";
|
|
6
6
|
export type { HandleOptions } from "./subscriber/handle";
|
|
7
|
+
export { derive } from "./subscriber/derive";
|
|
8
|
+
export type { DeriveDecoratorOptions } from "./subscriber/derive";
|
|
7
9
|
export { autoSubscribe } from "./subscriber/autoSubscribe";
|
|
8
10
|
export { autoFill } from "./subscriber/autoFill";
|
|
9
11
|
export { ancestorAttribute } from "./subscriber/ancestorAttribute";
|
|
@@ -303,6 +303,91 @@ describe("post", () => {
|
|
|
303
303
|
);
|
|
304
304
|
expect(el.payload?.result).toEqual({ ok: true });
|
|
305
305
|
});
|
|
306
|
+
|
|
307
|
+
it("transmet le payload complet et conserve le retrigger du POST", async () => {
|
|
308
|
+
const bodyPubId = "postSpecHandlerBody";
|
|
309
|
+
const triggerPubId = "postSpecHandlerTrigger";
|
|
310
|
+
PublisherManager.get(bodyPubId).set({ tick: 1 });
|
|
311
|
+
const bodyKey = new DataProviderKey<{ tick: number }>(bodyPubId);
|
|
312
|
+
const triggerKey = new DataProviderKey<number>(triggerPubId);
|
|
313
|
+
const ep = new Endpoint<{ ok: boolean }>("items/handler");
|
|
314
|
+
const localPost = post(ep, bodyKey, {
|
|
315
|
+
autoPostOnBodyMutation: false,
|
|
316
|
+
triggerKey,
|
|
317
|
+
});
|
|
318
|
+
const received: Array<ApiResult<{ ok: boolean }> | undefined> = [];
|
|
319
|
+
const errorPayload = mockPayload({ ok: false });
|
|
320
|
+
errorPayload.response = new Response(JSON.stringify({ error: true }), {
|
|
321
|
+
status: 409,
|
|
322
|
+
headers: { "Content-Type": "application/json" },
|
|
323
|
+
});
|
|
324
|
+
vi.mocked(API.prototype.postDetailed).mockResolvedValueOnce(
|
|
325
|
+
errorPayload as never,
|
|
326
|
+
);
|
|
327
|
+
const tag = "test-api-post-handler";
|
|
328
|
+
|
|
329
|
+
class C extends HTMLElement {
|
|
330
|
+
@localPost
|
|
331
|
+
onPayload(payload: ApiResult<{ ok: boolean }> | undefined) {
|
|
332
|
+
received.push(payload);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (!customElements.get(tag)) {
|
|
336
|
+
customElements.define(tag, C);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const wrap = document.createElement("div");
|
|
340
|
+
wrap.setAttribute("serviceURL", "https://api.example.test");
|
|
341
|
+
const el = document.createElement(tag) as InstanceType<typeof C>;
|
|
342
|
+
wrap.appendChild(el);
|
|
343
|
+
document.body.appendChild(wrap);
|
|
344
|
+
await flushSendFrames();
|
|
345
|
+
|
|
346
|
+
expect(API.prototype.postDetailed).not.toHaveBeenCalled();
|
|
347
|
+
await localPost.send(el);
|
|
348
|
+
expect(received).toHaveLength(1);
|
|
349
|
+
expect(received[0]?.response?.status).toBe(409);
|
|
350
|
+
|
|
351
|
+
PublisherManager.get(triggerPubId).invalidate();
|
|
352
|
+
await flushSendFrames();
|
|
353
|
+
expect(API.prototype.postDetailed).toHaveBeenCalledTimes(2);
|
|
354
|
+
expect(received).toHaveLength(2);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("transmet undefined à une méthode @post sur une erreur de transport", async () => {
|
|
358
|
+
const bodyPubId = "postSpecHandlerTransportBody";
|
|
359
|
+
PublisherManager.get(bodyPubId).set({ tick: 1 });
|
|
360
|
+
const bodyKey = new DataProviderKey<{ tick: number }>(bodyPubId);
|
|
361
|
+
const ep = new Endpoint<{ ok: boolean }>("items/handler-transport");
|
|
362
|
+
const localPost = post(ep, bodyKey, {
|
|
363
|
+
autoPostOnBodyMutation: false,
|
|
364
|
+
});
|
|
365
|
+
const received: Array<ApiResult<{ ok: boolean }> | undefined> = [];
|
|
366
|
+
vi.mocked(API.prototype.postDetailed).mockRejectedValueOnce(
|
|
367
|
+
new Error("network failure"),
|
|
368
|
+
);
|
|
369
|
+
const tag = "test-api-post-handler-transport";
|
|
370
|
+
|
|
371
|
+
class C extends HTMLElement {
|
|
372
|
+
@localPost
|
|
373
|
+
onPayload(payload: ApiResult<{ ok: boolean }> | undefined) {
|
|
374
|
+
received.push(payload);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (!customElements.get(tag)) {
|
|
378
|
+
customElements.define(tag, C);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const wrap = document.createElement("div");
|
|
382
|
+
wrap.setAttribute("serviceURL", "https://api.example.test");
|
|
383
|
+
const el = document.createElement(tag) as InstanceType<typeof C>;
|
|
384
|
+
wrap.appendChild(el);
|
|
385
|
+
document.body.appendChild(wrap);
|
|
386
|
+
await flushSendFrames();
|
|
387
|
+
|
|
388
|
+
await expect(localPost.send(el)).rejects.toThrow("network failure");
|
|
389
|
+
expect(received).toEqual([undefined]);
|
|
390
|
+
});
|
|
306
391
|
});
|
|
307
392
|
|
|
308
393
|
describe("put / patch", () => {
|
|
@@ -319,11 +404,15 @@ describe("put / patch", () => {
|
|
|
319
404
|
PublisherManager.get(bodyPubId).set({ name: "x" });
|
|
320
405
|
const bodyKey = new DataProviderKey<{ name: string }>(bodyPubId);
|
|
321
406
|
const ep = new Endpoint<{ ok: boolean }>("items/1");
|
|
407
|
+
const localPut = put(ep, bodyKey);
|
|
408
|
+
const received: Array<ApiResult<{ ok: boolean }> | undefined> = [];
|
|
322
409
|
const tag = "test-api-put";
|
|
323
410
|
|
|
324
411
|
class C extends HTMLElement {
|
|
325
|
-
@
|
|
326
|
-
payload: ApiResult<{ ok: boolean }> |
|
|
412
|
+
@localPut
|
|
413
|
+
onPayload(payload: ApiResult<{ ok: boolean }> | undefined) {
|
|
414
|
+
received.push(payload);
|
|
415
|
+
}
|
|
327
416
|
}
|
|
328
417
|
if (!customElements.get(tag)) {
|
|
329
418
|
customElements.define(tag, C);
|
|
@@ -338,6 +427,7 @@ describe("put / patch", () => {
|
|
|
338
427
|
expect(API.prototype.putDetailed).toHaveBeenCalledWith("items/1", {
|
|
339
428
|
name: "x",
|
|
340
429
|
});
|
|
430
|
+
expect(received).toHaveLength(1);
|
|
341
431
|
});
|
|
342
432
|
|
|
343
433
|
it("@patch appelle patchDetailed", async () => {
|
|
@@ -348,11 +438,15 @@ describe("put / patch", () => {
|
|
|
348
438
|
PublisherManager.get(bodyPubId).set({ name: "y" });
|
|
349
439
|
const bodyKey = new DataProviderKey<{ name: string }>(bodyPubId);
|
|
350
440
|
const ep = new Endpoint<{ ok: boolean }>("items/1");
|
|
441
|
+
const localPatch = patch(ep, bodyKey);
|
|
442
|
+
const received: Array<ApiResult<{ ok: boolean }> | undefined> = [];
|
|
351
443
|
const tag = "test-api-patch";
|
|
352
444
|
|
|
353
445
|
class C extends HTMLElement {
|
|
354
|
-
@
|
|
355
|
-
payload: ApiResult<{ ok: boolean }> |
|
|
446
|
+
@localPatch
|
|
447
|
+
onPayload(payload: ApiResult<{ ok: boolean }> | undefined) {
|
|
448
|
+
received.push(payload);
|
|
449
|
+
}
|
|
356
450
|
}
|
|
357
451
|
if (!customElements.get(tag)) {
|
|
358
452
|
customElements.define(tag, C);
|
|
@@ -367,5 +461,6 @@ describe("put / patch", () => {
|
|
|
367
461
|
expect(API.prototype.patchDetailed).toHaveBeenCalledWith("items/1", {
|
|
368
462
|
name: "y",
|
|
369
463
|
});
|
|
464
|
+
expect(received).toHaveLength(1);
|
|
370
465
|
});
|
|
371
466
|
});
|
|
@@ -190,6 +190,96 @@ describe("get", () => {
|
|
|
190
190
|
expect(el.payload?.result).toEqual({ id: "loaded" });
|
|
191
191
|
});
|
|
192
192
|
|
|
193
|
+
it("transmet le payload complet à une méthode @get", async () => {
|
|
194
|
+
const itemEp = new Endpoint<{ id: string }>("items/handler");
|
|
195
|
+
const received: Array<ApiResult<{ id: string }> | undefined> = [];
|
|
196
|
+
const tag = "test-api-get-handler";
|
|
197
|
+
const errorPayload = mockPayload({ id: "error" });
|
|
198
|
+
errorPayload.response = new Response(JSON.stringify({ error: true }), {
|
|
199
|
+
status: 422,
|
|
200
|
+
headers: { "Content-Type": "application/json" },
|
|
201
|
+
});
|
|
202
|
+
vi.mocked(API.prototype.getDetailed).mockResolvedValueOnce(
|
|
203
|
+
errorPayload as never,
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
class C extends HTMLElement {
|
|
207
|
+
@get(itemEp)
|
|
208
|
+
onPayload(payload: ApiResult<{ id: string }> | undefined) {
|
|
209
|
+
received.push(payload);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (!customElements.get(tag)) {
|
|
213
|
+
customElements.define(tag, C);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const wrap = document.createElement("div");
|
|
217
|
+
wrap.setAttribute("serviceURL", "https://api.example.test");
|
|
218
|
+
wrap.appendChild(document.createElement(tag));
|
|
219
|
+
document.body.appendChild(wrap);
|
|
220
|
+
await flushGetFrames();
|
|
221
|
+
|
|
222
|
+
expect(received).toHaveLength(1);
|
|
223
|
+
expect(received[0]?.result).toEqual({ id: "error" });
|
|
224
|
+
expect(received[0]?.response?.status).toBe(422);
|
|
225
|
+
expect(received[0]?.request.url).toContain("api.example.test");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("transmet undefined à une méthode @get quand aucun payload n'est produit", async () => {
|
|
229
|
+
vi.mocked(API.prototype.getDetailed).mockResolvedValueOnce(undefined);
|
|
230
|
+
const itemEp = new Endpoint<{ id: string }>("items/handler-error");
|
|
231
|
+
const received: Array<ApiResult<{ id: string }> | undefined> = [];
|
|
232
|
+
const tag = "test-api-get-handler-undefined";
|
|
233
|
+
|
|
234
|
+
class C extends HTMLElement {
|
|
235
|
+
@get(itemEp)
|
|
236
|
+
onPayload(payload: ApiResult<{ id: string }> | undefined) {
|
|
237
|
+
received.push(payload);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (!customElements.get(tag)) {
|
|
241
|
+
customElements.define(tag, C);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const wrap = document.createElement("div");
|
|
245
|
+
wrap.setAttribute("serviceURL", "https://api.example.test");
|
|
246
|
+
wrap.appendChild(document.createElement(tag));
|
|
247
|
+
document.body.appendChild(wrap);
|
|
248
|
+
await flushGetFrames();
|
|
249
|
+
|
|
250
|
+
expect(received).toEqual([undefined]);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("permet d'appliquer un handle get directement à une méthode", async () => {
|
|
254
|
+
const itemEp = new Endpoint<{ id: string }>("items/method");
|
|
255
|
+
const localGet = get(itemEp);
|
|
256
|
+
const received: Array<ApiResult<{ id: string }> | undefined> = [];
|
|
257
|
+
const tag = "test-api-get-method-handler";
|
|
258
|
+
|
|
259
|
+
class C extends HTMLElement {
|
|
260
|
+
@localGet
|
|
261
|
+
onPayload(payload: ApiResult<{ id: string }> | undefined) {
|
|
262
|
+
received.push(payload);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (!customElements.get(tag)) {
|
|
266
|
+
customElements.define(tag, C);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const wrap = document.createElement("div");
|
|
270
|
+
wrap.setAttribute("serviceURL", "https://api.example.test");
|
|
271
|
+
const el = document.createElement(tag) as InstanceType<typeof C>;
|
|
272
|
+
wrap.appendChild(el);
|
|
273
|
+
document.body.appendChild(wrap);
|
|
274
|
+
await flushGetFrames();
|
|
275
|
+
|
|
276
|
+
expect(received).toHaveLength(1);
|
|
277
|
+
expect(received[0]?.result).toEqual({ id: "loaded" });
|
|
278
|
+
|
|
279
|
+
await localGet.refetch(el);
|
|
280
|
+
expect(received).toHaveLength(2);
|
|
281
|
+
});
|
|
282
|
+
|
|
193
283
|
it("expose un refetch local avant et après le premier payload", async () => {
|
|
194
284
|
const pubId = "getSpecLocalRefetch";
|
|
195
285
|
PublisherManager.get(pubId).set(staticConfig);
|