@inwink-mcp/backoffice-extension-helpers 1.0.12 → 1.0.14
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/README.md +15 -1
- package/dist/cli.js +133 -34
- package/package.json +1 -1
- package/templates/vite-extension/AGENTS.md.tpl +2 -1
- package/templates/vite-extension/docs/inwink-api/README.md +2 -0
- package/templates/vite-extension/docs/inwink-api/delete.md +2 -0
- package/templates/vite-extension/docs/inwink-api/entities.md +7 -0
- package/templates/vite-extension/docs/inwink-api/import.md +33 -2
- package/templates/vite-extension/docs/inwink-api/update.md +2 -0
package/README.md
CHANGED
|
@@ -75,7 +75,21 @@ Contrôles effectués :
|
|
|
75
75
|
- `extension.manifest.json` présent, `name` (slug) et `version` (semver) valides ;
|
|
76
76
|
- `dist/` contient `index.html` + `extension.manifest.json` à la racine + `assets/` ;
|
|
77
77
|
- le zip contient `index.html` et `extension.manifest.json` à la racine, sans `node_modules/`, `src/` ni `dist/` ;
|
|
78
|
-
- avertissements : `base: './'` absent de `vite.config.ts`, usage de `BrowserRouter
|
|
78
|
+
- avertissements : `base: './'` absent de `vite.config.ts`, usage de `BrowserRouter`, `docs/inwink-api/` différent de la doc de cette version de la CLI.
|
|
79
|
+
|
|
80
|
+
### `update-docs`
|
|
81
|
+
|
|
82
|
+
Met à jour la doc API d'une extension **déjà générée** : `docs/inwink-api/` est remplacé en miroir par celui de cette version (fichiers ajoutés, modifiés, et supprimés s'ils ont disparu du template). Rien d'autre n'est touché, la commande peut être relancée sans risque.
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
npx @inwink-mcp/backoffice-extension-helpers@latest update-docs
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
| Option | Description |
|
|
89
|
+
|--------|-------------|
|
|
90
|
+
| `--dir <path>` | Dossier du projet (défaut : dossier courant) |
|
|
91
|
+
|
|
92
|
+
Ce dossier appartient à la CLI : une modification locale est écrasée au prochain `update-docs`. `AGENTS.md` et le code du squelette ne sont pas mis à jour.
|
|
79
93
|
|
|
80
94
|
## Templates
|
|
81
95
|
|
package/dist/cli.js
CHANGED
|
@@ -340,12 +340,70 @@ function bootstrap(opts) {
|
|
|
340
340
|
}
|
|
341
341
|
|
|
342
342
|
// src/commands/pack.ts
|
|
343
|
-
import
|
|
343
|
+
import path8 from "path";
|
|
344
344
|
|
|
345
345
|
// src/lib/verify.ts
|
|
346
|
+
import fs5 from "fs";
|
|
347
|
+
import path6 from "path";
|
|
348
|
+
import AdmZip from "adm-zip";
|
|
349
|
+
|
|
350
|
+
// src/lib/docs.ts
|
|
346
351
|
import fs4 from "fs";
|
|
347
352
|
import path5 from "path";
|
|
348
|
-
|
|
353
|
+
var DOCS_REL_DIR = "docs/inwink-api";
|
|
354
|
+
var DOCS_TEMPLATE = "vite-extension";
|
|
355
|
+
var UPDATE_DOCS_COMMAND = "npx @inwink-mcp/backoffice-extension-helpers@latest update-docs";
|
|
356
|
+
function templateDocsDir() {
|
|
357
|
+
return path5.join(templatesRoot, DOCS_TEMPLATE, DOCS_REL_DIR);
|
|
358
|
+
}
|
|
359
|
+
function listFiles(root, current = root) {
|
|
360
|
+
if (!fs4.existsSync(current)) return [];
|
|
361
|
+
const files = [];
|
|
362
|
+
for (const entry of fs4.readdirSync(current, { withFileTypes: true })) {
|
|
363
|
+
const full = path5.join(current, entry.name);
|
|
364
|
+
if (entry.isDirectory()) files.push(...listFiles(root, full));
|
|
365
|
+
else files.push(path5.relative(root, full).split(path5.sep).join("/"));
|
|
366
|
+
}
|
|
367
|
+
return files.sort();
|
|
368
|
+
}
|
|
369
|
+
function readNormalized(file) {
|
|
370
|
+
return fs4.readFileSync(file, "utf8").replaceAll("\r\n", "\n");
|
|
371
|
+
}
|
|
372
|
+
function diffDocs(projectDir) {
|
|
373
|
+
const src = templateDocsDir();
|
|
374
|
+
const dest = path5.join(projectDir, DOCS_REL_DIR);
|
|
375
|
+
const srcFiles = listFiles(src);
|
|
376
|
+
const destFiles = new Set(listFiles(dest));
|
|
377
|
+
const diff = { added: [], changed: [], removed: [] };
|
|
378
|
+
for (const rel of srcFiles) {
|
|
379
|
+
if (!destFiles.has(rel)) diff.added.push(rel);
|
|
380
|
+
else if (readNormalized(path5.join(src, rel)) !== readNormalized(path5.join(dest, rel))) {
|
|
381
|
+
diff.changed.push(rel);
|
|
382
|
+
}
|
|
383
|
+
destFiles.delete(rel);
|
|
384
|
+
}
|
|
385
|
+
diff.removed = [...destFiles];
|
|
386
|
+
return diff;
|
|
387
|
+
}
|
|
388
|
+
function isDocsUpToDate(diff) {
|
|
389
|
+
return diff.added.length + diff.changed.length + diff.removed.length === 0;
|
|
390
|
+
}
|
|
391
|
+
function syncDocs(projectDir) {
|
|
392
|
+
const diff = diffDocs(projectDir);
|
|
393
|
+
const src = templateDocsDir();
|
|
394
|
+
const dest = path5.join(projectDir, DOCS_REL_DIR);
|
|
395
|
+
for (const rel of [...diff.added, ...diff.changed]) {
|
|
396
|
+
const target = path5.join(dest, rel);
|
|
397
|
+
fs4.mkdirSync(path5.dirname(target), { recursive: true });
|
|
398
|
+
fs4.copyFileSync(path5.join(src, rel), target);
|
|
399
|
+
}
|
|
400
|
+
for (const rel of diff.removed) {
|
|
401
|
+
fs4.rmSync(path5.join(dest, rel));
|
|
402
|
+
}
|
|
403
|
+
return diff;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// src/lib/verify.ts
|
|
349
407
|
function verifyExtension(options) {
|
|
350
408
|
const { projectDir, requireDist = false } = options;
|
|
351
409
|
const errors = [];
|
|
@@ -366,40 +424,49 @@ function verifyExtension(options) {
|
|
|
366
424
|
} catch (err) {
|
|
367
425
|
errors.push(err.message);
|
|
368
426
|
}
|
|
369
|
-
const distDir =
|
|
370
|
-
if (!
|
|
427
|
+
const distDir = path6.join(projectDir, "dist");
|
|
428
|
+
if (!fs5.existsSync(distDir)) {
|
|
371
429
|
if (requireDist) {
|
|
372
430
|
errors.push("Dossier dist/ introuvable apr\xE8s le build.");
|
|
373
431
|
}
|
|
374
432
|
} else {
|
|
375
|
-
if (!
|
|
433
|
+
if (!fs5.existsSync(path6.join(distDir, "index.html"))) {
|
|
376
434
|
errors.push("dist/index.html manquant.");
|
|
377
435
|
}
|
|
378
|
-
if (!
|
|
436
|
+
if (!fs5.existsSync(path6.join(distDir, "extension.manifest.json"))) {
|
|
379
437
|
errors.push("dist/extension.manifest.json manquant (le plugin Vite doit le copier).");
|
|
380
438
|
}
|
|
381
|
-
const assetsDir =
|
|
382
|
-
if (!
|
|
439
|
+
const assetsDir = path6.join(distDir, "assets");
|
|
440
|
+
if (!fs5.existsSync(assetsDir) || !fs5.statSync(assetsDir).isDirectory()) {
|
|
383
441
|
warnings.push("dist/assets/ introuvable \u2014 build Vite incomplet ?");
|
|
384
442
|
}
|
|
385
443
|
}
|
|
386
444
|
const zipPath = resolveZipPath(options, projectDir, name, version);
|
|
387
|
-
if (zipPath &&
|
|
445
|
+
if (zipPath && fs5.existsSync(zipPath)) {
|
|
388
446
|
verifyZip(zipPath, errors);
|
|
389
447
|
} else if (options.zipPath) {
|
|
390
448
|
errors.push(`Zip introuvable : ${options.zipPath}`);
|
|
391
449
|
}
|
|
392
450
|
checkViteBase(projectDir, warnings);
|
|
393
451
|
checkRouter(projectDir, warnings);
|
|
452
|
+
checkDocs(projectDir, warnings);
|
|
394
453
|
return { errors, warnings };
|
|
395
454
|
}
|
|
455
|
+
function checkDocs(projectDir, warnings) {
|
|
456
|
+
const diff = diffDocs(projectDir);
|
|
457
|
+
if (isDocsUpToDate(diff)) return;
|
|
458
|
+
const count = diff.added.length + diff.changed.length + diff.removed.length;
|
|
459
|
+
warnings.push(
|
|
460
|
+
`${DOCS_REL_DIR}/ diff\xE8re de la doc API actuelle (${count} fichier(s)). Mettez-la \xE0 jour avec : ${UPDATE_DOCS_COMMAND}`
|
|
461
|
+
);
|
|
462
|
+
}
|
|
396
463
|
function resolveZipPath(options, projectDir, name, version) {
|
|
397
464
|
if (options.zipPath) {
|
|
398
|
-
return
|
|
465
|
+
return path6.isAbsolute(options.zipPath) ? options.zipPath : path6.join(projectDir, options.zipPath);
|
|
399
466
|
}
|
|
400
467
|
if (name && version) {
|
|
401
|
-
const candidate =
|
|
402
|
-
return
|
|
468
|
+
const candidate = path6.join(projectDir, `${name}-${version}.zip`);
|
|
469
|
+
return fs5.existsSync(candidate) ? candidate : void 0;
|
|
403
470
|
}
|
|
404
471
|
return void 0;
|
|
405
472
|
}
|
|
@@ -408,7 +475,7 @@ function verifyZip(zipPath, errors) {
|
|
|
408
475
|
try {
|
|
409
476
|
entries = new AdmZip(zipPath).getEntries();
|
|
410
477
|
} catch (err) {
|
|
411
|
-
errors.push(`Impossible de lire le zip ${
|
|
478
|
+
errors.push(`Impossible de lire le zip ${path6.basename(zipPath)} : ${err.message}`);
|
|
412
479
|
return;
|
|
413
480
|
}
|
|
414
481
|
const names = entries.map((e) => e.entryName.split("\\").join("/"));
|
|
@@ -429,60 +496,60 @@ function verifyZip(zipPath, errors) {
|
|
|
429
496
|
}
|
|
430
497
|
}
|
|
431
498
|
function checkViteBase(projectDir, warnings) {
|
|
432
|
-
const file =
|
|
433
|
-
if (!
|
|
434
|
-
const content =
|
|
499
|
+
const file = path6.join(projectDir, "vite.config.ts");
|
|
500
|
+
if (!fs5.existsSync(file)) return;
|
|
501
|
+
const content = fs5.readFileSync(file, "utf8");
|
|
435
502
|
if (!/base:\s*['"]\.\/['"]/.test(content)) {
|
|
436
503
|
warnings.push("vite.config.ts : `base: './'` introuvable \u2014 les assets risquent de casser apr\xE8s d\xE9ploiement.");
|
|
437
504
|
}
|
|
438
505
|
}
|
|
439
506
|
function checkRouter(projectDir, warnings) {
|
|
440
|
-
const srcDir =
|
|
441
|
-
if (!
|
|
507
|
+
const srcDir = path6.join(projectDir, "src");
|
|
508
|
+
if (!fs5.existsSync(srcDir)) return;
|
|
442
509
|
if (sourceUsesBrowserRouter(srcDir)) {
|
|
443
510
|
warnings.push("Usage de BrowserRouter d\xE9tect\xE9 \u2014 utilisez HashRouter pour l'h\xE9bergement versionn\xE9.");
|
|
444
511
|
}
|
|
445
512
|
}
|
|
446
513
|
function sourceUsesBrowserRouter(dir) {
|
|
447
|
-
for (const entry of
|
|
448
|
-
const full =
|
|
514
|
+
for (const entry of fs5.readdirSync(dir, { withFileTypes: true })) {
|
|
515
|
+
const full = path6.join(dir, entry.name);
|
|
449
516
|
if (entry.isDirectory()) {
|
|
450
517
|
if (sourceUsesBrowserRouter(full)) return true;
|
|
451
518
|
} else if (/\.(tsx?|jsx?)$/.test(entry.name)) {
|
|
452
|
-
if (/\bBrowserRouter\b/.test(
|
|
519
|
+
if (/\bBrowserRouter\b/.test(fs5.readFileSync(full, "utf8"))) return true;
|
|
453
520
|
}
|
|
454
521
|
}
|
|
455
522
|
return false;
|
|
456
523
|
}
|
|
457
524
|
|
|
458
525
|
// src/lib/zip.ts
|
|
459
|
-
import
|
|
460
|
-
import
|
|
526
|
+
import fs6 from "fs";
|
|
527
|
+
import path7 from "path";
|
|
461
528
|
import AdmZip2 from "adm-zip";
|
|
462
529
|
function createSourceZip(projectDir) {
|
|
463
530
|
const sourceZip = new AdmZip2();
|
|
464
|
-
const rootFiles =
|
|
531
|
+
const rootFiles = fs6.readdirSync(projectDir);
|
|
465
532
|
for (const file of rootFiles) {
|
|
466
533
|
if (file === "node_modules" || file === "dist" || file.endsWith(".zip")) {
|
|
467
534
|
continue;
|
|
468
535
|
}
|
|
469
|
-
const filePath =
|
|
470
|
-
const stat =
|
|
536
|
+
const filePath = path7.join(projectDir, file);
|
|
537
|
+
const stat = fs6.statSync(filePath);
|
|
471
538
|
if (stat.isDirectory()) {
|
|
472
539
|
sourceZip.addLocalFolder(filePath, file);
|
|
473
540
|
} else {
|
|
474
|
-
sourceZip.addFile(file,
|
|
541
|
+
sourceZip.addFile(file, fs6.readFileSync(filePath));
|
|
475
542
|
}
|
|
476
543
|
}
|
|
477
544
|
return sourceZip.toBuffer();
|
|
478
545
|
}
|
|
479
546
|
function zipDist(projectDir, name, version) {
|
|
480
|
-
const distDir =
|
|
481
|
-
if (!
|
|
547
|
+
const distDir = path7.join(projectDir, "dist");
|
|
548
|
+
if (!fs6.existsSync(distDir)) {
|
|
482
549
|
throw new Error("Dossier dist/ introuvable. Lancez le build avant de zipper.");
|
|
483
550
|
}
|
|
484
551
|
const zipFileName = `${name}-${version}.zip`;
|
|
485
|
-
const zipFilePath =
|
|
552
|
+
const zipFilePath = path7.join(projectDir, zipFileName);
|
|
486
553
|
const zip = new AdmZip2();
|
|
487
554
|
zip.addLocalFolder(distDir);
|
|
488
555
|
const sourceZipBuffer = createSourceZip(projectDir);
|
|
@@ -493,7 +560,7 @@ function zipDist(projectDir, name, version) {
|
|
|
493
560
|
|
|
494
561
|
// src/commands/pack.ts
|
|
495
562
|
function pack(opts) {
|
|
496
|
-
const projectDir =
|
|
563
|
+
const projectDir = path8.resolve(opts.dir ?? process.cwd());
|
|
497
564
|
let name;
|
|
498
565
|
let version;
|
|
499
566
|
try {
|
|
@@ -515,7 +582,7 @@ function pack(opts) {
|
|
|
515
582
|
error(err.message);
|
|
516
583
|
return 1;
|
|
517
584
|
}
|
|
518
|
-
success(`Zip cr\xE9\xE9 : ${
|
|
585
|
+
success(`Zip cr\xE9\xE9 : ${path8.basename(zipPath)}`);
|
|
519
586
|
info(`Version : ${version}`);
|
|
520
587
|
info("Mise \xE0 jour de la version avec la date du jour\u2026");
|
|
521
588
|
try {
|
|
@@ -531,10 +598,39 @@ function pack(opts) {
|
|
|
531
598
|
return ok ? 0 : 1;
|
|
532
599
|
}
|
|
533
600
|
|
|
601
|
+
// src/commands/update-docs.ts
|
|
602
|
+
import path9 from "path";
|
|
603
|
+
import pc4 from "picocolors";
|
|
604
|
+
function updateDocs(opts) {
|
|
605
|
+
const projectDir = path9.resolve(opts.dir ?? process.cwd());
|
|
606
|
+
try {
|
|
607
|
+
readManifest(projectDir);
|
|
608
|
+
} catch (err) {
|
|
609
|
+
error(`Projet cible invalide : ${err.message}`);
|
|
610
|
+
return 1;
|
|
611
|
+
}
|
|
612
|
+
let diff;
|
|
613
|
+
try {
|
|
614
|
+
diff = syncDocs(projectDir);
|
|
615
|
+
} catch (err) {
|
|
616
|
+
error(`\xC9chec de la mise \xE0 jour : ${err.message}`);
|
|
617
|
+
return 1;
|
|
618
|
+
}
|
|
619
|
+
if (isDocsUpToDate(diff)) {
|
|
620
|
+
success(`${DOCS_REL_DIR}/ est d\xE9j\xE0 \xE0 jour.`);
|
|
621
|
+
return 0;
|
|
622
|
+
}
|
|
623
|
+
success(`${DOCS_REL_DIR}/ mis \xE0 jour :`);
|
|
624
|
+
for (const file of diff.added) info(` ${pc4.green("+")} ${file}`);
|
|
625
|
+
for (const file of diff.changed) info(` ${pc4.dim("~")} ${file}`);
|
|
626
|
+
for (const file of diff.removed) info(` ${pc4.red("-")} ${file}`);
|
|
627
|
+
return 0;
|
|
628
|
+
}
|
|
629
|
+
|
|
534
630
|
// src/commands/verify.ts
|
|
535
|
-
import
|
|
631
|
+
import path10 from "path";
|
|
536
632
|
function verify(opts) {
|
|
537
|
-
const projectDir =
|
|
633
|
+
const projectDir = path10.resolve(opts.dir ?? process.cwd());
|
|
538
634
|
info("V\xE9rification de conformit\xE9\u2026");
|
|
539
635
|
const result = verifyExtension({ projectDir, zipPath: opts.zip });
|
|
540
636
|
const ok = printVerifyResult(result);
|
|
@@ -550,6 +646,9 @@ program.command("bootstrap").description("G\xE9n\xE8re le squelette d'une extens
|
|
|
550
646
|
program.command("apply").description("Applique un template par-dessus un projet existant (overlay).").argument("<template>", "template \xE0 appliquer (sous-dossier de templates/)").option("--dir <path>", "dossier du projet (d\xE9faut : dossier courant)").action((template, options) => {
|
|
551
647
|
process.exitCode = apply(template, options);
|
|
552
648
|
});
|
|
649
|
+
program.command("update-docs").description("Remplace la doc API (docs/inwink-api/) d'une extension existante par celle de cette version.").option("--dir <path>", "dossier du projet (d\xE9faut : dossier courant)").action((options) => {
|
|
650
|
+
process.exitCode = updateDocs(options);
|
|
651
|
+
});
|
|
553
652
|
program.command("package").description("Build + zip + v\xE9rification de conformit\xE9 de l'extension.").option("--dir <path>", "dossier du projet (d\xE9faut : dossier courant)").action((options) => {
|
|
554
653
|
process.exitCode = pack(options);
|
|
555
654
|
});
|
package/package.json
CHANGED
|
@@ -56,7 +56,7 @@ Avant d'écrire **tout** appel API, utilise les outils MCP inwink pour découvri
|
|
|
56
56
|
|
|
57
57
|
### Référence des patterns d'API (`docs/inwink-api/`)
|
|
58
58
|
|
|
59
|
-
Les outils MCP donnent les endpoints/champs **exacts** ; la doc statique dans `docs/inwink-api/` décrit les **patterns d'appel** (verbes, forme des payloads, langage de requête). Lis uniquement le fichier correspondant à ta tâche :
|
|
59
|
+
Les outils MCP donnent les endpoints/champs **exacts** ; la doc statique dans `docs/inwink-api/` décrit les **patterns d'appel** (verbes, forme des payloads, langage de requête). Ce dossier est une copie de la CLI helper, figée à la génération : mets-le à jour une fois par session de travail (pas avant chaque lecture) avec `npx @inwink-mcp/backoffice-extension-helpers@latest update-docs` (ne réécrit que `docs/inwink-api/`), et ne le modifie pas à la main. Lis uniquement le fichier correspondant à ta tâche :
|
|
60
60
|
|
|
61
61
|
| Tâche | Fichier |
|
|
62
62
|
|-------|---------|
|
|
@@ -223,6 +223,7 @@ Packaging et vérification via la CLI helper :
|
|
|
223
223
|
```powershell
|
|
224
224
|
npx @inwink-mcp/backoffice-extension-helpers@latest package # build + zip + vérification de conformité
|
|
225
225
|
npx @inwink-mcp/backoffice-extension-helpers@latest verify # vérification de conformité seule
|
|
226
|
+
npx @inwink-mcp/backoffice-extension-helpers@latest update-docs # met à jour docs/inwink-api/
|
|
226
227
|
```
|
|
227
228
|
|
|
228
229
|
## Conventions de code
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Reference for consuming the inwink APIs from the extension. Calls always go through the `useInwink*Api` hooks, which set the Bearer token and the base URL: the paths below are relative to that base.
|
|
4
4
|
|
|
5
|
+
> **Keep this folder current.** It is a snapshot of the helper CLI taken when the extension was generated, and it is never edited by hand. Refresh it once per working session, not before each read, with `npx @inwink-mcp/backoffice-extension-helpers@latest update-docs` (rewrites `docs/inwink-api/` only).
|
|
6
|
+
|
|
5
7
|
> **Dynamic discovery first.** This documentation describes call *patterns* (verbs, payload shapes, query language). The **exact endpoints, fields and entities** are discovered with the MCP tools (`inwink_mcp_find_api_endpoint`, `inwink_mcp_get_entity_description`) — never guess a route or a schema.
|
|
6
8
|
|
|
7
9
|
## Contents
|
|
@@ -20,4 +20,6 @@ POST {api}/{shardId}/{entity}/massdelete
|
|
|
20
20
|
|
|
21
21
|
Bulk DELETE, like `DELETE … WHERE` in SQL. Relies on a filter (see [query-language.md](./query-language.md#filters)).
|
|
22
22
|
|
|
23
|
+
Body `{ "filters": … }`. With `?direct=true`: runs in the call and returns `{ "deleted": n }`; without: returns `{ "taskId" }` (queued). Count first with the same filter (`count`).
|
|
24
|
+
|
|
23
25
|
> A deleted "With History" entity remains readable through `history/removed/query` (see [history.md](./history.md)).
|
|
@@ -30,3 +30,10 @@
|
|
|
30
30
|
| Massdelete | `POST {api}/{shardId}/{entity}/massdelete` | Bulk DELETE, like `DELETE … WHERE` in SQL — see [delete.md](./delete.md) |
|
|
31
31
|
| Export | | Returns entities as an Excel file |
|
|
32
32
|
| File | | File upload — see [files.md](./files.md) |
|
|
33
|
+
|
|
34
|
+
## Emails and push notifications (dedicated routes)
|
|
35
|
+
|
|
36
|
+
Email templates (`mailtemplate` on events, `communitymailtemplate` on communities) and scheduled email sends (`mailschedule` / `mailschedulecommunity`) are ordinary entities (create, update, delete, query). Sending and push notifications use dedicated routes. Filters (`expression`) have the same shape as `filters` in `query`. Sending reaches real people and cannot be undone: count first.
|
|
37
|
+
|
|
38
|
+
| Purpose | Pattern | Notes |
|
|
39
|
+
|
|
|
@@ -20,5 +20,36 @@ POST {api}/{shardId}/{entity}/import
|
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
- If an entity includes `id`, it is treated as an **update**.
|
|
23
|
-
- Without `id`, the `identifiers` map acts as a uniqueness constraint to decide create vs. update. Above, `mail` is used to match existing records.
|
|
24
|
-
- Much faster than individual create/edit calls.
|
|
23
|
+
- Without `id`, the `identifiers` map acts as a uniqueness constraint to decide create vs. update. Above, `mail` is used to match existing records (case-insensitive). Several matches → nothing is done for that row.
|
|
24
|
+
- Much faster than individual create/edit calls. Send batches of **200** rows (what the back-office does).
|
|
25
|
+
- `arrayMergeKeys` (optional, e.g. `["kinds"]`): array fields to **merge** with the existing values instead of replacing them.
|
|
26
|
+
- Synchronous: the response has one entry per input row, in the same order:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"entities": [
|
|
31
|
+
{ "entityId": "uuid", "modelErrors": [], "modelWarnings": [], "validationErrors": [] },
|
|
32
|
+
{ "entityIds": ["uuid1", "uuid2"], "modelErrors": [ { "property": "mail", "code": "required" } ] }
|
|
33
|
+
],
|
|
34
|
+
"exceptionMessage": null
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
A row without `entityId` was **not** written. `exceptionMessage` (top level or per row) reports a database error.
|
|
39
|
+
|
|
40
|
+
## Import preview (check without writing)
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
POST {api}/{shardId}/{entity}/import/preview
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Same body as the import (`selects` projects the matched record). Nothing is written. One entry per row:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{ "entities": [ { "entity": { "id": "uuid", "mail": "…" }, "entities": null, "modelErrors": [], "modelWarnings": [], "validationErrors": [] } ] }
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
- `entity` = the existing record that would be **updated**; `null` = the row would be **created**.
|
|
53
|
+
- `entities` with several items = ambiguous match (nothing would be done).
|
|
54
|
+
- Errors on fields the row did not send are only warnings on an update; on a create every mandatory field must be present.
|
|
55
|
+
- Run the preview, show the result to the user, then send the **same** body to `/import`.
|
|
@@ -33,4 +33,6 @@ POST {api}/{shardId}/{entity}/massupdate
|
|
|
33
33
|
|
|
34
34
|
Bulk EDIT, like `UPDATE … WHERE` in SQL. Relies on a filter (see [query-language.md](./query-language.md#filters)).
|
|
35
35
|
|
|
36
|
+
Body `{ "query": { "filters": … }, "input": { field: value } }`. With `?direct=true`: runs in the call and returns `{ "updated": n }`; without: returns `{ "taskId" }` (queued). Count first with the same filter (`count`).
|
|
37
|
+
|
|
36
38
|
> To update a batch of entities identified one by one, see also [import.md](./import.md).
|