@solumflow-app/crm-mcp-server 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1872 -16
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -47,7 +47,7 @@ ${snippet}
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
// src/catalog-tools.ts
|
|
50
|
-
var CONTENT_BLOCKS = 'Besides plain values (text, number, choice), a product can carry content blocks, three field types: "content_text" is { html, links? } with already-sanitised html; "cta" is { heading, body, primary: {label,url}, secondary? }, a call to action; "repeater" is { items: [{ id, ...one value per item field }] } and covers lists, accordions (FAQ), downloads and testimonials. A repeater\u2019s field says how to draw it: "display" is "list" or "accordion", "preset" names where it started (list, faq, downloads, testimonials), "itemFields" is the structure of one item (key, label, type: text, richtext, url {label,url}, icon {set,name}, image or file {url,name,size,contentType}) and "titleKey" is the item field that names an item, such as the question of a FAQ. A block that is public but empty is null; a block that is not public is not there at all.';
|
|
50
|
+
var CONTENT_BLOCKS = 'Besides plain values (text, number, choice), a product can carry content blocks, three field types: "content_text" is { html, links? } with already-sanitised html; "cta" is { heading, body, primary: {label,url}, secondary? }, a call to action; "repeater" is { items: [{ id, ...one value per item field }] } and covers lists, accordions (FAQ), downloads and testimonials. A repeater\u2019s field says how to draw it: "display" is "list" or "accordion", "preset" names where it started (list, faq, downloads, testimonials), "itemFields" is the structure of one item (key, label, type: text, richtext, url {label,url}, icon {set,name}, image or file {url,name,size,contentType}) and "titleKey" is the item field that names an item, such as the question of a FAQ. A "configurator" is a 3D product configurator: a listing carries only { configurator: true }, a product\u2019s own page the whole document (see read_configurator). A block that is public but empty is null; a block that is not public is not there at all.';
|
|
51
51
|
var slugOrId = z.string().min(1).max(200).describe("The address (slug) or the id of the product.");
|
|
52
52
|
function registerCatalogTools(server, client) {
|
|
53
53
|
server.registerTool(
|
|
@@ -105,14 +105,1869 @@ function describeField(field) {
|
|
|
105
105
|
return `- ${field.slug}: ${field.label}, repeater (${field.display ?? "list"}${preset}; items: ${items}${title})`;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
// src/
|
|
108
|
+
// src/configurator-tools.ts
|
|
109
|
+
import { z as z5 } from "zod";
|
|
110
|
+
|
|
111
|
+
// ../features/product-configurator/src/shared/configurator-limits.ts
|
|
112
|
+
var CONFIGURATOR_LIMITS = {
|
|
113
|
+
structureBytes: 64 * 1024,
|
|
114
|
+
valueBytes: 32 * 1024,
|
|
115
|
+
parameters: 60,
|
|
116
|
+
tabs: 12,
|
|
117
|
+
choiceLists: 40,
|
|
118
|
+
optionsPerList: 60,
|
|
119
|
+
materials: 120,
|
|
120
|
+
priceGroups: 20,
|
|
121
|
+
amounts: 20,
|
|
122
|
+
rules: 80,
|
|
123
|
+
priceRules: 20,
|
|
124
|
+
presets: 60,
|
|
125
|
+
formulaChars: 200,
|
|
126
|
+
formulaNodes: 64,
|
|
127
|
+
formulaDepth: 8,
|
|
128
|
+
geometryInputs: 60,
|
|
129
|
+
geometryConstants: 60,
|
|
130
|
+
geometrySlots: 20,
|
|
131
|
+
geometryNodes: 200,
|
|
132
|
+
expandedParts: 200,
|
|
133
|
+
repeatCount: 12,
|
|
134
|
+
repeatDepth: 2,
|
|
135
|
+
annotations: 30
|
|
136
|
+
};
|
|
137
|
+
function jsonByteLength(value) {
|
|
138
|
+
return new TextEncoder().encode(JSON.stringify(value)).length + jsonbSpacing(value);
|
|
139
|
+
}
|
|
140
|
+
function jsonbSpacing(value) {
|
|
141
|
+
if (Array.isArray(value)) {
|
|
142
|
+
return Math.max(0, value.length - 1) + value.reduce((sum, item) => sum + jsonbSpacing(item), 0);
|
|
143
|
+
}
|
|
144
|
+
if (value !== null && typeof value === "object") {
|
|
145
|
+
const entries = Object.values(value).filter(
|
|
146
|
+
(item) => item !== void 0
|
|
147
|
+
);
|
|
148
|
+
return entries.length + Math.max(0, entries.length - 1) + entries.reduce((sum, item) => sum + jsonbSpacing(item), 0);
|
|
149
|
+
}
|
|
150
|
+
return 0;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ../features/product-configurator/src/shared/configurator-issue.ts
|
|
154
|
+
function issue(path, code, params) {
|
|
155
|
+
return params ? { path, code, params } : { path, code };
|
|
156
|
+
}
|
|
157
|
+
function formatIssuePath(path) {
|
|
158
|
+
return path.reduce((text, part) => {
|
|
159
|
+
if (typeof part === "number") {
|
|
160
|
+
return `${text}[${part}]`;
|
|
161
|
+
}
|
|
162
|
+
return text ? `${text}.${part}` : part;
|
|
163
|
+
}, "");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ../features/product-configurator/src/shared/evaluate-formula.ts
|
|
167
|
+
var FUNCTIONS = ["min", "max", "floor", "ceil", "round", "abs"];
|
|
168
|
+
var NAME_START = /[a-z_]/;
|
|
169
|
+
var NAME_PART = /[a-z0-9_]/;
|
|
170
|
+
function tokenize(source) {
|
|
171
|
+
const tokens = [];
|
|
172
|
+
let index = 0;
|
|
173
|
+
while (index < source.length) {
|
|
174
|
+
const char = source.charAt(index);
|
|
175
|
+
if (char === " " || char === " " || char === "\n") {
|
|
176
|
+
index += 1;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (/[0-9.]/.test(char)) {
|
|
180
|
+
let end = index;
|
|
181
|
+
while (end < source.length && /[0-9.]/.test(source.charAt(end))) {
|
|
182
|
+
end += 1;
|
|
183
|
+
}
|
|
184
|
+
const text = source.slice(index, end);
|
|
185
|
+
if (!/^(\d+(\.\d+)?|\.\d+)$/.test(text)) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
tokens.push({ type: "number", value: Number(text) });
|
|
189
|
+
index = end;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (NAME_START.test(char)) {
|
|
193
|
+
let end = index;
|
|
194
|
+
while (end < source.length && NAME_PART.test(source.charAt(end))) {
|
|
195
|
+
end += 1;
|
|
196
|
+
}
|
|
197
|
+
tokens.push({ type: "name", value: source.slice(index, end) });
|
|
198
|
+
index = end;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const operator = char === "\xD7" ? "*" : char === "\xF7" ? "/" : char === "\u2212" ? "-" : char;
|
|
202
|
+
if (operator === "+" || operator === "-" || operator === "*" || operator === "/" || operator === "(" || operator === ")" || operator === ",") {
|
|
203
|
+
tokens.push({ type: "op", value: operator });
|
|
204
|
+
index += 1;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
return tokens;
|
|
210
|
+
}
|
|
211
|
+
var FormulaParser = class {
|
|
212
|
+
constructor(tokens) {
|
|
213
|
+
this.tokens = tokens;
|
|
214
|
+
this.position = 0;
|
|
215
|
+
this.nodes = 0;
|
|
216
|
+
this.depth = 0;
|
|
217
|
+
this.names = [];
|
|
218
|
+
}
|
|
219
|
+
parse() {
|
|
220
|
+
const node = this.expression();
|
|
221
|
+
if (this.position !== this.tokens.length) {
|
|
222
|
+
throw new FormulaSyntaxError("formula_syntax");
|
|
223
|
+
}
|
|
224
|
+
return node;
|
|
225
|
+
}
|
|
226
|
+
count(node) {
|
|
227
|
+
this.nodes += 1;
|
|
228
|
+
if (this.nodes > CONFIGURATOR_LIMITS.formulaNodes) {
|
|
229
|
+
throw new FormulaSyntaxError("formula_too_many_nodes");
|
|
230
|
+
}
|
|
231
|
+
return node;
|
|
232
|
+
}
|
|
233
|
+
peek() {
|
|
234
|
+
return this.tokens[this.position];
|
|
235
|
+
}
|
|
236
|
+
takeOperator(value) {
|
|
237
|
+
const token = this.peek();
|
|
238
|
+
if (token?.type === "op" && token.value === value) {
|
|
239
|
+
this.position += 1;
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
expression() {
|
|
245
|
+
let left = this.term();
|
|
246
|
+
for (; ; ) {
|
|
247
|
+
if (this.takeOperator("+")) {
|
|
248
|
+
left = this.count({
|
|
249
|
+
kind: "binary",
|
|
250
|
+
operator: "+",
|
|
251
|
+
left,
|
|
252
|
+
right: this.term()
|
|
253
|
+
});
|
|
254
|
+
} else if (this.takeOperator("-")) {
|
|
255
|
+
left = this.count({
|
|
256
|
+
kind: "binary",
|
|
257
|
+
operator: "-",
|
|
258
|
+
left,
|
|
259
|
+
right: this.term()
|
|
260
|
+
});
|
|
261
|
+
} else {
|
|
262
|
+
return left;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
term() {
|
|
267
|
+
let left = this.unary();
|
|
268
|
+
for (; ; ) {
|
|
269
|
+
if (this.takeOperator("*")) {
|
|
270
|
+
left = this.count({
|
|
271
|
+
kind: "binary",
|
|
272
|
+
operator: "*",
|
|
273
|
+
left,
|
|
274
|
+
right: this.unary()
|
|
275
|
+
});
|
|
276
|
+
} else if (this.takeOperator("/")) {
|
|
277
|
+
left = this.count({
|
|
278
|
+
kind: "binary",
|
|
279
|
+
operator: "/",
|
|
280
|
+
left,
|
|
281
|
+
right: this.unary()
|
|
282
|
+
});
|
|
283
|
+
} else {
|
|
284
|
+
return left;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
unary() {
|
|
289
|
+
if (this.takeOperator("-")) {
|
|
290
|
+
return this.count({ kind: "negate", operand: this.unary() });
|
|
291
|
+
}
|
|
292
|
+
if (this.takeOperator("+")) {
|
|
293
|
+
return this.unary();
|
|
294
|
+
}
|
|
295
|
+
return this.primary();
|
|
296
|
+
}
|
|
297
|
+
nested(read) {
|
|
298
|
+
this.depth += 1;
|
|
299
|
+
if (this.depth > CONFIGURATOR_LIMITS.formulaDepth) {
|
|
300
|
+
throw new FormulaSyntaxError("formula_too_deep");
|
|
301
|
+
}
|
|
302
|
+
const result = read();
|
|
303
|
+
this.depth -= 1;
|
|
304
|
+
return result;
|
|
305
|
+
}
|
|
306
|
+
primary() {
|
|
307
|
+
const token = this.peek();
|
|
308
|
+
if (!token) {
|
|
309
|
+
throw new FormulaSyntaxError("formula_syntax");
|
|
310
|
+
}
|
|
311
|
+
if (token.type === "number") {
|
|
312
|
+
this.position += 1;
|
|
313
|
+
return this.count({ kind: "number", value: token.value });
|
|
314
|
+
}
|
|
315
|
+
if (token.type === "name") {
|
|
316
|
+
this.position += 1;
|
|
317
|
+
if (this.takeOperator("(")) {
|
|
318
|
+
if (!FUNCTIONS.includes(token.value)) {
|
|
319
|
+
throw new FormulaSyntaxError("formula_unknown_function", token.value);
|
|
320
|
+
}
|
|
321
|
+
const fn = token.value;
|
|
322
|
+
return this.nested(() => {
|
|
323
|
+
const args = [this.expression()];
|
|
324
|
+
while (this.takeOperator(",")) {
|
|
325
|
+
args.push(this.expression());
|
|
326
|
+
}
|
|
327
|
+
if (!this.takeOperator(")")) {
|
|
328
|
+
throw new FormulaSyntaxError("formula_syntax");
|
|
329
|
+
}
|
|
330
|
+
const variadic = fn === "min" || fn === "max";
|
|
331
|
+
if (!variadic && args.length !== 1) {
|
|
332
|
+
throw new FormulaSyntaxError("formula_syntax", fn);
|
|
333
|
+
}
|
|
334
|
+
return this.count({ kind: "call", fn, args });
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
if (!this.names.includes(token.value)) {
|
|
338
|
+
this.names.push(token.value);
|
|
339
|
+
}
|
|
340
|
+
return this.count({ kind: "name", name: token.value });
|
|
341
|
+
}
|
|
342
|
+
if (token.value === "(") {
|
|
343
|
+
this.position += 1;
|
|
344
|
+
return this.nested(() => {
|
|
345
|
+
const inner = this.expression();
|
|
346
|
+
if (!this.takeOperator(")")) {
|
|
347
|
+
throw new FormulaSyntaxError("formula_syntax");
|
|
348
|
+
}
|
|
349
|
+
return inner;
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
throw new FormulaSyntaxError("formula_syntax");
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
var FormulaSyntaxError = class extends Error {
|
|
356
|
+
constructor(code, detail) {
|
|
357
|
+
super(code);
|
|
358
|
+
this.code = code;
|
|
359
|
+
this.detail = detail;
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
var parsedCache = /* @__PURE__ */ new Map();
|
|
363
|
+
function parseFormula(source) {
|
|
364
|
+
if (typeof source === "number") {
|
|
365
|
+
if (!Number.isFinite(source)) {
|
|
366
|
+
return { ok: false, code: "formula_syntax" };
|
|
367
|
+
}
|
|
368
|
+
return {
|
|
369
|
+
ok: true,
|
|
370
|
+
formula: { source, root: { kind: "number", value: source }, names: [] }
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
const cached = parsedCache.get(source);
|
|
374
|
+
if (cached) {
|
|
375
|
+
return cached;
|
|
376
|
+
}
|
|
377
|
+
const result = parseUncached(source);
|
|
378
|
+
if (parsedCache.size > 2e3) {
|
|
379
|
+
parsedCache.clear();
|
|
380
|
+
}
|
|
381
|
+
parsedCache.set(source, result);
|
|
382
|
+
return result;
|
|
383
|
+
}
|
|
384
|
+
function parseUncached(source) {
|
|
385
|
+
if (source.length > CONFIGURATOR_LIMITS.formulaChars) {
|
|
386
|
+
return { ok: false, code: "formula_too_long" };
|
|
387
|
+
}
|
|
388
|
+
const tokens = tokenize(source);
|
|
389
|
+
if (!tokens || tokens.length === 0) {
|
|
390
|
+
return { ok: false, code: "formula_syntax" };
|
|
391
|
+
}
|
|
392
|
+
const parser = new FormulaParser(tokens);
|
|
393
|
+
try {
|
|
394
|
+
const root = parser.parse();
|
|
395
|
+
return { ok: true, formula: { source, root, names: parser.names } };
|
|
396
|
+
} catch (error) {
|
|
397
|
+
if (error instanceof FormulaSyntaxError) {
|
|
398
|
+
return { ok: false, code: error.code, detail: error.detail };
|
|
399
|
+
}
|
|
400
|
+
throw error;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
function evaluateFormula(formula, scope) {
|
|
404
|
+
for (const name of formula.names) {
|
|
405
|
+
if (!Object.prototype.hasOwnProperty.call(scope, name)) {
|
|
406
|
+
return { ok: false, code: "formula_unknown_name", name };
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
const value = evaluateNode(formula.root, scope);
|
|
410
|
+
if (!Number.isFinite(value)) {
|
|
411
|
+
return { ok: false, code: "formula_not_finite" };
|
|
412
|
+
}
|
|
413
|
+
return { ok: true, value };
|
|
414
|
+
}
|
|
415
|
+
function evaluateNode(node, scope) {
|
|
416
|
+
switch (node.kind) {
|
|
417
|
+
case "number":
|
|
418
|
+
return node.value;
|
|
419
|
+
case "name":
|
|
420
|
+
return scope[node.name] ?? Number.NaN;
|
|
421
|
+
case "negate":
|
|
422
|
+
return -evaluateNode(node.operand, scope);
|
|
423
|
+
case "binary":
|
|
424
|
+
return applyOperator(
|
|
425
|
+
node.operator,
|
|
426
|
+
evaluateNode(node.left, scope),
|
|
427
|
+
evaluateNode(node.right, scope)
|
|
428
|
+
);
|
|
429
|
+
case "call":
|
|
430
|
+
return applyFunction(
|
|
431
|
+
node.fn,
|
|
432
|
+
node.args.map((arg) => evaluateNode(arg, scope))
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
function applyOperator(operator, left, right) {
|
|
437
|
+
switch (operator) {
|
|
438
|
+
case "+":
|
|
439
|
+
return left + right;
|
|
440
|
+
case "-":
|
|
441
|
+
return left - right;
|
|
442
|
+
case "*":
|
|
443
|
+
return left * right;
|
|
444
|
+
case "/":
|
|
445
|
+
return left / right;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
function applyFunction(fn, args) {
|
|
449
|
+
const first = args[0] ?? Number.NaN;
|
|
450
|
+
switch (fn) {
|
|
451
|
+
case "min":
|
|
452
|
+
return Math.min(...args);
|
|
453
|
+
case "max":
|
|
454
|
+
return Math.max(...args);
|
|
455
|
+
case "floor":
|
|
456
|
+
return Math.floor(first);
|
|
457
|
+
case "ceil":
|
|
458
|
+
return Math.ceil(first);
|
|
459
|
+
case "round":
|
|
460
|
+
return Math.round(first);
|
|
461
|
+
case "abs":
|
|
462
|
+
return Math.abs(first);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
function formulaNames(source) {
|
|
466
|
+
const parsed = parseFormula(source);
|
|
467
|
+
return parsed.ok ? { ok: true, names: parsed.formula.names } : parsed;
|
|
468
|
+
}
|
|
469
|
+
function evaluateFormulaSource(source, scope) {
|
|
470
|
+
const parsed = parseFormula(source);
|
|
471
|
+
return parsed.ok ? evaluateFormula(parsed.formula, scope) : parsed;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// ../features/product-configurator/src/shared/geometry-document.schema.ts
|
|
109
475
|
import { z as z2 } from "zod";
|
|
476
|
+
var CONFIGURATOR_NAME_PATTERN = /^[a-z][a-z0-9_]{0,39}$/;
|
|
477
|
+
var CONFIGURATOR_CODE_PATTERN = /^[a-z0-9][a-z0-9_-]{0,39}$/;
|
|
478
|
+
var ConfiguratorNameSchema = z2.string().regex(CONFIGURATOR_NAME_PATTERN, "configurator.invalidName");
|
|
479
|
+
var ConfiguratorCodeSchema = z2.string().regex(CONFIGURATOR_CODE_PATTERN, "configurator.invalidCode");
|
|
480
|
+
var FormulaSchema = z2.union([
|
|
481
|
+
z2.number().finite(),
|
|
482
|
+
z2.string().trim().min(1).max(CONFIGURATOR_LIMITS.formulaChars, "configurator.formulaTooLong")
|
|
483
|
+
]);
|
|
484
|
+
var Vector3Schema = z2.tuple([FormulaSchema, FormulaSchema, FormulaSchema]);
|
|
485
|
+
var PRIMITIVE_TYPES = [
|
|
486
|
+
"box",
|
|
487
|
+
"rounded_top_box",
|
|
488
|
+
"stadium_prism",
|
|
489
|
+
"arc_panel",
|
|
490
|
+
"led_strip",
|
|
491
|
+
"niche_panel"
|
|
492
|
+
];
|
|
493
|
+
var PRIMITIVE_PARAMS = {
|
|
494
|
+
box: { required: [], optional: [] },
|
|
495
|
+
// The front top edge, rounded over its whole width.
|
|
496
|
+
rounded_top_box: { required: ["radius"], optional: [] },
|
|
497
|
+
// A stadium (a rectangle with half-round ends) seen from the front,
|
|
498
|
+
// extruded over the depth.
|
|
499
|
+
stadium_prism: { required: [], optional: [] },
|
|
500
|
+
// A quarter of an elliptical shell in plan view, filling the block's width
|
|
501
|
+
// and depth; `mirror` 0 bends it towards the right, 1 towards the left.
|
|
502
|
+
arc_panel: { required: ["thickness"], optional: ["mirror"] },
|
|
503
|
+
// A thin strip that gives light.
|
|
504
|
+
led_strip: { required: [], optional: [] },
|
|
505
|
+
// A wall with a rectangular opening, measured from the block's own corner.
|
|
506
|
+
niche_panel: {
|
|
507
|
+
required: ["niche_x", "niche_y", "niche_width", "niche_height"],
|
|
508
|
+
optional: []
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
var PrimitiveNodeSchema = z2.strictObject({
|
|
512
|
+
id: ConfiguratorNameSchema,
|
|
513
|
+
type: z2.enum(PRIMITIVE_TYPES),
|
|
514
|
+
slot: ConfiguratorNameSchema,
|
|
515
|
+
size: Vector3Schema,
|
|
516
|
+
at: Vector3Schema,
|
|
517
|
+
visible: FormulaSchema.optional(),
|
|
518
|
+
params: z2.record(ConfiguratorNameSchema, FormulaSchema).optional()
|
|
519
|
+
});
|
|
520
|
+
var GeometryNodeSchema = z2.lazy(
|
|
521
|
+
() => z2.union([
|
|
522
|
+
PrimitiveNodeSchema,
|
|
523
|
+
z2.strictObject({
|
|
524
|
+
id: ConfiguratorNameSchema,
|
|
525
|
+
type: z2.literal("repeat"),
|
|
526
|
+
count: FormulaSchema,
|
|
527
|
+
index: z2.enum(["i", "j"]).optional(),
|
|
528
|
+
visible: FormulaSchema.optional(),
|
|
529
|
+
children: z2.array(GeometryNodeSchema).min(1)
|
|
530
|
+
})
|
|
531
|
+
])
|
|
532
|
+
);
|
|
533
|
+
var GeometryBindingSchema = z2.strictObject({
|
|
534
|
+
param: ConfiguratorNameSchema,
|
|
535
|
+
map: z2.record(ConfiguratorCodeSchema, z2.number().finite()).optional()
|
|
536
|
+
});
|
|
537
|
+
var GeometryInputSchema = z2.strictObject({
|
|
538
|
+
name: ConfiguratorNameSchema,
|
|
539
|
+
binding: GeometryBindingSchema
|
|
540
|
+
});
|
|
541
|
+
var GeometrySlotSchema = z2.strictObject({
|
|
542
|
+
name: ConfiguratorNameSchema,
|
|
543
|
+
param: ConfiguratorNameSchema.optional(),
|
|
544
|
+
material: ConfiguratorCodeSchema.optional(),
|
|
545
|
+
color: z2.string().regex(/^#[0-9a-f]{6}$/i, "configurator.invalidColor").optional()
|
|
546
|
+
});
|
|
547
|
+
var GeometryAnnotationSchema = z2.strictObject({
|
|
548
|
+
id: ConfiguratorNameSchema,
|
|
549
|
+
axis: z2.enum(["x", "y"]),
|
|
550
|
+
from: FormulaSchema,
|
|
551
|
+
to: FormulaSchema,
|
|
552
|
+
at: FormulaSchema,
|
|
553
|
+
label: z2.string().trim().min(1).max(80).optional()
|
|
554
|
+
});
|
|
555
|
+
var GeometryDocumentSchema = z2.strictObject({
|
|
556
|
+
/** Rises with every new upload, so a product page can say which one it shows. */
|
|
557
|
+
version: z2.number().int().min(1),
|
|
558
|
+
/** The starter document this one was copied from, for information only. */
|
|
559
|
+
startedFrom: z2.string().trim().max(80).optional(),
|
|
560
|
+
inputs: z2.array(GeometryInputSchema).max(CONFIGURATOR_LIMITS.geometryInputs),
|
|
561
|
+
constants: z2.record(ConfiguratorNameSchema, z2.number().finite()),
|
|
562
|
+
slots: z2.array(GeometrySlotSchema).min(1).max(CONFIGURATOR_LIMITS.geometrySlots),
|
|
563
|
+
root: z2.array(GeometryNodeSchema).min(1),
|
|
564
|
+
camera: z2.strictObject({
|
|
565
|
+
/** Degrees around the vertical axis, 0 is straight on. */
|
|
566
|
+
azimuth: z2.number().min(-90).max(90),
|
|
567
|
+
/** Degrees above the horizon. */
|
|
568
|
+
elevation: z2.number().min(-10).max(80)
|
|
569
|
+
}).optional(),
|
|
570
|
+
annotations: z2.array(GeometryAnnotationSchema).max(CONFIGURATOR_LIMITS.annotations)
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
// ../features/product-configurator/src/shared/geometry-document-problems.ts
|
|
574
|
+
function geometryDocumentProblems(document, context = {}) {
|
|
575
|
+
const base = context.path ?? [];
|
|
576
|
+
const problems = [];
|
|
577
|
+
const constantNames = Object.keys(document.constants);
|
|
578
|
+
if (constantNames.length > CONFIGURATOR_LIMITS.geometryConstants) {
|
|
579
|
+
problems.push(
|
|
580
|
+
issue([...base, "constants"], "configurator.tooMany", {
|
|
581
|
+
max: CONFIGURATOR_LIMITS.geometryConstants
|
|
582
|
+
})
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
const inputNames = /* @__PURE__ */ new Set();
|
|
586
|
+
document.inputs.forEach((input, index) => {
|
|
587
|
+
const path = [...base, "inputs", index];
|
|
588
|
+
if (inputNames.has(input.name) || constantNames.includes(input.name)) {
|
|
589
|
+
problems.push(
|
|
590
|
+
issue([...path, "name"], "configurator.duplicateName", {
|
|
591
|
+
name: input.name
|
|
592
|
+
})
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
if (input.name === "i" || input.name === "j") {
|
|
596
|
+
problems.push(
|
|
597
|
+
issue([...path, "name"], "configurator.reservedName", {
|
|
598
|
+
name: input.name
|
|
599
|
+
})
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
inputNames.add(input.name);
|
|
603
|
+
if (!context.parameters) {
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
const parameter = context.parameters.get(input.binding.param);
|
|
607
|
+
if (!parameter) {
|
|
608
|
+
problems.push(
|
|
609
|
+
issue([...path, "binding", "param"], "configurator.inputUnbound", {
|
|
610
|
+
name: input.name,
|
|
611
|
+
param: input.binding.param
|
|
612
|
+
})
|
|
613
|
+
);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
if (parameter.kind === "material") {
|
|
617
|
+
problems.push(
|
|
618
|
+
issue(
|
|
619
|
+
[...path, "binding", "param"],
|
|
620
|
+
"configurator.inputCannotBindMaterial",
|
|
621
|
+
{
|
|
622
|
+
name: input.name
|
|
623
|
+
}
|
|
624
|
+
)
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
if (parameter.kind === "choice") {
|
|
628
|
+
const missing = (parameter.codes ?? []).filter(
|
|
629
|
+
(code) => input.binding.map?.[code] === void 0
|
|
630
|
+
);
|
|
631
|
+
if (missing.length > 0) {
|
|
632
|
+
problems.push(
|
|
633
|
+
issue(
|
|
634
|
+
[...path, "binding", "map"],
|
|
635
|
+
"configurator.choiceMapIncomplete",
|
|
636
|
+
{
|
|
637
|
+
name: input.name,
|
|
638
|
+
codes: missing.join(", ")
|
|
639
|
+
}
|
|
640
|
+
)
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
} else if (input.binding.map) {
|
|
644
|
+
problems.push(
|
|
645
|
+
issue([...path, "binding", "map"], "configurator.mapOnlyForChoice", {
|
|
646
|
+
name: input.name
|
|
647
|
+
})
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
const slotNames = /* @__PURE__ */ new Set();
|
|
652
|
+
document.slots.forEach((slot, index) => {
|
|
653
|
+
const path = [...base, "slots", index];
|
|
654
|
+
if (slotNames.has(slot.name)) {
|
|
655
|
+
problems.push(
|
|
656
|
+
issue([...path, "name"], "configurator.duplicateName", {
|
|
657
|
+
name: slot.name
|
|
658
|
+
})
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
slotNames.add(slot.name);
|
|
662
|
+
const sources = [slot.param, slot.material, slot.color].filter(
|
|
663
|
+
(source) => source !== void 0
|
|
664
|
+
);
|
|
665
|
+
if (sources.length !== 1) {
|
|
666
|
+
problems.push(
|
|
667
|
+
issue(path, "configurator.slotNeedsOneSource", { name: slot.name })
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
if (slot.param && context.parameters) {
|
|
671
|
+
const parameter = context.parameters.get(slot.param);
|
|
672
|
+
if (parameter?.kind !== "material") {
|
|
673
|
+
problems.push(
|
|
674
|
+
issue([...path, "param"], "configurator.slotParamNotMaterial", {
|
|
675
|
+
param: slot.param
|
|
676
|
+
})
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
if (slot.material && context.materialCodes && !context.materialCodes.has(slot.material)) {
|
|
681
|
+
problems.push(
|
|
682
|
+
issue([...path, "material"], "configurator.unknownMaterial", {
|
|
683
|
+
code: slot.material
|
|
684
|
+
})
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
const shapeNames = /* @__PURE__ */ new Set([...inputNames, ...constantNames]);
|
|
689
|
+
const ids = /* @__PURE__ */ new Set();
|
|
690
|
+
let declared = 0;
|
|
691
|
+
const checkFormula2 = (formula, path, scope) => {
|
|
692
|
+
const names = formulaNames(formula);
|
|
693
|
+
if (!names.ok) {
|
|
694
|
+
problems.push(issue(path, `configurator.${names.code}`));
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
for (const name of names.names) {
|
|
698
|
+
if (!scope.has(name)) {
|
|
699
|
+
problems.push(issue(path, "configurator.unknownName", { name }));
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
const walk = (nodes, path, scope, repeatDepth) => {
|
|
704
|
+
for (const node of nodes) {
|
|
705
|
+
const nodePath = [...path, node.id];
|
|
706
|
+
declared += 1;
|
|
707
|
+
if (ids.has(node.id)) {
|
|
708
|
+
problems.push(
|
|
709
|
+
issue([...nodePath, "id"], "configurator.duplicateId", {
|
|
710
|
+
id: node.id
|
|
711
|
+
})
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
ids.add(node.id);
|
|
715
|
+
if (node.visible !== void 0) {
|
|
716
|
+
checkFormula2(node.visible, [...nodePath, "visible"], scope);
|
|
717
|
+
}
|
|
718
|
+
if (node.type === "repeat") {
|
|
719
|
+
checkFormula2(node.count, [...nodePath, "count"], scope);
|
|
720
|
+
if (repeatDepth + 1 > CONFIGURATOR_LIMITS.repeatDepth) {
|
|
721
|
+
problems.push(
|
|
722
|
+
issue(nodePath, "configurator.repeatTooDeep", {
|
|
723
|
+
max: CONFIGURATOR_LIMITS.repeatDepth
|
|
724
|
+
})
|
|
725
|
+
);
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
const index = node.index ?? "i";
|
|
729
|
+
if (scope.has(index)) {
|
|
730
|
+
problems.push(
|
|
731
|
+
issue([...nodePath, "index"], "configurator.indexInUse", { index })
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
walk(
|
|
735
|
+
node.children,
|
|
736
|
+
nodePath,
|
|
737
|
+
/* @__PURE__ */ new Set([...scope, index]),
|
|
738
|
+
repeatDepth + 1
|
|
739
|
+
);
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
if (!slotNames.has(node.slot)) {
|
|
743
|
+
problems.push(
|
|
744
|
+
issue([...nodePath, "slot"], "configurator.unknownSlot", {
|
|
745
|
+
slot: node.slot
|
|
746
|
+
})
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
node.size.forEach(
|
|
750
|
+
(formula, axis) => checkFormula2(formula, [...nodePath, "size", axis], scope)
|
|
751
|
+
);
|
|
752
|
+
node.at.forEach(
|
|
753
|
+
(formula, axis) => checkFormula2(formula, [...nodePath, "at", axis], scope)
|
|
754
|
+
);
|
|
755
|
+
const allowed = PRIMITIVE_PARAMS[node.type];
|
|
756
|
+
const given = node.params ?? {};
|
|
757
|
+
for (const required of allowed.required) {
|
|
758
|
+
if (given[required] === void 0) {
|
|
759
|
+
problems.push(
|
|
760
|
+
issue(
|
|
761
|
+
[...nodePath, "params", required],
|
|
762
|
+
"configurator.missingBlockParam",
|
|
763
|
+
{
|
|
764
|
+
type: node.type,
|
|
765
|
+
param: required
|
|
766
|
+
}
|
|
767
|
+
)
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
for (const [key, formula] of Object.entries(given)) {
|
|
772
|
+
if (!allowed.required.includes(key) && !allowed.optional.includes(key)) {
|
|
773
|
+
problems.push(
|
|
774
|
+
issue(
|
|
775
|
+
[...nodePath, "params", key],
|
|
776
|
+
"configurator.unknownBlockParam",
|
|
777
|
+
{
|
|
778
|
+
type: node.type,
|
|
779
|
+
param: key
|
|
780
|
+
}
|
|
781
|
+
)
|
|
782
|
+
);
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
checkFormula2(formula, [...nodePath, "params", key], scope);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
walk(document.root, [...base, "root"], shapeNames, 0);
|
|
790
|
+
if (declared > CONFIGURATOR_LIMITS.geometryNodes) {
|
|
791
|
+
problems.push(
|
|
792
|
+
issue([...base, "root"], "configurator.tooMany", {
|
|
793
|
+
max: CONFIGURATOR_LIMITS.geometryNodes
|
|
794
|
+
})
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
const annotationIds = /* @__PURE__ */ new Set();
|
|
798
|
+
document.annotations.forEach((annotation, index) => {
|
|
799
|
+
const path = [...base, "annotations", index];
|
|
800
|
+
if (annotationIds.has(annotation.id)) {
|
|
801
|
+
problems.push(
|
|
802
|
+
issue([...path, "id"], "configurator.duplicateId", {
|
|
803
|
+
id: annotation.id
|
|
804
|
+
})
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
annotationIds.add(annotation.id);
|
|
808
|
+
checkFormula2(annotation.from, [...path, "from"], shapeNames);
|
|
809
|
+
checkFormula2(annotation.to, [...path, "to"], shapeNames);
|
|
810
|
+
checkFormula2(annotation.at, [...path, "at"], shapeNames);
|
|
811
|
+
});
|
|
812
|
+
return problems;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// ../features/product-configurator/src/shared/configurator-structure.schema.ts
|
|
816
|
+
import { z as z3 } from "zod";
|
|
817
|
+
var LabelSchema = z3.string().trim().min(1).max(80);
|
|
818
|
+
var CONFIGURATOR_TEXTURE_PATH_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.(webp|jpg|jpeg)$/;
|
|
819
|
+
var TexturePathSchema = z3.string().regex(CONFIGURATOR_TEXTURE_PATH_PATTERN, "configurator.invalidTexturePath");
|
|
820
|
+
var ParameterBase = {
|
|
821
|
+
key: ConfiguratorNameSchema,
|
|
822
|
+
label: LabelSchema,
|
|
823
|
+
tab: ConfiguratorNameSchema
|
|
824
|
+
};
|
|
825
|
+
var ConfiguratorParameterSchema = z3.discriminatedUnion("kind", [
|
|
826
|
+
z3.strictObject({
|
|
827
|
+
...ParameterBase,
|
|
828
|
+
kind: z3.literal("number"),
|
|
829
|
+
/** `mm` is a measure, `count` a number of things (compartments). */
|
|
830
|
+
unit: z3.enum(["mm", "count"]),
|
|
831
|
+
step: z3.number().positive().finite(),
|
|
832
|
+
/** The widest range any model may offer; a model narrows it in its value. */
|
|
833
|
+
min: z3.number().finite(),
|
|
834
|
+
max: z3.number().finite()
|
|
835
|
+
}),
|
|
836
|
+
z3.strictObject({
|
|
837
|
+
...ParameterBase,
|
|
838
|
+
kind: z3.literal("choice"),
|
|
839
|
+
list: ConfiguratorNameSchema
|
|
840
|
+
}),
|
|
841
|
+
z3.strictObject({ ...ParameterBase, kind: z3.literal("material") }),
|
|
842
|
+
z3.strictObject({ ...ParameterBase, kind: z3.literal("toggle") })
|
|
843
|
+
]);
|
|
844
|
+
var ConfiguratorChoiceListSchema = z3.strictObject({
|
|
845
|
+
key: ConfiguratorNameSchema,
|
|
846
|
+
label: LabelSchema,
|
|
847
|
+
options: z3.array(
|
|
848
|
+
z3.strictObject({
|
|
849
|
+
code: ConfiguratorCodeSchema,
|
|
850
|
+
label: LabelSchema,
|
|
851
|
+
image: TexturePathSchema.optional()
|
|
852
|
+
})
|
|
853
|
+
).min(1).max(CONFIGURATOR_LIMITS.optionsPerList)
|
|
854
|
+
});
|
|
855
|
+
var ConfiguratorMaterialSchema = z3.strictObject({
|
|
856
|
+
code: ConfiguratorCodeSchema,
|
|
857
|
+
label: LabelSchema,
|
|
858
|
+
color: z3.string().regex(/^#[0-9a-f]{6}$/i, "configurator.invalidColor"),
|
|
859
|
+
texture: TexturePathSchema.optional(),
|
|
860
|
+
/** Millimetres one repeat of the texture covers. */
|
|
861
|
+
textureSize: z3.number().positive().max(1e4).optional(),
|
|
862
|
+
roughness: z3.number().min(0).max(1).optional(),
|
|
863
|
+
metalness: z3.number().min(0).max(1).optional(),
|
|
864
|
+
/** How much of what is behind it shows through: glass in front of a flame. Absent is solid. */
|
|
865
|
+
opacity: z3.number().min(0.05).max(1).optional(),
|
|
866
|
+
/** Gives light rather than reflecting it: a flame, a LED. */
|
|
867
|
+
emissive: z3.boolean().optional(),
|
|
868
|
+
priceGroup: ConfiguratorCodeSchema.optional()
|
|
869
|
+
});
|
|
870
|
+
var ConditionValueSchema = z3.union([
|
|
871
|
+
z3.number().finite(),
|
|
872
|
+
z3.boolean(),
|
|
873
|
+
ConfiguratorCodeSchema,
|
|
874
|
+
z3.array(ConfiguratorCodeSchema).min(1).max(CONFIGURATOR_LIMITS.optionsPerList)
|
|
875
|
+
]);
|
|
876
|
+
var ConfiguratorConditionSchema = z3.strictObject({
|
|
877
|
+
param: ConfiguratorNameSchema,
|
|
878
|
+
op: z3.enum(["eq", "neq", "in", "gte", "lte"]),
|
|
879
|
+
value: ConditionValueSchema
|
|
880
|
+
});
|
|
881
|
+
var ConfiguratorRuleSchema = z3.discriminatedUnion("kind", [
|
|
882
|
+
/** Show `param` only while `when` holds. */
|
|
883
|
+
z3.strictObject({
|
|
884
|
+
kind: z3.literal("visible"),
|
|
885
|
+
param: ConfiguratorNameSchema,
|
|
886
|
+
when: ConfiguratorConditionSchema
|
|
887
|
+
}),
|
|
888
|
+
/** Narrow a number parameter: `max cabinet_height = total_height - 300`. */
|
|
889
|
+
z3.strictObject({
|
|
890
|
+
kind: z3.literal("limit"),
|
|
891
|
+
param: ConfiguratorNameSchema,
|
|
892
|
+
bound: z3.enum(["min", "max"]),
|
|
893
|
+
formula: FormulaSchema
|
|
894
|
+
}),
|
|
895
|
+
/** While `when` holds, these codes of a choice or material are not offered. */
|
|
896
|
+
z3.strictObject({
|
|
897
|
+
kind: z3.literal("exclude"),
|
|
898
|
+
param: ConfiguratorNameSchema,
|
|
899
|
+
codes: z3.array(ConfiguratorCodeSchema).min(1).max(CONFIGURATOR_LIMITS.optionsPerList),
|
|
900
|
+
when: ConfiguratorConditionSchema
|
|
901
|
+
})
|
|
902
|
+
]);
|
|
903
|
+
var PriceRuleBase = {
|
|
904
|
+
key: ConfiguratorNameSchema,
|
|
905
|
+
label: LabelSchema,
|
|
906
|
+
/** How many times the amount counts, e.g. `compartments`. Absent is once. */
|
|
907
|
+
multiplier: FormulaSchema.optional()
|
|
908
|
+
};
|
|
909
|
+
var ConfiguratorPriceRuleSchema = z3.discriminatedUnion("kind", [
|
|
910
|
+
/** The surcharge the model gives the chosen option. */
|
|
911
|
+
z3.strictObject({
|
|
912
|
+
...PriceRuleBase,
|
|
913
|
+
kind: z3.literal("choice"),
|
|
914
|
+
param: ConfiguratorNameSchema
|
|
915
|
+
}),
|
|
916
|
+
/** The surcharge of the chosen material's price group. */
|
|
917
|
+
z3.strictObject({
|
|
918
|
+
...PriceRuleBase,
|
|
919
|
+
kind: z3.literal("material"),
|
|
920
|
+
param: ConfiguratorNameSchema
|
|
921
|
+
}),
|
|
922
|
+
/** An amount, counted while a switch is on. */
|
|
923
|
+
z3.strictObject({
|
|
924
|
+
...PriceRuleBase,
|
|
925
|
+
kind: z3.literal("toggle"),
|
|
926
|
+
param: ConfiguratorNameSchema,
|
|
927
|
+
amount: ConfiguratorNameSchema
|
|
928
|
+
}),
|
|
929
|
+
/**
|
|
930
|
+
* A price on measure: `max(0, total_width - 1800) / 1000 * meter_price`.
|
|
931
|
+
* Reads number parameters (in their unit) and amounts (in cents).
|
|
932
|
+
*/
|
|
933
|
+
z3.strictObject({
|
|
934
|
+
key: ConfiguratorNameSchema,
|
|
935
|
+
label: LabelSchema,
|
|
936
|
+
kind: z3.literal("formula"),
|
|
937
|
+
formula: FormulaSchema
|
|
938
|
+
})
|
|
939
|
+
]);
|
|
940
|
+
var ConfiguratorStructureObjectSchema = z3.strictObject({
|
|
941
|
+
schemaVersion: z3.literal(1),
|
|
942
|
+
geometry: GeometryDocumentSchema,
|
|
943
|
+
tabs: z3.array(z3.strictObject({ key: ConfiguratorNameSchema, label: LabelSchema })).min(1).max(CONFIGURATOR_LIMITS.tabs),
|
|
944
|
+
parameters: z3.array(ConfiguratorParameterSchema).max(CONFIGURATOR_LIMITS.parameters),
|
|
945
|
+
choiceLists: z3.array(ConfiguratorChoiceListSchema).max(CONFIGURATOR_LIMITS.choiceLists),
|
|
946
|
+
materials: z3.array(ConfiguratorMaterialSchema).max(CONFIGURATOR_LIMITS.materials),
|
|
947
|
+
priceGroups: z3.array(z3.strictObject({ code: ConfiguratorCodeSchema, label: LabelSchema })).max(CONFIGURATOR_LIMITS.priceGroups),
|
|
948
|
+
/** Named amounts a model fills in and price rules read (`meter_price`). */
|
|
949
|
+
amounts: z3.array(z3.strictObject({ key: ConfiguratorNameSchema, label: LabelSchema })).max(CONFIGURATOR_LIMITS.amounts),
|
|
950
|
+
/**
|
|
951
|
+
* A list of fixed combinations a model offers, each setting some number
|
|
952
|
+
* parameters at once: a fireplace and the niche it needs.
|
|
953
|
+
*/
|
|
954
|
+
presets: z3.strictObject({
|
|
955
|
+
label: LabelSchema,
|
|
956
|
+
tab: ConfiguratorNameSchema,
|
|
957
|
+
sets: z3.array(ConfiguratorNameSchema).min(1).max(CONFIGURATOR_LIMITS.parameters),
|
|
958
|
+
required: z3.boolean()
|
|
959
|
+
}).optional(),
|
|
960
|
+
rules: z3.array(ConfiguratorRuleSchema).max(CONFIGURATOR_LIMITS.rules),
|
|
961
|
+
priceRules: z3.array(ConfiguratorPriceRuleSchema).max(CONFIGURATOR_LIMITS.priceRules)
|
|
962
|
+
});
|
|
963
|
+
var ConfiguratorStructureSchema = ConfiguratorStructureObjectSchema.superRefine((structure, ctx) => {
|
|
964
|
+
for (const problem of configuratorStructureProblems(structure)) {
|
|
965
|
+
ctx.addIssue({
|
|
966
|
+
code: "custom",
|
|
967
|
+
path: problem.path,
|
|
968
|
+
message: problem.code,
|
|
969
|
+
params: problem.params
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
});
|
|
973
|
+
function configuratorStructureProblems(structure) {
|
|
974
|
+
const problems = [];
|
|
975
|
+
if (jsonByteLength(structure) > CONFIGURATOR_LIMITS.structureBytes) {
|
|
976
|
+
problems.push(
|
|
977
|
+
issue([], "configurator.structureTooLarge", {
|
|
978
|
+
max: CONFIGURATOR_LIMITS.structureBytes
|
|
979
|
+
})
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
const tabs = uniqueKeys(
|
|
983
|
+
structure.tabs,
|
|
984
|
+
(tab) => tab.key,
|
|
985
|
+
["tabs"],
|
|
986
|
+
"key",
|
|
987
|
+
problems
|
|
988
|
+
);
|
|
989
|
+
const lists = new Map(
|
|
990
|
+
structure.choiceLists.map((list) => [
|
|
991
|
+
list.key,
|
|
992
|
+
list.options.map((option) => option.code)
|
|
993
|
+
])
|
|
994
|
+
);
|
|
995
|
+
uniqueKeys(
|
|
996
|
+
structure.choiceLists,
|
|
997
|
+
(list) => list.key,
|
|
998
|
+
["choiceLists"],
|
|
999
|
+
"key",
|
|
1000
|
+
problems
|
|
1001
|
+
);
|
|
1002
|
+
structure.choiceLists.forEach(
|
|
1003
|
+
(list, index) => uniqueKeys(
|
|
1004
|
+
list.options,
|
|
1005
|
+
(option) => option.code,
|
|
1006
|
+
["choiceLists", index, "options"],
|
|
1007
|
+
"code",
|
|
1008
|
+
problems
|
|
1009
|
+
)
|
|
1010
|
+
);
|
|
1011
|
+
const materials = uniqueKeys(
|
|
1012
|
+
structure.materials,
|
|
1013
|
+
(material) => material.code,
|
|
1014
|
+
["materials"],
|
|
1015
|
+
"code",
|
|
1016
|
+
problems
|
|
1017
|
+
);
|
|
1018
|
+
const groups = uniqueKeys(
|
|
1019
|
+
structure.priceGroups,
|
|
1020
|
+
(group) => group.code,
|
|
1021
|
+
["priceGroups"],
|
|
1022
|
+
"code",
|
|
1023
|
+
problems
|
|
1024
|
+
);
|
|
1025
|
+
const amounts = uniqueKeys(
|
|
1026
|
+
structure.amounts,
|
|
1027
|
+
(amount) => amount.key,
|
|
1028
|
+
["amounts"],
|
|
1029
|
+
"key",
|
|
1030
|
+
problems
|
|
1031
|
+
);
|
|
1032
|
+
structure.materials.forEach((material, index) => {
|
|
1033
|
+
if (material.priceGroup && !groups.has(material.priceGroup)) {
|
|
1034
|
+
problems.push(
|
|
1035
|
+
issue(
|
|
1036
|
+
["materials", index, "priceGroup"],
|
|
1037
|
+
"configurator.unknownPriceGroup",
|
|
1038
|
+
{
|
|
1039
|
+
code: material.priceGroup
|
|
1040
|
+
}
|
|
1041
|
+
)
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
1045
|
+
const parameters = new Map(
|
|
1046
|
+
structure.parameters.map((parameter) => [parameter.key, parameter])
|
|
1047
|
+
);
|
|
1048
|
+
uniqueKeys(
|
|
1049
|
+
structure.parameters,
|
|
1050
|
+
(parameter) => parameter.key,
|
|
1051
|
+
["parameters"],
|
|
1052
|
+
"key",
|
|
1053
|
+
problems
|
|
1054
|
+
);
|
|
1055
|
+
structure.parameters.forEach((parameter, index) => {
|
|
1056
|
+
const path = ["parameters", index];
|
|
1057
|
+
if (!tabs.has(parameter.tab)) {
|
|
1058
|
+
problems.push(
|
|
1059
|
+
issue([...path, "tab"], "configurator.unknownTab", {
|
|
1060
|
+
tab: parameter.tab
|
|
1061
|
+
})
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
1064
|
+
if (parameter.kind === "number" && parameter.min > parameter.max) {
|
|
1065
|
+
problems.push(
|
|
1066
|
+
issue([...path, "min"], "configurator.minAboveMax", {
|
|
1067
|
+
key: parameter.key
|
|
1068
|
+
})
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
if (parameter.kind === "choice" && !lists.has(parameter.list)) {
|
|
1072
|
+
problems.push(
|
|
1073
|
+
issue([...path, "list"], "configurator.unknownList", {
|
|
1074
|
+
list: parameter.list
|
|
1075
|
+
})
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
});
|
|
1079
|
+
const numberKeys = new Set(
|
|
1080
|
+
structure.parameters.filter((parameter) => parameter.kind === "number").map((parameter) => parameter.key)
|
|
1081
|
+
);
|
|
1082
|
+
const toggleKeys = new Set(
|
|
1083
|
+
structure.parameters.filter((parameter) => parameter.kind === "toggle").map((parameter) => parameter.key)
|
|
1084
|
+
);
|
|
1085
|
+
const numericScope = /* @__PURE__ */ new Set([...numberKeys, ...toggleKeys]);
|
|
1086
|
+
if (structure.presets) {
|
|
1087
|
+
if (!tabs.has(structure.presets.tab)) {
|
|
1088
|
+
problems.push(
|
|
1089
|
+
issue(["presets", "tab"], "configurator.unknownTab", {
|
|
1090
|
+
tab: structure.presets.tab
|
|
1091
|
+
})
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
structure.presets.sets.forEach((key, index) => {
|
|
1095
|
+
if (!numberKeys.has(key)) {
|
|
1096
|
+
problems.push(
|
|
1097
|
+
issue(["presets", "sets", index], "configurator.notANumberParam", {
|
|
1098
|
+
key
|
|
1099
|
+
})
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
const dependsOn = /* @__PURE__ */ new Map();
|
|
1105
|
+
const addDependency = (from, on) => {
|
|
1106
|
+
const set = dependsOn.get(from) ?? /* @__PURE__ */ new Set();
|
|
1107
|
+
set.add(on);
|
|
1108
|
+
dependsOn.set(from, set);
|
|
1109
|
+
};
|
|
1110
|
+
structure.rules.forEach((rule, index) => {
|
|
1111
|
+
const path = ["rules", index];
|
|
1112
|
+
const target = parameters.get(rule.param);
|
|
1113
|
+
if (!target) {
|
|
1114
|
+
problems.push(
|
|
1115
|
+
issue([...path, "param"], "configurator.unknownParam", {
|
|
1116
|
+
key: rule.param
|
|
1117
|
+
})
|
|
1118
|
+
);
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
if (rule.kind === "limit") {
|
|
1122
|
+
if (target.kind !== "number") {
|
|
1123
|
+
problems.push(
|
|
1124
|
+
issue([...path, "param"], "configurator.notANumberParam", {
|
|
1125
|
+
key: rule.param
|
|
1126
|
+
})
|
|
1127
|
+
);
|
|
1128
|
+
}
|
|
1129
|
+
for (const name of checkFormula(
|
|
1130
|
+
rule.formula,
|
|
1131
|
+
[...path, "formula"],
|
|
1132
|
+
numericScope,
|
|
1133
|
+
problems
|
|
1134
|
+
)) {
|
|
1135
|
+
addDependency(rule.param, name);
|
|
1136
|
+
}
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
if (rule.kind === "exclude") {
|
|
1140
|
+
const codes = codesOf(target, lists, materials);
|
|
1141
|
+
if (!codes) {
|
|
1142
|
+
problems.push(
|
|
1143
|
+
issue([...path, "param"], "configurator.notAChoiceParam", {
|
|
1144
|
+
key: rule.param
|
|
1145
|
+
})
|
|
1146
|
+
);
|
|
1147
|
+
} else {
|
|
1148
|
+
rule.codes.forEach((code, codeIndex) => {
|
|
1149
|
+
if (!codes.has(code)) {
|
|
1150
|
+
problems.push(
|
|
1151
|
+
issue([...path, "codes", codeIndex], "configurator.unknownCode", {
|
|
1152
|
+
code
|
|
1153
|
+
})
|
|
1154
|
+
);
|
|
1155
|
+
}
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
const condition = rule.when;
|
|
1160
|
+
const conditionParam = parameters.get(condition.param);
|
|
1161
|
+
if (!conditionParam) {
|
|
1162
|
+
problems.push(
|
|
1163
|
+
issue([...path, "when", "param"], "configurator.unknownParam", {
|
|
1164
|
+
key: condition.param
|
|
1165
|
+
})
|
|
1166
|
+
);
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
addDependency(rule.param, condition.param);
|
|
1170
|
+
const valueProblem = conditionValueProblem(
|
|
1171
|
+
condition.op,
|
|
1172
|
+
condition.value,
|
|
1173
|
+
conditionParam,
|
|
1174
|
+
lists,
|
|
1175
|
+
materials
|
|
1176
|
+
);
|
|
1177
|
+
if (valueProblem) {
|
|
1178
|
+
problems.push(
|
|
1179
|
+
issue([...path, "when", "value"], valueProblem, {
|
|
1180
|
+
key: condition.param
|
|
1181
|
+
})
|
|
1182
|
+
);
|
|
1183
|
+
}
|
|
1184
|
+
});
|
|
1185
|
+
const cycle = findCycle(dependsOn);
|
|
1186
|
+
if (cycle) {
|
|
1187
|
+
problems.push(
|
|
1188
|
+
issue(["rules"], "configurator.ruleCycle", { cycle: cycle.join(" \u2192 ") })
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
const priceRuleKeys = /* @__PURE__ */ new Set();
|
|
1192
|
+
structure.priceRules.forEach((rule, index) => {
|
|
1193
|
+
const path = ["priceRules", index];
|
|
1194
|
+
if (priceRuleKeys.has(rule.key)) {
|
|
1195
|
+
problems.push(
|
|
1196
|
+
issue([...path, "key"], "configurator.duplicateName", {
|
|
1197
|
+
name: rule.key
|
|
1198
|
+
})
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
1201
|
+
priceRuleKeys.add(rule.key);
|
|
1202
|
+
if (rule.kind === "formula") {
|
|
1203
|
+
checkFormula(
|
|
1204
|
+
rule.formula,
|
|
1205
|
+
[...path, "formula"],
|
|
1206
|
+
/* @__PURE__ */ new Set([...numericScope, ...amounts]),
|
|
1207
|
+
problems
|
|
1208
|
+
);
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
if (rule.multiplier !== void 0) {
|
|
1212
|
+
checkFormula(
|
|
1213
|
+
rule.multiplier,
|
|
1214
|
+
[...path, "multiplier"],
|
|
1215
|
+
numericScope,
|
|
1216
|
+
problems
|
|
1217
|
+
);
|
|
1218
|
+
}
|
|
1219
|
+
const target = parameters.get(rule.param);
|
|
1220
|
+
if (!target || target.kind !== rule.kind) {
|
|
1221
|
+
problems.push(
|
|
1222
|
+
issue([...path, "param"], "configurator.priceRuleParamKind", {
|
|
1223
|
+
key: rule.param,
|
|
1224
|
+
kind: rule.kind
|
|
1225
|
+
})
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
if (rule.kind === "toggle" && !amounts.has(rule.amount)) {
|
|
1229
|
+
problems.push(
|
|
1230
|
+
issue([...path, "amount"], "configurator.unknownAmount", {
|
|
1231
|
+
key: rule.amount
|
|
1232
|
+
})
|
|
1233
|
+
);
|
|
1234
|
+
}
|
|
1235
|
+
});
|
|
1236
|
+
const geometryParameters = new Map(
|
|
1237
|
+
structure.parameters.map((parameter) => [
|
|
1238
|
+
parameter.key,
|
|
1239
|
+
parameter.kind === "choice" ? { kind: "choice", codes: lists.get(parameter.list) ?? [] } : { kind: parameter.kind }
|
|
1240
|
+
])
|
|
1241
|
+
);
|
|
1242
|
+
problems.push(
|
|
1243
|
+
...geometryDocumentProblems(structure.geometry, {
|
|
1244
|
+
parameters: geometryParameters,
|
|
1245
|
+
materialCodes: materials,
|
|
1246
|
+
path: ["geometry"]
|
|
1247
|
+
})
|
|
1248
|
+
);
|
|
1249
|
+
return problems;
|
|
1250
|
+
}
|
|
1251
|
+
function uniqueKeys(items, keyOf, path, field, problems) {
|
|
1252
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1253
|
+
items.forEach((item, index) => {
|
|
1254
|
+
const key = keyOf(item);
|
|
1255
|
+
if (seen.has(key)) {
|
|
1256
|
+
problems.push(
|
|
1257
|
+
issue([...path, index, field], "configurator.duplicateName", {
|
|
1258
|
+
name: key
|
|
1259
|
+
})
|
|
1260
|
+
);
|
|
1261
|
+
}
|
|
1262
|
+
seen.add(key);
|
|
1263
|
+
});
|
|
1264
|
+
return seen;
|
|
1265
|
+
}
|
|
1266
|
+
function checkFormula(formula, path, scope, problems) {
|
|
1267
|
+
const names = formulaNames(formula);
|
|
1268
|
+
if (!names.ok) {
|
|
1269
|
+
problems.push(issue(path, `configurator.${names.code}`));
|
|
1270
|
+
return [];
|
|
1271
|
+
}
|
|
1272
|
+
for (const name of names.names) {
|
|
1273
|
+
if (!scope.has(name)) {
|
|
1274
|
+
problems.push(issue(path, "configurator.unknownName", { name }));
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
return names.names;
|
|
1278
|
+
}
|
|
1279
|
+
function codesOf(parameter, lists, materials) {
|
|
1280
|
+
if (parameter.kind === "choice") {
|
|
1281
|
+
return new Set(lists.get(parameter.list) ?? []);
|
|
1282
|
+
}
|
|
1283
|
+
if (parameter.kind === "material") {
|
|
1284
|
+
return materials;
|
|
1285
|
+
}
|
|
1286
|
+
return null;
|
|
1287
|
+
}
|
|
1288
|
+
function conditionValueProblem(op, value, parameter, lists, materials) {
|
|
1289
|
+
if (parameter.kind === "number") {
|
|
1290
|
+
return typeof value === "number" && op !== "in" ? null : "configurator.conditionNeedsNumber";
|
|
1291
|
+
}
|
|
1292
|
+
if (parameter.kind === "toggle") {
|
|
1293
|
+
return typeof value === "boolean" && (op === "eq" || op === "neq") ? null : "configurator.conditionNeedsBoolean";
|
|
1294
|
+
}
|
|
1295
|
+
const codes = codesOf(parameter, lists, materials) ?? /* @__PURE__ */ new Set();
|
|
1296
|
+
if (op === "in") {
|
|
1297
|
+
return Array.isArray(value) && value.every((code) => codes.has(code)) ? null : "configurator.conditionUnknownCode";
|
|
1298
|
+
}
|
|
1299
|
+
return (op === "eq" || op === "neq") && typeof value === "string" && codes.has(value) ? null : "configurator.conditionUnknownCode";
|
|
1300
|
+
}
|
|
1301
|
+
function findCycle(graph) {
|
|
1302
|
+
const state = /* @__PURE__ */ new Map();
|
|
1303
|
+
const stack = [];
|
|
1304
|
+
const visit = (node) => {
|
|
1305
|
+
const current = state.get(node);
|
|
1306
|
+
if (current === "done") {
|
|
1307
|
+
return null;
|
|
1308
|
+
}
|
|
1309
|
+
if (current === "visiting") {
|
|
1310
|
+
return [...stack.slice(stack.indexOf(node)), node];
|
|
1311
|
+
}
|
|
1312
|
+
state.set(node, "visiting");
|
|
1313
|
+
stack.push(node);
|
|
1314
|
+
for (const next of graph.get(node) ?? []) {
|
|
1315
|
+
const cycle = visit(next);
|
|
1316
|
+
if (cycle) {
|
|
1317
|
+
return cycle;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
stack.pop();
|
|
1321
|
+
state.set(node, "done");
|
|
1322
|
+
return null;
|
|
1323
|
+
};
|
|
1324
|
+
for (const node of graph.keys()) {
|
|
1325
|
+
const cycle = visit(node);
|
|
1326
|
+
if (cycle) {
|
|
1327
|
+
return cycle;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
return null;
|
|
1331
|
+
}
|
|
1332
|
+
function parameterResolutionOrder(structure) {
|
|
1333
|
+
const dependsOn = /* @__PURE__ */ new Map();
|
|
1334
|
+
for (const rule of structure.rules) {
|
|
1335
|
+
const set = dependsOn.get(rule.param) ?? /* @__PURE__ */ new Set();
|
|
1336
|
+
if (rule.kind === "limit") {
|
|
1337
|
+
const names = formulaNames(rule.formula);
|
|
1338
|
+
for (const name of names.ok ? names.names : []) {
|
|
1339
|
+
set.add(name);
|
|
1340
|
+
}
|
|
1341
|
+
} else {
|
|
1342
|
+
set.add(rule.when.param);
|
|
1343
|
+
}
|
|
1344
|
+
dependsOn.set(rule.param, set);
|
|
1345
|
+
}
|
|
1346
|
+
const order = [];
|
|
1347
|
+
const placed = /* @__PURE__ */ new Set();
|
|
1348
|
+
const place = (key, trail) => {
|
|
1349
|
+
if (placed.has(key) || trail.has(key)) {
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
trail.add(key);
|
|
1353
|
+
for (const dependency of dependsOn.get(key) ?? []) {
|
|
1354
|
+
place(dependency, trail);
|
|
1355
|
+
}
|
|
1356
|
+
placed.add(key);
|
|
1357
|
+
order.push(key);
|
|
1358
|
+
};
|
|
1359
|
+
for (const parameter of structure.parameters) {
|
|
1360
|
+
place(parameter.key, /* @__PURE__ */ new Set());
|
|
1361
|
+
}
|
|
1362
|
+
return order.filter(
|
|
1363
|
+
(key) => structure.parameters.some((parameter) => parameter.key === key)
|
|
1364
|
+
);
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// ../features/product-configurator/src/shared/configurator-value.schema.ts
|
|
1368
|
+
import { z as z4 } from "zod";
|
|
1369
|
+
var CentsSchema = z4.number().int().min(0).max(1e8);
|
|
1370
|
+
var PriceSchema = CentsSchema.nullable();
|
|
1371
|
+
var ConfiguratorValueObjectSchema = z4.strictObject({
|
|
1372
|
+
/** Null: the model has no price of its own, every quote is "open". */
|
|
1373
|
+
basePriceCents: PriceSchema,
|
|
1374
|
+
numbers: z4.record(
|
|
1375
|
+
ConfiguratorNameSchema,
|
|
1376
|
+
z4.strictObject({
|
|
1377
|
+
min: z4.number().finite(),
|
|
1378
|
+
max: z4.number().finite(),
|
|
1379
|
+
default: z4.number().finite()
|
|
1380
|
+
})
|
|
1381
|
+
).default({}),
|
|
1382
|
+
/** Per choice list: the options this model offers, with their surcharge. */
|
|
1383
|
+
choices: z4.record(
|
|
1384
|
+
ConfiguratorNameSchema,
|
|
1385
|
+
z4.strictObject({
|
|
1386
|
+
options: z4.record(
|
|
1387
|
+
ConfiguratorCodeSchema,
|
|
1388
|
+
z4.strictObject({ priceCents: PriceSchema })
|
|
1389
|
+
),
|
|
1390
|
+
default: ConfiguratorCodeSchema.optional()
|
|
1391
|
+
})
|
|
1392
|
+
).default({}),
|
|
1393
|
+
/** Per material parameter: the materials this model offers. */
|
|
1394
|
+
materials: z4.record(
|
|
1395
|
+
ConfiguratorNameSchema,
|
|
1396
|
+
z4.strictObject({
|
|
1397
|
+
offered: z4.array(ConfiguratorCodeSchema).max(CONFIGURATOR_LIMITS.materials),
|
|
1398
|
+
default: ConfiguratorCodeSchema.optional()
|
|
1399
|
+
})
|
|
1400
|
+
).default({}),
|
|
1401
|
+
/** Per switch: whether this model offers it, and how it starts. Absent is offered, off. */
|
|
1402
|
+
toggles: z4.record(
|
|
1403
|
+
ConfiguratorNameSchema,
|
|
1404
|
+
z4.strictObject({ offered: z4.boolean(), default: z4.boolean() })
|
|
1405
|
+
).default({}),
|
|
1406
|
+
/** The surcharge of each material price group. */
|
|
1407
|
+
priceGroups: z4.record(ConfiguratorCodeSchema, PriceSchema).default({}),
|
|
1408
|
+
/** The amounts the structure names, such as a price per metre. */
|
|
1409
|
+
amounts: z4.record(ConfiguratorNameSchema, PriceSchema).default({}),
|
|
1410
|
+
presets: z4.array(
|
|
1411
|
+
z4.strictObject({
|
|
1412
|
+
code: ConfiguratorCodeSchema,
|
|
1413
|
+
label: z4.string().trim().min(1).max(80),
|
|
1414
|
+
/** A product of the same account this preset stands for, e.g. the fireplace. */
|
|
1415
|
+
productId: z4.string().uuid().nullable(),
|
|
1416
|
+
values: z4.record(ConfiguratorNameSchema, z4.number().finite())
|
|
1417
|
+
})
|
|
1418
|
+
).max(CONFIGURATOR_LIMITS.presets).default([])
|
|
1419
|
+
});
|
|
1420
|
+
function emptyConfiguratorValue() {
|
|
1421
|
+
return ConfiguratorValueObjectSchema.parse({ basePriceCents: null });
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
// ../features/product-configurator/src/shared/resolve-configuration.ts
|
|
1425
|
+
var NEUTRAL_SLOT_COLOR = "#b8b4ad";
|
|
1426
|
+
var STEP_TOLERANCE = 1e-6;
|
|
1427
|
+
function resolveConfiguration(params) {
|
|
1428
|
+
const { structure, value, selection } = params;
|
|
1429
|
+
const issues = [];
|
|
1430
|
+
const parametersByKey = new Map(
|
|
1431
|
+
structure.parameters.map((parameter) => [parameter.key, parameter])
|
|
1432
|
+
);
|
|
1433
|
+
const materialsByCode = new Map(
|
|
1434
|
+
structure.materials.map((material) => [material.code, material])
|
|
1435
|
+
);
|
|
1436
|
+
for (const key of Object.keys(selection.values)) {
|
|
1437
|
+
if (!parametersByKey.has(key)) {
|
|
1438
|
+
issues.push(issue(["values", key], "configurator.unknownParam", { key }));
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
const presets = structure.presets ? value.presets.map((preset2) => ({
|
|
1442
|
+
code: preset2.code,
|
|
1443
|
+
label: preset2.label,
|
|
1444
|
+
productId: preset2.productId
|
|
1445
|
+
})) : [];
|
|
1446
|
+
let preset = null;
|
|
1447
|
+
if (selection.preset !== void 0) {
|
|
1448
|
+
preset = presets.find((candidate) => candidate.code === selection.preset) ?? null;
|
|
1449
|
+
if (!preset) {
|
|
1450
|
+
issues.push(
|
|
1451
|
+
issue(["preset"], "configurator.presetNotOffered", {
|
|
1452
|
+
code: selection.preset
|
|
1453
|
+
})
|
|
1454
|
+
);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
if (!preset && structure.presets?.required) {
|
|
1458
|
+
preset = presets[0] ?? null;
|
|
1459
|
+
}
|
|
1460
|
+
const presetValues = preset ? value.presets.find((candidate) => candidate.code === preset?.code)?.values ?? {} : {};
|
|
1461
|
+
const presetSets = new Set(structure.presets?.sets ?? []);
|
|
1462
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
1463
|
+
const numericScope = {};
|
|
1464
|
+
const conditionHolds = (condition) => {
|
|
1465
|
+
const current = resolved.get(condition.param);
|
|
1466
|
+
const actual = current?.visible ? current.value : null;
|
|
1467
|
+
switch (condition.op) {
|
|
1468
|
+
case "eq":
|
|
1469
|
+
return actual === condition.value;
|
|
1470
|
+
case "neq":
|
|
1471
|
+
return actual !== condition.value;
|
|
1472
|
+
case "in":
|
|
1473
|
+
return Array.isArray(condition.value) && condition.value.includes(actual);
|
|
1474
|
+
case "gte":
|
|
1475
|
+
return typeof actual === "number" && actual >= condition.value;
|
|
1476
|
+
case "lte":
|
|
1477
|
+
return typeof actual === "number" && actual <= condition.value;
|
|
1478
|
+
}
|
|
1479
|
+
};
|
|
1480
|
+
for (const key of parameterResolutionOrder(structure)) {
|
|
1481
|
+
const parameter = parametersByKey.get(key);
|
|
1482
|
+
if (!parameter) {
|
|
1483
|
+
continue;
|
|
1484
|
+
}
|
|
1485
|
+
const rules = structure.rules.filter((rule) => rule.param === key);
|
|
1486
|
+
let visible = rules.filter((rule) => rule.kind === "visible").every((rule) => conditionHolds(rule.when));
|
|
1487
|
+
const chosen = selection.values[key];
|
|
1488
|
+
const path = ["values", key];
|
|
1489
|
+
const base = {
|
|
1490
|
+
key,
|
|
1491
|
+
kind: parameter.kind,
|
|
1492
|
+
label: parameter.label,
|
|
1493
|
+
tab: parameter.tab
|
|
1494
|
+
};
|
|
1495
|
+
if (parameter.kind === "number") {
|
|
1496
|
+
const range = value.numbers[key] ?? {
|
|
1497
|
+
min: parameter.min,
|
|
1498
|
+
max: parameter.max,
|
|
1499
|
+
default: parameter.min
|
|
1500
|
+
};
|
|
1501
|
+
let min = range.min;
|
|
1502
|
+
let max = range.max;
|
|
1503
|
+
for (const rule of rules) {
|
|
1504
|
+
if (rule.kind !== "limit") {
|
|
1505
|
+
continue;
|
|
1506
|
+
}
|
|
1507
|
+
const bound = evaluateFormulaSource(rule.formula, numericScope);
|
|
1508
|
+
if (!bound.ok) {
|
|
1509
|
+
issues.push(
|
|
1510
|
+
issue(
|
|
1511
|
+
["rules", structure.rules.indexOf(rule)],
|
|
1512
|
+
`configurator.${bound.code}`
|
|
1513
|
+
)
|
|
1514
|
+
);
|
|
1515
|
+
continue;
|
|
1516
|
+
}
|
|
1517
|
+
if (rule.bound === "min") {
|
|
1518
|
+
min = Math.max(min, bound.value);
|
|
1519
|
+
} else {
|
|
1520
|
+
max = Math.min(max, bound.value);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
if (min > max) {
|
|
1524
|
+
issues.push(issue(path, "configurator.emptyRange", { key, min, max }));
|
|
1525
|
+
max = min;
|
|
1526
|
+
}
|
|
1527
|
+
const fixedValue = presetSets.has(key) ? presetValues[key] : void 0;
|
|
1528
|
+
let current2 = clamp(range.default, min, max);
|
|
1529
|
+
if (fixedValue !== void 0) {
|
|
1530
|
+
current2 = fixedValue;
|
|
1531
|
+
} else if (visible && chosen !== void 0) {
|
|
1532
|
+
if (typeof chosen !== "number") {
|
|
1533
|
+
issues.push(issue(path, "configurator.expectedNumber", { key }));
|
|
1534
|
+
} else if (chosen < min || chosen > max) {
|
|
1535
|
+
issues.push(
|
|
1536
|
+
issue(path, "configurator.outOfRange", { key, min, max })
|
|
1537
|
+
);
|
|
1538
|
+
current2 = clamp(chosen, min, max);
|
|
1539
|
+
} else if (!onStep(chosen, parameter.step)) {
|
|
1540
|
+
issues.push(
|
|
1541
|
+
issue(path, "configurator.offStep", { key, step: parameter.step })
|
|
1542
|
+
);
|
|
1543
|
+
} else {
|
|
1544
|
+
current2 = chosen;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
numericScope[key] = current2;
|
|
1548
|
+
resolved.set(key, {
|
|
1549
|
+
...base,
|
|
1550
|
+
visible,
|
|
1551
|
+
value: current2,
|
|
1552
|
+
min,
|
|
1553
|
+
max,
|
|
1554
|
+
step: parameter.step,
|
|
1555
|
+
unit: parameter.unit,
|
|
1556
|
+
fixed: fixedValue !== void 0
|
|
1557
|
+
});
|
|
1558
|
+
continue;
|
|
1559
|
+
}
|
|
1560
|
+
if (parameter.kind === "toggle") {
|
|
1561
|
+
const setting = value.toggles[key] ?? { offered: true, default: false };
|
|
1562
|
+
visible = visible && setting.offered;
|
|
1563
|
+
let current2 = setting.default;
|
|
1564
|
+
if (visible && chosen !== void 0) {
|
|
1565
|
+
if (typeof chosen !== "boolean") {
|
|
1566
|
+
issues.push(issue(path, "configurator.expectedBoolean", { key }));
|
|
1567
|
+
} else {
|
|
1568
|
+
current2 = chosen;
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
const on = visible && current2;
|
|
1572
|
+
numericScope[key] = on ? 1 : 0;
|
|
1573
|
+
resolved.set(key, { ...base, visible, value: on });
|
|
1574
|
+
continue;
|
|
1575
|
+
}
|
|
1576
|
+
const excluded = new Set(
|
|
1577
|
+
rules.filter((rule) => rule.kind === "exclude" && conditionHolds(rule.when)).flatMap((rule) => rule.kind === "exclude" ? rule.codes : [])
|
|
1578
|
+
);
|
|
1579
|
+
let options;
|
|
1580
|
+
let preferred;
|
|
1581
|
+
if (parameter.kind === "choice") {
|
|
1582
|
+
const list = structure.choiceLists.find(
|
|
1583
|
+
(candidate) => candidate.key === parameter.list
|
|
1584
|
+
);
|
|
1585
|
+
const offer = value.choices[parameter.list];
|
|
1586
|
+
options = (list?.options ?? []).filter(
|
|
1587
|
+
(option) => offer?.options[option.code] !== void 0 && !excluded.has(option.code)
|
|
1588
|
+
).map((option) => ({
|
|
1589
|
+
code: option.code,
|
|
1590
|
+
label: option.label,
|
|
1591
|
+
priceCents: offer?.options[option.code]?.priceCents ?? null
|
|
1592
|
+
}));
|
|
1593
|
+
preferred = offer?.default;
|
|
1594
|
+
} else {
|
|
1595
|
+
const offer = value.materials[key];
|
|
1596
|
+
options = (offer?.offered ?? []).map((code) => materialsByCode.get(code)).filter(
|
|
1597
|
+
(material) => material !== void 0
|
|
1598
|
+
).filter((material) => !excluded.has(material.code)).map((material) => ({
|
|
1599
|
+
code: material.code,
|
|
1600
|
+
label: material.label,
|
|
1601
|
+
priceCents: material.priceGroup ? value.priceGroups[material.priceGroup] ?? null : 0,
|
|
1602
|
+
material
|
|
1603
|
+
}));
|
|
1604
|
+
preferred = offer?.default;
|
|
1605
|
+
}
|
|
1606
|
+
visible = visible && options.length > 0;
|
|
1607
|
+
let current = options.find((option) => option.code === preferred)?.code ?? options[0]?.code ?? null;
|
|
1608
|
+
if (visible && chosen !== void 0) {
|
|
1609
|
+
if (typeof chosen !== "string" || !options.some((option) => option.code === chosen)) {
|
|
1610
|
+
issues.push(
|
|
1611
|
+
issue(path, "configurator.optionNotOffered", {
|
|
1612
|
+
key,
|
|
1613
|
+
code: String(chosen)
|
|
1614
|
+
})
|
|
1615
|
+
);
|
|
1616
|
+
} else {
|
|
1617
|
+
current = chosen;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
resolved.set(key, {
|
|
1621
|
+
...base,
|
|
1622
|
+
visible,
|
|
1623
|
+
value: visible ? current : null,
|
|
1624
|
+
options
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
const parameters = structure.parameters.map((parameter) => resolved.get(parameter.key)).filter(
|
|
1628
|
+
(parameter) => parameter !== void 0
|
|
1629
|
+
);
|
|
1630
|
+
const inputs = {};
|
|
1631
|
+
for (const input of structure.geometry.inputs) {
|
|
1632
|
+
const parameter = resolved.get(input.binding.param);
|
|
1633
|
+
if (!parameter) {
|
|
1634
|
+
continue;
|
|
1635
|
+
}
|
|
1636
|
+
if (parameter.kind === "number") {
|
|
1637
|
+
inputs[input.name] = parameter.value;
|
|
1638
|
+
} else if (parameter.kind === "toggle") {
|
|
1639
|
+
inputs[input.name] = parameter.value === true ? 1 : 0;
|
|
1640
|
+
} else if (parameter.kind === "choice") {
|
|
1641
|
+
inputs[input.name] = typeof parameter.value === "string" ? input.binding.map?.[parameter.value] ?? 0 : 0;
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
const slots = {};
|
|
1645
|
+
for (const slot of structure.geometry.slots) {
|
|
1646
|
+
let material = null;
|
|
1647
|
+
if (slot.param) {
|
|
1648
|
+
const parameter = resolved.get(slot.param);
|
|
1649
|
+
material = parameter?.options?.find((option) => option.code === parameter.value)?.material ?? null;
|
|
1650
|
+
} else if (slot.material) {
|
|
1651
|
+
material = materialsByCode.get(slot.material) ?? null;
|
|
1652
|
+
}
|
|
1653
|
+
slots[slot.name] = {
|
|
1654
|
+
color: material?.color ?? slot.color ?? NEUTRAL_SLOT_COLOR,
|
|
1655
|
+
material
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
const normalised = { values: {} };
|
|
1659
|
+
for (const parameter of parameters) {
|
|
1660
|
+
if (parameter.visible && !parameter.fixed && parameter.value !== null) {
|
|
1661
|
+
normalised.values[parameter.key] = parameter.value;
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
if (preset) {
|
|
1665
|
+
normalised.preset = preset.code;
|
|
1666
|
+
}
|
|
1667
|
+
return {
|
|
1668
|
+
configuration: {
|
|
1669
|
+
parameters,
|
|
1670
|
+
byKey: Object.fromEntries(resolved),
|
|
1671
|
+
presets,
|
|
1672
|
+
preset,
|
|
1673
|
+
inputs,
|
|
1674
|
+
slots,
|
|
1675
|
+
selection: normalised
|
|
1676
|
+
},
|
|
1677
|
+
issues
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
function clamp(value, min, max) {
|
|
1681
|
+
return Math.min(max, Math.max(min, value));
|
|
1682
|
+
}
|
|
1683
|
+
function onStep(value, step) {
|
|
1684
|
+
const ratio = value / step;
|
|
1685
|
+
return Math.abs(ratio - Math.round(ratio)) < STEP_TOLERANCE;
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
// ../features/product-configurator/src/shared/expand-geometry.ts
|
|
1689
|
+
function expandGeometry(document, inputs) {
|
|
1690
|
+
const issues = [];
|
|
1691
|
+
const parts = [];
|
|
1692
|
+
const baseScope = { ...document.constants };
|
|
1693
|
+
for (const input of document.inputs) {
|
|
1694
|
+
const number = inputs[input.name];
|
|
1695
|
+
if (number === void 0 || !Number.isFinite(number)) {
|
|
1696
|
+
issues.push(
|
|
1697
|
+
issue(["inputs", input.name], "configurator.inputMissing", {
|
|
1698
|
+
name: input.name
|
|
1699
|
+
})
|
|
1700
|
+
);
|
|
1701
|
+
continue;
|
|
1702
|
+
}
|
|
1703
|
+
baseScope[input.name] = number;
|
|
1704
|
+
}
|
|
1705
|
+
if (issues.length > 0) {
|
|
1706
|
+
return { ok: false, issues };
|
|
1707
|
+
}
|
|
1708
|
+
const evaluate = (formula, scope, path) => {
|
|
1709
|
+
const result = evaluateFormulaSource(formula, scope);
|
|
1710
|
+
if (!result.ok) {
|
|
1711
|
+
issues.push(
|
|
1712
|
+
result.code === "formula_unknown_name" ? issue(path, "configurator.unknownName", { name: result.name }) : issue(path, `configurator.${result.code}`)
|
|
1713
|
+
);
|
|
1714
|
+
return null;
|
|
1715
|
+
}
|
|
1716
|
+
return result.value;
|
|
1717
|
+
};
|
|
1718
|
+
const walk = (nodes, scope, path, keySuffix, depth) => {
|
|
1719
|
+
for (const node of nodes) {
|
|
1720
|
+
if (issues.length > 0) {
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1723
|
+
const nodePath = [...path, node.id];
|
|
1724
|
+
if (node.visible !== void 0) {
|
|
1725
|
+
const visible = evaluate(node.visible, scope, [...nodePath, "visible"]);
|
|
1726
|
+
if (visible === null || visible <= 0) {
|
|
1727
|
+
continue;
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
if (node.type === "repeat") {
|
|
1731
|
+
if (depth + 1 > CONFIGURATOR_LIMITS.repeatDepth) {
|
|
1732
|
+
issues.push(
|
|
1733
|
+
issue(nodePath, "configurator.repeatTooDeep", {
|
|
1734
|
+
max: CONFIGURATOR_LIMITS.repeatDepth
|
|
1735
|
+
})
|
|
1736
|
+
);
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
const count = evaluate(node.count, scope, [...nodePath, "count"]);
|
|
1740
|
+
if (count === null) {
|
|
1741
|
+
return;
|
|
1742
|
+
}
|
|
1743
|
+
if (!Number.isInteger(count) || count < 0 || count > CONFIGURATOR_LIMITS.repeatCount) {
|
|
1744
|
+
issues.push(
|
|
1745
|
+
issue([...nodePath, "count"], "configurator.repeatCountInvalid", {
|
|
1746
|
+
count,
|
|
1747
|
+
max: CONFIGURATOR_LIMITS.repeatCount
|
|
1748
|
+
})
|
|
1749
|
+
);
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
const index = node.index ?? "i";
|
|
1753
|
+
for (let step = 0; step < count; step += 1) {
|
|
1754
|
+
walk(
|
|
1755
|
+
node.children,
|
|
1756
|
+
{ ...scope, [index]: step },
|
|
1757
|
+
[...nodePath, step],
|
|
1758
|
+
`${keySuffix}[${step}]`,
|
|
1759
|
+
depth + 1
|
|
1760
|
+
);
|
|
1761
|
+
}
|
|
1762
|
+
continue;
|
|
1763
|
+
}
|
|
1764
|
+
const size = node.size.map(
|
|
1765
|
+
(formula, axis) => evaluate(formula, scope, [...nodePath, "size", axis])
|
|
1766
|
+
);
|
|
1767
|
+
const at = node.at.map(
|
|
1768
|
+
(formula, axis) => evaluate(formula, scope, [...nodePath, "at", axis])
|
|
1769
|
+
);
|
|
1770
|
+
if (size.includes(null) || at.includes(null)) {
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
const negative = size.findIndex((measure) => measure < 0);
|
|
1774
|
+
if (negative >= 0) {
|
|
1775
|
+
issues.push(
|
|
1776
|
+
issue(
|
|
1777
|
+
[...nodePath, "size", negative],
|
|
1778
|
+
"configurator.negativeMeasure",
|
|
1779
|
+
{
|
|
1780
|
+
value: size[negative]
|
|
1781
|
+
}
|
|
1782
|
+
)
|
|
1783
|
+
);
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
const params = {};
|
|
1787
|
+
for (const [key, formula] of Object.entries(node.params ?? {})) {
|
|
1788
|
+
const number = evaluate(formula, scope, [...nodePath, "params", key]);
|
|
1789
|
+
if (number === null) {
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
params[key] = number;
|
|
1793
|
+
}
|
|
1794
|
+
parts.push({
|
|
1795
|
+
key: `${node.id}${keySuffix}`,
|
|
1796
|
+
id: node.id,
|
|
1797
|
+
type: node.type,
|
|
1798
|
+
slot: node.slot,
|
|
1799
|
+
size,
|
|
1800
|
+
at,
|
|
1801
|
+
params
|
|
1802
|
+
});
|
|
1803
|
+
if (parts.length > CONFIGURATOR_LIMITS.expandedParts) {
|
|
1804
|
+
issues.push(
|
|
1805
|
+
issue(["root"], "configurator.tooManyParts", {
|
|
1806
|
+
max: CONFIGURATOR_LIMITS.expandedParts
|
|
1807
|
+
})
|
|
1808
|
+
);
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
};
|
|
1813
|
+
walk(document.root, baseScope, ["root"], "", 0);
|
|
1814
|
+
const dimensions = [];
|
|
1815
|
+
for (const annotation of document.annotations) {
|
|
1816
|
+
const path = ["annotations", annotation.id];
|
|
1817
|
+
const from = evaluate(annotation.from, baseScope, [...path, "from"]);
|
|
1818
|
+
const to = evaluate(annotation.to, baseScope, [...path, "to"]);
|
|
1819
|
+
const at = evaluate(annotation.at, baseScope, [...path, "at"]);
|
|
1820
|
+
if (from === null || to === null || at === null) {
|
|
1821
|
+
continue;
|
|
1822
|
+
}
|
|
1823
|
+
dimensions.push({
|
|
1824
|
+
id: annotation.id,
|
|
1825
|
+
axis: annotation.axis,
|
|
1826
|
+
from,
|
|
1827
|
+
to,
|
|
1828
|
+
at,
|
|
1829
|
+
label: annotation.label ?? null,
|
|
1830
|
+
length: Math.abs(to - from)
|
|
1831
|
+
});
|
|
1832
|
+
}
|
|
1833
|
+
if (issues.length > 0) {
|
|
1834
|
+
return { ok: false, issues };
|
|
1835
|
+
}
|
|
1836
|
+
return { ok: true, geometry: { parts, dimensions, bounds: boundsOf(parts) } };
|
|
1837
|
+
}
|
|
1838
|
+
function boundsOf(parts) {
|
|
1839
|
+
if (parts.length === 0) {
|
|
1840
|
+
return { min: [0, 0, 0], max: [0, 0, 0] };
|
|
1841
|
+
}
|
|
1842
|
+
const min = [Infinity, Infinity, Infinity];
|
|
1843
|
+
const max = [-Infinity, -Infinity, -Infinity];
|
|
1844
|
+
for (const part of parts) {
|
|
1845
|
+
for (let axis = 0; axis < 3; axis += 1) {
|
|
1846
|
+
min[axis] = Math.min(min[axis], part.at[axis]);
|
|
1847
|
+
max[axis] = Math.max(
|
|
1848
|
+
max[axis],
|
|
1849
|
+
part.at[axis] + part.size[axis]
|
|
1850
|
+
);
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
return { min, max };
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
// src/configurator-tools.ts
|
|
1857
|
+
var CONFIGURATOR = 'A "configurator" field holds a 3D product configurator. Its structure has "geometry" (the shape document: "inputs" bound to parameters, "constants", material "slots", a "root" tree of building blocks and "annotations" for dimension lines), "parameters", "choiceLists", "materials", "priceGroups", "amounts", optional "presets", "rules" and "priceRules". Building blocks: box, rounded_top_box (params.radius), stadium_prism, arc_panel (params.thickness, params.mirror), led_strip, niche_panel (params.niche_x, niche_y, niche_width, niche_height) and repeat (count, children, index i or j). Every measure is millimetres; "at" is the minimum corner, x left to right, y bottom to top, z from the wall into the room; "size" is width, height, depth. A measure is a number or a formula with + - * / ( ) and min, max, floor, ceil, round, abs over the shape\u2019s inputs, constants and the repeat index. This server can check such a document but never save it: a reseller uploads it in the object builder.';
|
|
1858
|
+
var documentInput = z5.string().min(2).max(2e5).describe(
|
|
1859
|
+
'The JSON text of a whole configurator structure (it has "schemaVersion"), or of a shape document alone (it has "root").'
|
|
1860
|
+
);
|
|
1861
|
+
function registerConfiguratorTools(server, client) {
|
|
1862
|
+
server.registerTool(
|
|
1863
|
+
"check_configurator_shape",
|
|
1864
|
+
{
|
|
1865
|
+
title: "Check a configurator structure or shape document",
|
|
1866
|
+
description: "Checks a configurator structure or a shape document exactly as the object builder will when a reseller uploads it: the outline, every name a formula reads, bindings, cycles in the rules and the limits. A whole structure is also expanded at its smallest measures, so a part that would come out negative is named. Answers with every problem and where it is, or that the document is sound. " + CONFIGURATOR,
|
|
1867
|
+
inputSchema: { document: documentInput },
|
|
1868
|
+
annotations: { readOnlyHint: true, idempotentHint: true }
|
|
1869
|
+
},
|
|
1870
|
+
({ document }) => {
|
|
1871
|
+
const problems = checkDocument(document);
|
|
1872
|
+
return answer(
|
|
1873
|
+
problems.length === 0 ? "The document is sound. A reseller can upload it in the object builder." : [
|
|
1874
|
+
`${problems.length} problem(s):`,
|
|
1875
|
+
...problems.map(describeIssue)
|
|
1876
|
+
].join("\n"),
|
|
1877
|
+
{ problems }
|
|
1878
|
+
);
|
|
1879
|
+
}
|
|
1880
|
+
);
|
|
1881
|
+
server.registerTool(
|
|
1882
|
+
"read_configurator",
|
|
1883
|
+
{
|
|
1884
|
+
title: "Read the configurator of a product",
|
|
1885
|
+
description: "The configurator field of one product as its page receives it: the structure (texture paths already URLs), the model\u2019s value (ranges, offered options, amounts, presets), the tax a quote uses and the two hashes a request must send back. Useful to draft a new shape from an existing one. " + CONFIGURATOR,
|
|
1886
|
+
inputSchema: {
|
|
1887
|
+
slugOrId: z5.string().min(1).max(200).describe("The address (slug) or the id of the product."),
|
|
1888
|
+
field: z5.string().min(1).max(64).describe("The slug of the configurator field.")
|
|
1889
|
+
},
|
|
1890
|
+
annotations: { readOnlyHint: true, idempotentHint: true }
|
|
1891
|
+
},
|
|
1892
|
+
async ({ slugOrId: slugOrId2, field }) => {
|
|
1893
|
+
try {
|
|
1894
|
+
const product = await client.getProduct(slugOrId2);
|
|
1895
|
+
const value = product?.fields[field];
|
|
1896
|
+
if (!product) {
|
|
1897
|
+
return answer(`No product "${slugOrId2}" on this account.`, null);
|
|
1898
|
+
}
|
|
1899
|
+
if (!value || typeof value !== "object" || !("structure" in value)) {
|
|
1900
|
+
return answer(
|
|
1901
|
+
`The product has no public configurator under "${field}".`,
|
|
1902
|
+
null
|
|
1903
|
+
);
|
|
1904
|
+
}
|
|
1905
|
+
return answer(`The configurator "${field}" of ${product.name}.`, value);
|
|
1906
|
+
} catch (error) {
|
|
1907
|
+
return refusal(error);
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
);
|
|
1911
|
+
}
|
|
1912
|
+
function checkDocument(text) {
|
|
1913
|
+
let parsed;
|
|
1914
|
+
try {
|
|
1915
|
+
parsed = JSON.parse(text);
|
|
1916
|
+
} catch {
|
|
1917
|
+
return [{ path: [], code: "configurator.notJson" }];
|
|
1918
|
+
}
|
|
1919
|
+
if (isRecord(parsed) && "schemaVersion" in parsed) {
|
|
1920
|
+
const structure = ConfiguratorStructureSchema.safeParse(parsed);
|
|
1921
|
+
if (!structure.success) {
|
|
1922
|
+
return structure.error.issues.map(toIssue);
|
|
1923
|
+
}
|
|
1924
|
+
const { configuration } = resolveConfiguration({
|
|
1925
|
+
structure: structure.data,
|
|
1926
|
+
value: emptyConfiguratorValue(),
|
|
1927
|
+
selection: { values: {} }
|
|
1928
|
+
});
|
|
1929
|
+
const expanded = expandGeometry(
|
|
1930
|
+
structure.data.geometry,
|
|
1931
|
+
configuration.inputs
|
|
1932
|
+
);
|
|
1933
|
+
return expanded.ok ? [] : expanded.issues.map((problem) => ({
|
|
1934
|
+
...problem,
|
|
1935
|
+
path: ["geometry", ...problem.path]
|
|
1936
|
+
}));
|
|
1937
|
+
}
|
|
1938
|
+
const geometry = GeometryDocumentSchema.safeParse(parsed);
|
|
1939
|
+
if (!geometry.success) {
|
|
1940
|
+
return geometry.error.issues.map(toIssue);
|
|
1941
|
+
}
|
|
1942
|
+
return geometryDocumentProblems(geometry.data);
|
|
1943
|
+
}
|
|
1944
|
+
function toIssue(problem) {
|
|
1945
|
+
const params = problem.params;
|
|
1946
|
+
return {
|
|
1947
|
+
path: problem.path.map(
|
|
1948
|
+
(part) => typeof part === "number" ? part : String(part)
|
|
1949
|
+
),
|
|
1950
|
+
code: problem.message,
|
|
1951
|
+
...params ? { params } : {}
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
function describeIssue(problem) {
|
|
1955
|
+
const where = formatIssuePath(problem.path) || "(the document)";
|
|
1956
|
+
const params = problem.params ? ` (${Object.entries(problem.params).map(([key, value]) => `${key}: ${value}`).join(", ")})` : "";
|
|
1957
|
+
return `- ${where}: ${problem.code}${params}`;
|
|
1958
|
+
}
|
|
1959
|
+
function isRecord(value) {
|
|
1960
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
// src/placement-tools.ts
|
|
1964
|
+
import { z as z6 } from "zod";
|
|
110
1965
|
var REACT_HINT = "On a React or Next.js site, prefer the components in `@solumflow-app/crm-client/react` (CrmForm, CrmBooking, CrmChatWidget, CrmSiteTracking) over pasting HTML.";
|
|
111
1966
|
var page = {
|
|
112
|
-
cursor:
|
|
113
|
-
limit:
|
|
1967
|
+
cursor: z6.string().optional().describe("The nextCursor of the previous page; leave out for the first."),
|
|
1968
|
+
limit: z6.number().int().min(1).max(100).optional()
|
|
114
1969
|
};
|
|
115
|
-
var origin =
|
|
1970
|
+
var origin = z6.string().regex(/^https?:\/\/[a-zA-Z0-9.-]+(:[0-9]{1,5})?$/).describe("Scheme, host and optional port; no path: https://www.example.com");
|
|
116
1971
|
function registerPlacementTools(server, client) {
|
|
117
1972
|
server.registerTool(
|
|
118
1973
|
"list_forms",
|
|
@@ -140,7 +1995,7 @@ function registerPlacementTools(server, client) {
|
|
|
140
1995
|
"list_booking_pages",
|
|
141
1996
|
{
|
|
142
1997
|
title: "List booking pages",
|
|
143
|
-
description: "Every page a visitor can book on: the services overview, each category and service, and each appointment calendar. Each has a link
|
|
1998
|
+
description: "Every page a visitor can book on: the services overview, each category and service, and each appointment calendar. Each has a link and an embed snippet (an iframe plus a sizing script); a paid calendar embedded this way opens its payment in a tab of its own. " + REACT_HINT,
|
|
144
1999
|
inputSchema: {},
|
|
145
2000
|
annotations: { readOnlyHint: true, idempotentHint: true }
|
|
146
2001
|
},
|
|
@@ -185,13 +2040,13 @@ function registerPlacementTools(server, client) {
|
|
|
185
2040
|
title: "Make or change a chat widget",
|
|
186
2041
|
description: "Makes the chat widget at this address when there is none, or changes it. Sets its name, whether it is live, and which sites it may run on. How it looks and what it says is done in the app\u2019s builder. Sites are added and removed, never replaced. Careful: an empty list means ANY site, so adding the first origin narrows the widget to that site only, and removing an origin takes the chat off a site that may be live.",
|
|
187
2042
|
inputSchema: {
|
|
188
|
-
slug:
|
|
2043
|
+
slug: z6.string().regex(/^[a-z][a-z0-9-]*$/).max(100).describe(
|
|
189
2044
|
'The widget\u2019s address on this account, such as "website-chat".'
|
|
190
2045
|
),
|
|
191
|
-
name:
|
|
192
|
-
status:
|
|
193
|
-
addAllowedOrigins:
|
|
194
|
-
removeAllowedOrigins:
|
|
2046
|
+
name: z6.string().min(1).max(200).optional().describe("Required when the widget does not exist yet."),
|
|
2047
|
+
status: z6.enum(["draft", "published"]).optional().describe("Only a published widget answers on a site."),
|
|
2048
|
+
addAllowedOrigins: z6.array(origin).max(20).optional(),
|
|
2049
|
+
removeAllowedOrigins: z6.array(origin).max(20).optional()
|
|
195
2050
|
},
|
|
196
2051
|
annotations: {
|
|
197
2052
|
readOnlyHint: false,
|
|
@@ -239,9 +2094,9 @@ ${describeChatWidget(widget)}`,
|
|
|
239
2094
|
title: "Set up or change the tracking script",
|
|
240
2095
|
description: "Sets website tracking up when it is not there, adds or removes the hosts it counts visits from, and changes the consent it waits for. An empty list of hosts counts nothing, so add the host of every site the script is placed on. Hosts are added and removed, never replaced.",
|
|
241
2096
|
inputSchema: {
|
|
242
|
-
addAllowedHosts:
|
|
243
|
-
removeAllowedHosts:
|
|
244
|
-
consentSignal:
|
|
2097
|
+
addAllowedHosts: z6.array(z6.string()).max(20).optional().describe('Hosts such as "www.example.com" or "*.example.com".'),
|
|
2098
|
+
removeAllowedHosts: z6.array(z6.string()).max(20).optional(),
|
|
2099
|
+
consentSignal: z6.enum([
|
|
245
2100
|
"ad_storage",
|
|
246
2101
|
"analytics_storage",
|
|
247
2102
|
"personalization_storage",
|
|
@@ -282,7 +2137,7 @@ function describeBookingPage(page2) {
|
|
|
282
2137
|
return [
|
|
283
2138
|
`**${page2.name}** (${page2.kind}${page2.id ? `, id ${page2.id}` : ""})`,
|
|
284
2139
|
`Link: ${page2.url}`,
|
|
285
|
-
|
|
2140
|
+
codeBlock(page2.embed)
|
|
286
2141
|
].join("\n");
|
|
287
2142
|
}
|
|
288
2143
|
function describeChatWidget(widget) {
|
|
@@ -338,7 +2193,7 @@ async function main() {
|
|
|
338
2193
|
}
|
|
339
2194
|
const server = new McpServer({
|
|
340
2195
|
name: "crm-mcp-server",
|
|
341
|
-
version: "0.
|
|
2196
|
+
version: "0.3.0"
|
|
342
2197
|
});
|
|
343
2198
|
const client = createClient({
|
|
344
2199
|
apiKey: config.apiKey,
|
|
@@ -346,6 +2201,7 @@ async function main() {
|
|
|
346
2201
|
});
|
|
347
2202
|
registerPlacementTools(server, client);
|
|
348
2203
|
registerCatalogTools(server, client);
|
|
2204
|
+
registerConfiguratorTools(server, client);
|
|
349
2205
|
await server.connect(new StdioServerTransport());
|
|
350
2206
|
}
|
|
351
2207
|
main().catch((error) => {
|