@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.
Files changed (111) hide show
  1. package/ai/cursor/rules/concorde.mdc +7 -0
  2. package/ai/jetbrains/rules/concorde.md +6 -0
  3. package/ai/skills/concorde/SKILL.md +48 -0
  4. package/build-infos.json +1 -1
  5. package/concorde-core.bundle.js +299 -292
  6. package/concorde-core.es.js +10139 -6035
  7. package/dist/concorde-core.bundle.js +299 -292
  8. package/dist/concorde-core.es.js +10139 -6035
  9. package/dist/docs-mock-api-sw.js +110 -0
  10. package/dist/docs-mock-api-sw.js.map +3 -3
  11. package/dist/js/core/components/functional/functional.js +1 -0
  12. package/dist/js/core/components/functional/functional.js.map +1 -1
  13. package/dist/js/core/components/functional/jsonata/jsonata.js +119 -0
  14. package/dist/js/core/components/functional/jsonata/jsonata.js.map +1 -0
  15. package/dist/js/core/components/ui/button/button.js +1 -1
  16. package/dist/js/core/components/ui/button/button.js.map +1 -1
  17. package/dist/js/core/decorators/Subscriber.js +1 -0
  18. package/dist/js/core/decorators/Subscriber.js.map +1 -1
  19. package/dist/js/core/decorators/api.js +108 -16
  20. package/dist/js/core/decorators/api.js.map +1 -1
  21. package/dist/js/core/decorators/subscriber/derive.js +60 -0
  22. package/dist/js/core/decorators/subscriber/derive.js.map +1 -0
  23. package/dist/js/core/utils/api.js +29 -8
  24. package/dist/js/core/utils/api.js.map +1 -1
  25. package/dist/js/core/utils/derive.js +246 -0
  26. package/dist/js/core/utils/derive.js.map +1 -0
  27. package/dist/js/core/utils/jsonataHelpers.js +69 -0
  28. package/dist/js/core/utils/jsonataHelpers.js.map +1 -0
  29. package/dist/js/core/utils/jsonataRuntime.js +75 -0
  30. package/dist/js/core/utils/jsonataRuntime.js.map +1 -0
  31. package/dist/js/utils.js +2 -0
  32. package/dist/js/utils.js.map +1 -1
  33. package/dist/types/core/components/functional/functional.d.ts +1 -0
  34. package/dist/types/core/components/functional/functional.d.ts.map +1 -1
  35. package/dist/types/core/components/functional/jsonata/jsonata.d.ts +29 -0
  36. package/dist/types/core/components/functional/jsonata/jsonata.d.ts.map +1 -0
  37. package/dist/types/core/components/ui/button/button.d.ts +1 -1
  38. package/dist/types/core/components/ui/button/button.d.ts.map +1 -1
  39. package/dist/types/core/components/ui/form/checkbox/checkbox.d.ts +3 -3
  40. package/dist/types/core/components/ui/form/input/input.d.ts +3 -3
  41. package/dist/types/core/components/ui/form/input-autocomplete/input-autocomplete.d.ts +3 -3
  42. package/dist/types/core/components/ui/form/textarea/textarea.d.ts +3 -3
  43. package/dist/types/core/decorators/Subscriber.d.ts +2 -0
  44. package/dist/types/core/decorators/Subscriber.d.ts.map +1 -1
  45. package/dist/types/core/decorators/api.d.ts +21 -12
  46. package/dist/types/core/decorators/api.d.ts.map +1 -1
  47. package/dist/types/core/decorators/subscriber/derive.d.ts +30 -0
  48. package/dist/types/core/decorators/subscriber/derive.d.ts.map +1 -0
  49. package/dist/types/core/utils/api.d.ts +1 -1
  50. package/dist/types/core/utils/api.d.ts.map +1 -1
  51. package/dist/types/core/utils/derive.d.ts +40 -0
  52. package/dist/types/core/utils/derive.d.ts.map +1 -0
  53. package/dist/types/core/utils/jsonataHelpers.d.ts +15 -0
  54. package/dist/types/core/utils/jsonataHelpers.d.ts.map +1 -0
  55. package/dist/types/core/utils/jsonataRuntime.d.ts +13 -0
  56. package/dist/types/core/utils/jsonataRuntime.d.ts.map +1 -0
  57. package/dist/types/mixins.d.ts +3 -3
  58. package/dist/types/utils.d.ts +2 -0
  59. package/dist/types/utils.d.ts.map +1 -1
  60. package/docs/assets/{index-DURr6O71.js → index-DpIkksht.js} +652 -479
  61. package/docs/crawl/docs/_decorators/get.html +27 -6
  62. package/docs/crawl/docs/_decorators/patch.html +6 -0
  63. package/docs/crawl/docs/_decorators/post.html +8 -0
  64. package/docs/crawl/docs/_decorators/put.html +6 -0
  65. package/docs/crawl/hubs/decorators.html +36 -31
  66. package/docs/crawl/hubs/remote-data.html +36 -31
  67. package/docs/index.html +1 -1
  68. package/docs/llms-full.txt +47 -3
  69. package/docs/src/docs/_decorators/get.md +31 -0
  70. package/docs/src/docs/_decorators/patch.md +9 -0
  71. package/docs/src/docs/_decorators/post.md +11 -0
  72. package/docs/src/docs/_decorators/put.md +9 -0
  73. package/docs/src/docs/search/docs-search.json +120 -5
  74. package/docs/src/tsconfig.json +268 -268
  75. package/package.json +28 -3
  76. package/public/docs-mock-api-sw.js +110 -0
  77. package/public/docs-mock-api-sw.js.map +3 -3
  78. package/src/core/components/functional/functional.ts +1 -0
  79. package/src/core/components/functional/jsonata/jsonata.md +56 -0
  80. package/src/core/components/functional/jsonata/jsonata.spec.ts +202 -0
  81. package/src/core/components/functional/jsonata/jsonata.ts +116 -0
  82. package/src/core/components/ui/button/button.ts +2 -2
  83. package/src/core/decorators/Subscriber.ts +2 -0
  84. package/src/core/decorators/api.post.spec.ts +99 -4
  85. package/src/core/decorators/api.spec.ts +90 -0
  86. package/src/core/decorators/api.ts +212 -92
  87. package/src/core/decorators/subscriber/derive.ts +95 -0
  88. package/src/core/utils/api.spec.ts +73 -0
  89. package/src/core/utils/api.ts +44 -11
  90. package/src/core/utils/derive.spec.ts +190 -0
  91. package/src/core/utils/derive.ts +357 -0
  92. package/src/core/utils/jsonataHelpers.ts +100 -0
  93. package/src/core/utils/jsonataRuntime.ts +88 -0
  94. package/src/docs/_core-concept/dataFlow.md +9 -0
  95. package/src/docs/_decorators/get.md +31 -0
  96. package/src/docs/_decorators/patch.md +9 -0
  97. package/src/docs/_decorators/post.md +11 -0
  98. package/src/docs/_decorators/put.md +9 -0
  99. package/src/docs/_misc/docs-mock-api.md +3 -0
  100. package/src/docs/components/docs-demo-sources.ts +40 -0
  101. package/src/docs/docs.ts +1 -0
  102. package/src/docs/example/decorators-demo-post.ts +70 -0
  103. package/src/docs/example/decorators-demo-subscribe-publish-get-demos.ts +59 -0
  104. package/src/docs/example/docs-jsonata-demos.ts +128 -0
  105. package/src/docs/mock-api/fixtures-jsonata.ts +131 -0
  106. package/src/docs/mock-api/router.ts +26 -0
  107. package/src/docs/navigation/navigation.ts +4 -0
  108. package/src/docs/search/docs-search.json +300 -5
  109. package/src/tsconfig.json +18 -0
  110. package/src/tsconfig.tsbuildinfo +1 -1
  111. package/src/utils.ts +13 -0
