@supersoniks/concorde 4.9.93 → 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/build-infos.json +1 -1
- package/concorde-core.bundle.js +299 -292
- package/concorde-core.es.js +10104 -6071
- package/dist/concorde-core.bundle.js +299 -292
- package/dist/concorde-core.es.js +10104 -6071
- 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/subscriber/derive.js +60 -0
- package/dist/js/core/decorators/subscriber/derive.js.map +1 -0
- 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/subscriber/derive.d.ts +30 -0
- package/dist/types/core/decorators/subscriber/derive.d.ts.map +1 -0
- 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/subscriber/derive.ts +95 -0
- 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/_misc/docs-mock-api.md +3 -0
- package/src/docs/components/docs-demo-sources.ts +26 -0
- package/src/docs/docs.ts +1 -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 +180 -0
- package/src/tsconfig.json +18 -0
- package/src/tsconfig.tsbuildinfo +1 -1
- 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 |
|
|
@@ -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) |
|
|
@@ -81,6 +81,24 @@ export const DOCS_LIT_DEMO_REGISTRY: Record<string, DocsLitDemoConfig> = {
|
|
|
81
81
|
{ path: "src/docs/example/docs-users-list.ts", line: 15, label: "docs-users-list" },
|
|
82
82
|
],
|
|
83
83
|
},
|
|
84
|
+
"docs-jsonata-space-cats-demo": {
|
|
85
|
+
sources: [
|
|
86
|
+
{
|
|
87
|
+
path: "src/docs/example/docs-jsonata-demos.ts",
|
|
88
|
+
line: 26,
|
|
89
|
+
label: "top-n",
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
"docs-jsonata-table-totals-demo": {
|
|
94
|
+
sources: [
|
|
95
|
+
{
|
|
96
|
+
path: "src/docs/example/docs-jsonata-demos.ts",
|
|
97
|
+
line: 69,
|
|
98
|
+
label: "totals",
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
},
|
|
84
102
|
"docs-list-alternate-demo": {
|
|
85
103
|
sources: [
|
|
86
104
|
{ path: "src/docs/example/docs-list-demos.ts", line: 51, label: "alternate" },
|
|
@@ -204,6 +222,14 @@ export const DOCS_DEMO_SOURCE_REGISTRY: Record<string, DocsSource[]> = {
|
|
|
204
222
|
"docs-joke-search-demo": [
|
|
205
223
|
{ path: "src/docs/example/docs-joke-demos.ts", label: "joke demos" },
|
|
206
224
|
],
|
|
225
|
+
"docs-jsonata-space-cats-demo": [
|
|
226
|
+
{ path: "src/docs/example/docs-jsonata-demos.ts", label: "jsonata demos" },
|
|
227
|
+
{ path: "src/docs/mock-api/fixtures-jsonata.ts", label: "fixtures" },
|
|
228
|
+
],
|
|
229
|
+
"docs-jsonata-table-totals-demo": [
|
|
230
|
+
{ path: "src/docs/example/docs-jsonata-demos.ts", label: "jsonata demos" },
|
|
231
|
+
{ path: "src/docs/mock-api/fixtures-jsonata.ts", label: "fixtures" },
|
|
232
|
+
],
|
|
207
233
|
"docs-joke-lang-demo": [
|
|
208
234
|
{ path: "src/docs/example/docs-joke-demos.ts", label: "joke demos" },
|
|
209
235
|
],
|
package/src/docs/docs.ts
CHANGED
|
@@ -15,6 +15,7 @@ import "./example/docs-joke-demos";
|
|
|
15
15
|
import "./example/docs-queue-demos";
|
|
16
16
|
import "./example/docs-toggle-demos";
|
|
17
17
|
import "./example/docs-list-demos";
|
|
18
|
+
import "./example/docs-jsonata-demos";
|
|
18
19
|
import "./example/docs-router-demos";
|
|
19
20
|
import "./example/docs-submit-demos";
|
|
20
21
|
import "./example/docs-api-config-demos";
|