@@ -0,0 +1,357 @@
1
+ import {
2
+ DataProviderKey,
3
+ resolveStaticPublisherPath,
4
+ type PublisherPathInput,
5
+ } from "@supersoniks/concorde/core/utils/dataProviderKey";
6
+ import {
7
+ PublisherManager,
8
+ type DataProvider,
9
+ } from "@supersoniks/concorde/core/utils/PublisherProxy";
10
+ import Objects from "@supersoniks/concorde/core/utils/Objects";
11
+
12
+ export type DeriveStatus = "loading" | "ready" | "error" | "empty";
13
+
14
+ export type DeriveState = {
15
+ status: DeriveStatus;
16
+ error?: string | null;
17
+ };
18
+
19
+ export type DeriveCompute<I extends Record<string, unknown>, O, V> = (
20
+ inputs: I,
21
+ vars: V,
22
+ ) => O | Promise<O>;
23
+
24
+ export type DeriveOptions<
25
+ I extends Record<string, unknown>,
26
+ O,
27
+ V = Record<string, unknown>,
28
+ > = {
29
+ inputs: { [K in keyof I]: DataProviderKey<I[K]> | string };
30
+ compute: DeriveCompute<I, O, V>;
31
+ output: DataProviderKey<O> | string;
32
+ /** formDataProvider (ou autre DP) servant de variables `$top`, `$season`, … */
33
+ variables?: DataProviderKey<V> | string;
34
+ debounceMs?: number;
35
+ isEmpty?: (value: O) => boolean;
36
+ /** Taille max d’un tableau d’entrée (warn au-delà). Défaut 10_000. */
37
+ maxInputItems?: number;
38
+ };
39
+
40
+ export type DeriveHandle = {
41
+ stop: () => void;
42
+ /** Force un recalcul immédiat (sans debounce). */
43
+ recompute: () => void;
44
+ };
45
+
46
+ const DEFAULT_DEBOUNCE_MS = 50;
47
+ const DEFAULT_MAX_INPUT_ITEMS = 10_000;
48
+ const SLOW_EVAL_MS = 100;
49
+
50
+ /** Graphe derivation : outputPath → set(inputRootIds) pour détection de cycles. */
51
+ const deriveEdges = new Map<string, Set<string>>();
52
+
53
+ function pathOf(input: PublisherPathInput): string {
54
+ return resolveStaticPublisherPath(input);
55
+ }
56
+
57
+ function rootOf(path: string): string {
58
+ return path.split(".")[0] ?? path;
59
+ }
60
+
61
+ function resolvePublisher(path: string): DataProvider {
62
+ const parts = path.split(".");
63
+ const root = parts.shift()!;
64
+ let pub: DataProvider = PublisherManager.get(root);
65
+ if (parts.length) {
66
+ pub = Objects.traverse(pub, parts) as DataProvider;
67
+ }
68
+ return pub;
69
+ }
70
+
71
+ function defaultIsEmpty(value: unknown): boolean {
72
+ if (value == null) return true;
73
+ if (Array.isArray(value)) return value.length === 0;
74
+ if (typeof value === "object") return Object.keys(value as object).length === 0;
75
+ if (typeof value === "string") return value.length === 0;
76
+ return false;
77
+ }
78
+
79
+ function countItems(value: unknown): number {
80
+ if (Array.isArray(value)) return value.length;
81
+ return 0;
82
+ }
83
+
84
+ /**
85
+ * Détecte un cycle si `output` chevauche une entrée, ou si une chaîne de
86
+ * dérivations (output → inputs) revient sur elle-même.
87
+ */
88
+ function assertNoCycle(outputPath: string, inputPaths: string[]): void {
89
+ for (const inputPath of inputPaths) {
90
+ if (
91
+ inputPath === outputPath ||
92
+ outputPath.startsWith(inputPath + ".") ||
93
+ inputPath.startsWith(outputPath + ".")
94
+ ) {
95
+ throw new Error(
96
+ `[derive] Cycle détecté : la sortie "${outputPath}" chevauche l'entrée "${inputPath}".`,
97
+ );
98
+ }
99
+ }
100
+
101
+ const outputRoot = rootOf(outputPath);
102
+ const deps = new Map<string, Set<string>>();
103
+ for (const [out, inputs] of deriveEdges) {
104
+ deps.set(rootOf(out), new Set(inputs));
105
+ }
106
+ deps.set(outputRoot, new Set(inputPaths.map(rootOf)));
107
+
108
+ const stack = new Set<string>();
109
+ const visit = (node: string): boolean => {
110
+ if (stack.has(node)) return true;
111
+ stack.add(node);
112
+ for (const d of deps.get(node) ?? []) {
113
+ if (visit(d)) return true;
114
+ }
115
+ stack.delete(node);
116
+ return false;
117
+ };
118
+
119
+ if (visit(outputRoot)) {
120
+ throw new Error(
121
+ `[derive] Cycle détecté (chaîne) : la sortie "${outputPath}" dépend indirectement d'elle-même.`,
122
+ );
123
+ }
124
+ }
125
+
126
+ function registerEdges(outputPath: string, inputPaths: string[]): void {
127
+ deriveEdges.set(
128
+ outputPath,
129
+ new Set(inputPaths.map((p) => rootOf(p))),
130
+ );
131
+ }
132
+
133
+ function unregisterEdges(outputPath: string): void {
134
+ deriveEdges.delete(outputPath);
135
+ }
136
+
137
+ /**
138
+ * État de dérivation publié sur `{outputRoot}/_state_` pour `sonic-states`.
139
+ */
140
+ export function deriveStateKey(output: PublisherPathInput): string {
141
+ return `${rootOf(pathOf(output))}/_state_`;
142
+ }
143
+
144
+ function publishState(outputPath: string, state: DeriveState): void {
145
+ const statePath = deriveStateKey(outputPath);
146
+ PublisherManager.get(statePath).set(state);
147
+ }
148
+
149
+ /**
150
+ * Primitive de données dérivées en lecture seule.
151
+ *
152
+ * Observe les DataProviders d’entrée (+ variables optionnelles), recalcule avec
153
+ * debounce, annule les résultats périmés (génération), publie sur `output`.
154
+ */
155
+ export function derive<
156
+ I extends Record<string, unknown>,
157
+ O,
158
+ V = Record<string, unknown>,
159
+ >(options: DeriveOptions<I, O, V>): DeriveHandle;
160
+ export function derive<
161
+ I extends Record<string, unknown>,
162
+ O,
163
+ V = Record<string, unknown>,
164
+ >(
165
+ inputs: { [K in keyof I]: DataProviderKey<I[K]> | string },
166
+ compute: DeriveCompute<I, O, V>,
167
+ output: DataProviderKey<O> | string,
168
+ variables?: DataProviderKey<V> | string,
169
+ ): DeriveHandle;
170
+ export function derive<
171
+ I extends Record<string, unknown>,
172
+ O,
173
+ V = Record<string, unknown>,
174
+ >(
175
+ inputsOrOptions:
176
+ | DeriveOptions<I, O, V>
177
+ | { [K in keyof I]: DataProviderKey<I[K]> | string },
178
+ compute?: DeriveCompute<I, O, V>,
179
+ output?: DataProviderKey<O> | string,
180
+ variables?: DataProviderKey<V> | string,
181
+ ): DeriveHandle {
182
+ const opts: DeriveOptions<I, O, V> =
183
+ typeof (inputsOrOptions as DeriveOptions<I, O, V>).compute === "function"
184
+ ? (inputsOrOptions as DeriveOptions<I, O, V>)
185
+ : {
186
+ inputs: inputsOrOptions as {
187
+ [K in keyof I]: DataProviderKey<I[K]> | string;
188
+ },
189
+ compute: compute!,
190
+ output: output!,
191
+ variables,
192
+ };
193
+
194
+ const debounceMs = opts.debounceMs ?? DEFAULT_DEBOUNCE_MS;
195
+ const maxItems = opts.maxInputItems ?? DEFAULT_MAX_INPUT_ITEMS;
196
+ const isEmpty = opts.isEmpty ?? defaultIsEmpty;
197
+
198
+ const outputPath = pathOf(opts.output);
199
+ const inputEntries = Object.entries(opts.inputs).map(([alias, key]) => ({
200
+ alias,
201
+ path: pathOf(key as PublisherPathInput),
202
+ }));
203
+ const inputPaths = inputEntries.map((e) => e.path);
204
+ const variablesPath = opts.variables
205
+ ? pathOf(opts.variables as PublisherPathInput)
206
+ : null;
207
+
208
+ assertNoCycle(outputPath, inputPaths);
209
+ registerEdges(outputPath, inputPaths);
210
+
211
+ const outputPub = resolvePublisher(outputPath);
212
+ let lastValue: O | undefined;
213
+ let writing = false;
214
+ let generation = 0;
215
+ let timer: ReturnType<typeof setTimeout> | null = null;
216
+ let stopped = false;
217
+ let lastMemoKey = "";
218
+
219
+ const setOutput = (value: O) => {
220
+ writing = true;
221
+ lastValue = value;
222
+ outputPub.set(value as never);
223
+ writing = false;
224
+ };
225
+
226
+ const onExternalWrite = () => {
227
+ if (writing || stopped) return;
228
+ if (typeof console !== "undefined" && console.warn) {
229
+ console.warn(
230
+ `[derive] Écriture ignorée sur la sortie readonly "${outputPath}".`,
231
+ );
232
+ }
233
+ if (lastValue !== undefined) {
234
+ writing = true;
235
+ outputPub.set(lastValue as never);
236
+ writing = false;
237
+ }
238
+ };
239
+ outputPub.onAssign(onExternalWrite, false);
240
+
241
+ const collectInputs = (): I => {
242
+ const result = {} as I;
243
+ for (const { alias, path } of inputEntries) {
244
+ const value = resolvePublisher(path).get();
245
+ const n = countItems(value);
246
+ if (n > maxItems && typeof console !== "undefined" && console.warn) {
247
+ console.warn(
248
+ `[derive] Entrée "${path}" : ${n} éléments (> ${maxItems}). Agrégation lourde — envisager un plafond côté fetch.`,
249
+ );
250
+ }
251
+ (result as Record<string, unknown>)[alias] = value;
252
+ }
253
+ return result;
254
+ };
255
+
256
+ const collectVars = (): V => {
257
+ if (!variablesPath) return {} as V;
258
+ return (resolvePublisher(variablesPath).get() ?? {}) as V;
259
+ };
260
+
261
+ const run = async () => {
262
+ if (stopped) return;
263
+ const gen = ++generation;
264
+ publishState(outputPath, { status: "loading", error: null });
265
+
266
+ const inputs = collectInputs();
267
+ const vars = collectVars();
268
+ const memoKey = JSON.stringify({ inputs, vars });
269
+ if (memoKey === lastMemoKey && lastValue !== undefined) {
270
+ publishState(outputPath, {
271
+ status: isEmpty(lastValue) ? "empty" : "ready",
272
+ error: null,
273
+ });
274
+ return;
275
+ }
276
+
277
+ const t0 =
278
+ typeof performance !== "undefined" ? performance.now() : Date.now();
279
+ try {
280
+ const result = await Promise.resolve(opts.compute(inputs, vars));
281
+ if (stopped || gen !== generation) return;
282
+
283
+ const elapsed =
284
+ (typeof performance !== "undefined" ? performance.now() : Date.now()) -
285
+ t0;
286
+ if (elapsed > SLOW_EVAL_MS && typeof console !== "undefined" && console.warn) {
287
+ console.warn(
288
+ `[derive] Évaluation longue (${Math.round(elapsed)} ms) → "${outputPath}".`,
289
+ );
290
+ }
291
+
292
+ lastMemoKey = memoKey;
293
+ setOutput(result);
294
+ publishState(outputPath, {
295
+ status: isEmpty(result) ? "empty" : "ready",
296
+ error: null,
297
+ });
298
+ } catch (err) {
299
+ if (stopped || gen !== generation) return;
300
+ const message = err instanceof Error ? err.message : String(err);
301
+ if (typeof console !== "undefined" && console.error) {
302
+ console.error(`[derive] Erreur sur "${outputPath}" :`, message);
303
+ }
304
+ publishState(outputPath, { status: "error", error: message });
305
+ }
306
+ };
307
+
308
+ const schedule = () => {
309
+ if (stopped) return;
310
+ if (timer) clearTimeout(timer);
311
+ timer = setTimeout(() => {
312
+ timer = null;
313
+ void run();
314
+ }, debounceMs);
315
+ };
316
+
317
+ const listeners: Array<{
318
+ pub: DataProvider;
319
+ handler: () => void;
320
+ }> = [];
321
+
322
+ /**
323
+ * Écoute les mutations profondes : les champs form (`name="top"`) écrivent sur
324
+ * un sous-proxy (`form.top.set`), qui ne déclenche pas `onAssign` sur la racine.
325
+ * `onInternalMutation` remonte bien parent ← enfant.
326
+ */
327
+ const watch = (path: string) => {
328
+ const pub = resolvePublisher(path);
329
+ const handler = () => schedule();
330
+ pub.onInternalMutation(handler);
331
+ listeners.push({ pub, handler });
332
+ };
333
+
334
+ for (const { path } of inputEntries) watch(path);
335
+ if (variablesPath) watch(variablesPath);
336
+
337
+ // Premier calcul (onInternalMutation a déjà appelé schedule à l’abonnement,
338
+ // mais on force un passage après init complète).
339
+ schedule();
340
+
341
+ return {
342
+ stop() {
343
+ if (stopped) return;
344
+ stopped = true;
345
+ if (timer) clearTimeout(timer);
346
+ for (const { pub, handler } of listeners) {
347
+ pub.offInternalMutation(handler);
348
+ }
349
+ outputPub.offAssign(onExternalWrite);
350
+ unregisterEdges(outputPath);
351
+ },
352
+ recompute() {
353
+ lastMemoKey = "";
354
+ void run();
355
+ },
356
+ };
357
+ }
@@ -0,0 +1,100 @@
1
+ import type { Expression } from "jsonata";
2
+ import Format from "@supersoniks/concorde/core/utils/Format";
3
+
4
+ export type ChartDataset = {
5
+ label?: string;
6
+ data: number[];
7
+ };
8
+
9
+ export type ChartShape = {
10
+ labels: string[];
11
+ datasets: ChartDataset[];
12
+ };
13
+
14
+ /**
15
+ * Enregistre les helpers whitelistés sur une expression JSONata compilée.
16
+ * Pas de DOM, pas de réseau, pas d’eval.
17
+ */
18
+ export function registerJsonataHelpers(expression: Expression): void {
19
+ expression.registerFunction("ucFirst", (value: unknown) =>
20
+ Format.ucFirst(String(value ?? "")),
21
+ );
22
+
23
+ expression.registerFunction(
24
+ "currency",
25
+ (value: unknown, locale = "fr-FR", currency = "EUR") => {
26
+ const n = Number(value);
27
+ if (Number.isNaN(n)) return String(value ?? "");
28
+ return new Intl.NumberFormat(String(locale), {
29
+ style: "currency",
30
+ currency: String(currency),
31
+ }).format(n);
32
+ },
33
+ );
34
+
35
+ expression.registerFunction(
36
+ "percent",
37
+ (value: unknown, locale = "fr-FR", digits = 0) => {
38
+ const n = Number(value);
39
+ if (Number.isNaN(n)) return String(value ?? "");
40
+ return new Intl.NumberFormat(String(locale), {
41
+ style: "percent",
42
+ maximumFractionDigits: Number(digits) || 0,
43
+ }).format(n);
44
+ },
45
+ );
46
+
47
+ expression.registerFunction(
48
+ "date",
49
+ (value: unknown, locale = "fr-FR", optionsJson?: string) => {
50
+ const d = value instanceof Date ? value : new Date(String(value));
51
+ if (Number.isNaN(d.getTime())) return String(value ?? "");
52
+ let options: Intl.DateTimeFormatOptions = {
53
+ dateStyle: "medium",
54
+ };
55
+ if (optionsJson) {
56
+ try {
57
+ options = JSON.parse(
58
+ String(optionsJson),
59
+ ) as Intl.DateTimeFormatOptions;
60
+ } catch {
61
+ /* keep default */
62
+ }
63
+ }
64
+ return new Intl.DateTimeFormat(String(locale), options).format(d);
65
+ },
66
+ );
67
+
68
+ /**
69
+ * Convertit un tableau d’objets `{ label, value }` (ou clés custom) en forme chart.
70
+ * Usage : `$toChart(items, 'titre', 'ca')`
71
+ */
72
+ expression.registerFunction(
73
+ "toChart",
74
+ (
75
+ items: unknown,
76
+ labelKey = "label",
77
+ valueKey = "value",
78
+ datasetLabel = "",
79
+ ): ChartShape => {
80
+ const rows = Array.isArray(items) ? items : [];
81
+ const labels: string[] = [];
82
+ const data: number[] = [];
83
+ for (const row of rows) {
84
+ if (!row || typeof row !== "object") continue;
85
+ const rec = row as Record<string, unknown>;
86
+ labels.push(String(rec[String(labelKey)] ?? ""));
87
+ data.push(Number(rec[String(valueKey)] ?? 0));
88
+ }
89
+ return {
90
+ labels,
91
+ datasets: [
92
+ {
93
+ label: String(datasetLabel || ""),
94
+ data,
95
+ },
96
+ ],
97
+ };
98
+ },
99
+ );
100
+ }
@@ -0,0 +1,88 @@
1
+ import jsonata, { type Expression } from "jsonata";
2
+ import { registerJsonataHelpers } from "@supersoniks/concorde/core/utils/jsonataHelpers";
3
+
4
+ const expressionCache = new Map<string, Expression>();
5
+
6
+ const MAX_EXPRESSION_LENGTH = 8_000;
7
+
8
+ /**
9
+ * Compile (avec cache) une expression JSONata + helpers Concorde.
10
+ */
11
+ export function compileJsonata(source: string): Expression {
12
+ if (!source || !source.trim()) {
13
+ throw new Error("[sonic-jsonata] expression vide.");
14
+ }
15
+ if (source.length > MAX_EXPRESSION_LENGTH) {
16
+ throw new Error(
17
+ `[sonic-jsonata] expression trop longue (${source.length} > ${MAX_EXPRESSION_LENGTH}).`,
18
+ );
19
+ }
20
+ const cached = expressionCache.get(source);
21
+ if (cached) return cached;
22
+ try {
23
+ const expr = jsonata(source);
24
+ registerJsonataHelpers(expr);
25
+ expressionCache.set(source, expr);
26
+ return expr;
27
+ } catch (err) {
28
+ const message =
29
+ err instanceof Error
30
+ ? err.message
31
+ : typeof err === "object" && err && "message" in err
32
+ ? String((err as { message: unknown }).message)
33
+ : String(err);
34
+ throw new Error(`[sonic-jsonata] Syntaxe JSONata invalide : ${message}`);
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Validation runtime légère (sous-ensemble JSON Schema).
40
+ * Vérifie `type` et `required` ; ignore le reste.
41
+ */
42
+ export function validateOutputSchema(
43
+ value: unknown,
44
+ schema: Record<string, unknown> | null | undefined,
45
+ ): void {
46
+ if (!schema || typeof schema !== "object") return;
47
+
48
+ const type = schema.type;
49
+ if (typeof type === "string") {
50
+ const ok =
51
+ (type === "array" && Array.isArray(value)) ||
52
+ (type === "object" &&
53
+ value !== null &&
54
+ typeof value === "object" &&
55
+ !Array.isArray(value)) ||
56
+ (type === "string" && typeof value === "string") ||
57
+ (type === "number" && typeof value === "number") ||
58
+ (type === "boolean" && typeof value === "boolean") ||
59
+ (type === "null" && value === null);
60
+ if (!ok) {
61
+ throw new Error(
62
+ `[sonic-jsonata] outputSchema : type attendu "${type}", reçu ${describeType(value)}.`,
63
+ );
64
+ }
65
+ }
66
+
67
+ if (Array.isArray(schema.required) && value && typeof value === "object") {
68
+ const obj = value as Record<string, unknown>;
69
+ for (const key of schema.required) {
70
+ if (typeof key === "string" && !(key in obj)) {
71
+ throw new Error(
72
+ `[sonic-jsonata] outputSchema : propriété requise manquante "${key}".`,
73
+ );
74
+ }
75
+ }
76
+ }
77
+ }
78
+
79
+ function describeType(value: unknown): string {
80
+ if (value === null) return "null";
81
+ if (Array.isArray(value)) return "array";
82
+ return typeof value;
83
+ }
84
+
85
+ /** Vide le cache (tests). */
86
+ export function clearJsonataCache(): void {
87
+ expressionCache.clear();
88
+ }
@@ -16,6 +16,7 @@ Recommended patterns for new Concorde apps (Lit + TypeScript). Under the hood, d
16
16
  | HTTP POST (body from store) | `@post` + `Endpoint` + body `DataProviderKey` |
17
17
  | HTTP PUT / PATCH (body from store) | `@put` / `@patch` — same model as `@post` |
18
18
  | Forms | `formDataProvider` + `name` on fields |
19
+ | Derived / aggregate (JSONata) | `derive()` or `sonic-jsonata` — [Jsonata](#core/components/functional/jsonata/jsonata.md/jsonata) |
19
20
  | Offline doc demos | `serviceURL="/docs-mock-api"` — [Local API demos](#docs/_misc/docs-mock-api.md/docs-mock-api) |
20
21
 
21
22
  Skill: `concorde-get-set-dp` in the package `ai/` folder.
@@ -69,6 +70,14 @@ npx @supersoniks/create-concorde-ts-starter my-app
69
70
 
70
71
  Interactive routes mirror these patterns (`/concepts/*`, `/demo/*`).
71
72
 
73
+ ## Derived data (`derive` / `sonic-jsonata`)
74
+
75
+ Use when you need **client-side aggregations** (groupby, top N, chart shapes) from existing DataProviders — without inline app JS in SDUI.
76
+
77
+ - Primitive: `derive({ inputs, compute, output, variables? })` from `@supersoniks/concorde/utils` (read-only output, debounce, generation, cycle detection, `{output}/_state_`).
78
+ - SDUI adapter: [`sonic-jsonata`](#core/components/functional/jsonata/jsonata.md/jsonata) (`inputProvider` → `expression` → `outputProvider`, optional `formDataProvider`).
79
+ - vs [`sonic-mix`](#core/components/functional/mix/mix.md/mix): mix composes bidirectionally; jsonata **computes** unidirectionally.
80
+
72
81
  ## Legacy integration
73
82
 
74
83
  | Topic | Page |
@@ -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
@@ -46,6 +59,22 @@ async refresh() {
46
59
  `refetch(component)` returns a `Promise<ApiResult<T> | undefined>` that resolves
47
60
  after the GET has completed.
48
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.
75
+ }
76
+ ```
77
+
49
78
  ### From a method decorator
50
79
 
51
80
  The refetch can decorate a local method. The default is `after`; if the
@@ -116,6 +145,8 @@ import { Endpoint } from "@supersoniks/concorde/utils/endpoint";
116
145
  <template>
117
146
  <docs-demo-sources for="demo-api-get"></docs-demo-sources>
118
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>
119
150
  <docs-demo-sources for="demo-api-get-configuration-key"></docs-demo-sources>
120
151
  <demo-api-get-configuration-key></demo-api-get-configuration-key>
121
152
  </template>
@@ -16,6 +16,15 @@ 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
@@ -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.
@@ -120,6 +129,8 @@ import { Endpoint } from "@supersoniks/concorde/utils/endpoint";
120
129
  <template>
121
130
  <docs-demo-sources for="demo-api-post"></docs-demo-sources>
122
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>
123
134
  <docs-demo-sources for="demo-api-post-dynamic"></docs-demo-sources>
124
135
  <demo-api-post-dynamic></demo-api-post-dynamic>
125
136
  </template>
@@ -16,6 +16,15 @@ 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
@@ -16,6 +16,9 @@ TypeScript constants: `src/docs/mock-api/urls.ts` (`DOCS_MOCK_REQRES_SERVICE`, `
16
16
  |------|---------|
17
17
  | `/docs-mock-api/api/users` | `sonic-list`, `sonic-fetch`, `sonic-queue` (users) |
18
18
  | `/docs-mock-api/api/users/:id` | Single user, `docs-user` |
19
+ | `/docs-mock-api/api/space-cats` | `sonic-jsonata` top-N demo |
20
+ | `/docs-mock-api/api/coffee-bags` | fixtures / tests jsonata (`?roast=`) |
21
+ | `/docs-mock-api/api/market-order` | `sonic-jsonata` totals demo |
19
22
  | `POST /docs-mock-api/api/register` | **sonic-submit** — JSON, `multipart/form-data` (`sendAsFormData`), or `application/x-www-form-urlencoded` (native `<form>`); response includes parsed `email` + `token` |
20
23
  | `POST /docs-mock-api/api/register/nested` | Wrapped body `{ data: { id, token } }` for `submit-result-key` demo |
21
24
  | `GET /docs-mock-api/api/register/echo` | Echoes query string (`method="get"` on submit) |