@qkix/strapi-plugin-ferry 0.1.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.
Files changed (35) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +262 -0
  3. package/dist/_chunks/TransferPage-Bhp6vkZE.js +1 -0
  4. package/dist/_chunks/TransferPage-c8hdFQDQ.mjs +398 -0
  5. package/dist/admin/index.js +1 -0
  6. package/dist/admin/index.mjs +43 -0
  7. package/dist/admin/src/api.d.ts +114 -0
  8. package/dist/admin/src/components/ExportPanel.d.ts +7 -0
  9. package/dist/admin/src/components/ImportPanel.d.ts +15 -0
  10. package/dist/admin/src/components/ReportView.d.ts +6 -0
  11. package/dist/admin/src/index.d.ts +11 -0
  12. package/dist/admin/src/pages/TransferPage.d.ts +10 -0
  13. package/dist/admin/src/pluginId.d.ts +7 -0
  14. package/dist/server/index.js +6 -0
  15. package/dist/server/index.mjs +996 -0
  16. package/dist/server/src/config/index.d.ts +30 -0
  17. package/dist/server/src/controllers/index.d.ts +12 -0
  18. package/dist/server/src/controllers/transfer.d.ts +14 -0
  19. package/dist/server/src/index.d.ts +97 -0
  20. package/dist/server/src/policies/index.d.ts +5 -0
  21. package/dist/server/src/register.d.ts +12 -0
  22. package/dist/server/src/routes/admin.d.ts +12 -0
  23. package/dist/server/src/routes/index.d.ts +14 -0
  24. package/dist/server/src/services/csv.d.ts +59 -0
  25. package/dist/server/src/services/export.d.ts +24 -0
  26. package/dist/server/src/services/import.d.ts +78 -0
  27. package/dist/server/src/services/index.d.ts +54 -0
  28. package/dist/server/src/services/nested.d.ts +38 -0
  29. package/dist/server/src/services/parse.d.ts +25 -0
  30. package/dist/server/src/services/populate.d.ts +20 -0
  31. package/dist/server/src/services/schema.d.ts +81 -0
  32. package/dist/server/src/services/serialise.d.ts +14 -0
  33. package/dist/server/src/types.d.ts +145 -0
  34. package/dist/server/src/uids.d.ts +9 -0
  35. package/package.json +118 -0
@@ -0,0 +1,996 @@
1
+ import { readFile as B } from "node:fs/promises";
2
+ const J = {
3
+ default: {
4
+ /**
5
+ * Content types Ferry will not carry, whatever the UI asks for.
6
+ *
7
+ * For anything that must not leave the environment it is in - a table of
8
+ * customer records, an integration's bookkeeping. The picker hides them and
9
+ * the endpoints refuse them, so a hidden thing cannot be exported by
10
+ * guessing its uid.
11
+ *
12
+ * ferry: {
13
+ * enabled: true,
14
+ * config: { exclude: ['api::invoice.invoice'] },
15
+ * }
16
+ */
17
+ exclude: [],
18
+ /**
19
+ * The most documents one request will read out.
20
+ *
21
+ * An export holds its whole result in memory before writing it, so this is
22
+ * the honest limit rather than a policy: past it, use the filters.
23
+ */
24
+ maxExport: 1e4
25
+ },
26
+ validator(n) {
27
+ if (n.exclude !== void 0 && !Array.isArray(n.exclude))
28
+ throw new Error("[ferry] `exclude` must be an array of content-type uids.");
29
+ if (n.maxExport !== void 0 && (typeof n.maxExport != "number" || n.maxExport <= 0))
30
+ throw new Error("[ferry] `maxExport` must be a positive number.");
31
+ }
32
+ }, T = "\uFEFF", Z = [",", ";", " ", "|"], U = (n) => {
33
+ const d = n.replace(T, "").split(/\r?\n/, 1)[0] ?? "";
34
+ let t = ",", r = 0;
35
+ for (const s of Z) {
36
+ let o = 0, a = !1;
37
+ for (let e = 0; e < d.length; e += 1) {
38
+ const c = d[e];
39
+ if (c === '"') {
40
+ a = !a;
41
+ continue;
42
+ }
43
+ !a && c === s && (o += 1);
44
+ }
45
+ o > r && (t = s, r = o);
46
+ }
47
+ return t;
48
+ }, W = (n, d) => n.includes(d) || n.includes('"') || n.includes(`
49
+ `) || n.includes("\r") || n !== n.trim(), O = (n, d) => {
50
+ if (n == null) return "";
51
+ const t = typeof n == "string" ? n : n instanceof Date ? n.toISOString() : typeof n == "object" ? JSON.stringify(n) : String(n);
52
+ return W(t, d) ? `"${t.replace(/"/g, '""')}"` : t;
53
+ }, X = (n, d, t = {}) => {
54
+ const { delimiter: r = ",", bom: s = !0, newline: o = `\r
55
+ ` } = t, a = [d.map((e) => O(e, r)).join(r)];
56
+ for (const e of n)
57
+ a.push(d.map((c) => O(e[c], r)).join(r));
58
+ return (s ? T : "") + a.join(o) + o;
59
+ }, V = (n, d) => {
60
+ const t = [];
61
+ let r = [], s = "", o = !1, a = !1;
62
+ const e = () => {
63
+ r.push(s), s = "", a = !0;
64
+ }, c = () => {
65
+ e(), (r.length > 1 || r[0] !== "") && t.push(r), r = [], a = !1;
66
+ }, u = n.startsWith(T) ? n.slice(T.length) : n;
67
+ for (let l = 0; l < u.length; l += 1) {
68
+ const i = u[l];
69
+ if (o) {
70
+ if (i === '"') {
71
+ if (u[l + 1] === '"') {
72
+ s += '"', l += 1;
73
+ continue;
74
+ }
75
+ o = !1;
76
+ continue;
77
+ }
78
+ s += i;
79
+ continue;
80
+ }
81
+ if (i === '"' && s === "") {
82
+ o = !0;
83
+ continue;
84
+ }
85
+ if (i === d) {
86
+ e();
87
+ continue;
88
+ }
89
+ if (i === "\r") {
90
+ u[l + 1] === `
91
+ ` && (l += 1), c();
92
+ continue;
93
+ }
94
+ if (i === `
95
+ `) {
96
+ c();
97
+ continue;
98
+ }
99
+ s += i;
100
+ }
101
+ return (s !== "" || o || a || r.length > 0) && c(), t;
102
+ }, G = (n, d) => {
103
+ const t = U(n), r = V(n, t);
104
+ if (r.length === 0) return { columns: [], rows: [] };
105
+ const [s, ...o] = r, a = s.map((c) => c.trim()), e = o.map((c) => {
106
+ const u = {};
107
+ return a.forEach((l, i) => {
108
+ u[l] = c[i] ?? "";
109
+ }), u;
110
+ });
111
+ return { columns: a, rows: e };
112
+ }, H = (n, d) => {
113
+ const t = n.trim();
114
+ switch (d) {
115
+ case "integer":
116
+ case "biginteger": {
117
+ const r = Number(t);
118
+ return Number.isFinite(r) ? Math.trunc(r) : void 0;
119
+ }
120
+ case "decimal":
121
+ case "float": {
122
+ const r = Number(t);
123
+ return Number.isFinite(r) ? r : void 0;
124
+ }
125
+ case "boolean": {
126
+ const r = t.toLowerCase();
127
+ return ["true", "1", "yes"].includes(r) ? !0 : ["false", "0", "no"].includes(r) ? !1 : void 0;
128
+ }
129
+ case "json":
130
+ try {
131
+ return JSON.parse(t);
132
+ } catch {
133
+ return;
134
+ }
135
+ default:
136
+ return n;
137
+ }
138
+ }, x = (n) => typeof n == "object" && n !== null && !Array.isArray(n), K = (n, d, t) => d === "csv" ? Y(n, t) : Q(n), Q = (n) => {
139
+ let d;
140
+ try {
141
+ d = JSON.parse(n);
142
+ } catch (s) {
143
+ throw new Error(`[ferry] the file is not valid JSON: ${s.message}`, {
144
+ cause: s
145
+ });
146
+ }
147
+ const t = [];
148
+ if (Array.isArray(d))
149
+ return t.push("Read as a plain list of documents, with no header to check against."), { documents: d.filter(x), warnings: t };
150
+ if (!x(d))
151
+ throw new Error("[ferry] expected an object or an array of documents.");
152
+ const r = d;
153
+ if (r.ferry !== void 0 && r.ferry !== 1)
154
+ throw new Error(
155
+ `[ferry] this file was written by a newer format (version ${r.ferry}) than this plugin can read.`
156
+ );
157
+ if (!Array.isArray(r.documents))
158
+ throw new Error("[ferry] the file has no `documents` array.");
159
+ return {
160
+ contentType: r.contentType,
161
+ status: r.status,
162
+ locale: r.locale,
163
+ documents: r.documents.filter(x),
164
+ warnings: t
165
+ };
166
+ }, Y = (n, d) => {
167
+ const { columns: t, rows: r } = G(n), s = [], o = new Map(d.scalars.map((l) => [l.name, l.type])), a = new Set(d.relations.map((l) => l.name)), e = /* @__PURE__ */ new Set([
168
+ "documentId",
169
+ "locale",
170
+ ...o.keys(),
171
+ ...a,
172
+ ...d.media.map((l) => l.name)
173
+ ]), c = t.filter((l) => l && !e.has(l));
174
+ return c.length > 0 && s.push(`Ignored columns not in the schema: ${c.join(", ")}.`), t.includes("documentId") || s.push(
175
+ "No documentId column, so every row will be created as a new document rather than matched against an existing one."
176
+ ), { documents: r.map((l) => {
177
+ const i = {};
178
+ for (const m of t) {
179
+ if (!e.has(m)) continue;
180
+ const p = l[m] ?? "";
181
+ if (p === "") continue;
182
+ if (m === "documentId" || m === "locale") {
183
+ i[m] = p.trim();
184
+ continue;
185
+ }
186
+ if (a.has(m)) {
187
+ i[m] = p.split(",").map((g) => g.trim()).filter(Boolean);
188
+ continue;
189
+ }
190
+ const f = o.get(m), h = f ? H(p, f) : p;
191
+ h !== void 0 && (i[m] = h);
192
+ }
193
+ return i;
194
+ }), warnings: s };
195
+ }, S = "ferry", L = "plugin::upload.file", I = (n) => n.plugin(S), ee = async (n) => {
196
+ const d = n.request.body?.content;
197
+ if (typeof d == "string" && d.length > 0) return d;
198
+ const t = n.request.files ?? {}, r = t.file ?? Object.values(t)[0], s = Array.isArray(r) ? r[0] : r;
199
+ s || n.throw(400, "Attach a file, or post its content as `content`.");
200
+ const o = s.filepath ?? s.path;
201
+ return o || n.throw(400, "The upload arrived without a readable path."), B(o, "utf8");
202
+ }, ne = (n, d) => {
203
+ const t = n.request.body?.format;
204
+ return t === "csv" || t === "json" ? t : d?.toLowerCase().endsWith(".csv") ? "csv" : "json";
205
+ }, te = (n) => {
206
+ const d = n.request.files ?? {}, t = d.file ?? Object.values(d)[0], r = Array.isArray(t) ? t[0] : t;
207
+ return r?.originalFilename ?? r?.name;
208
+ }, E = (n, d) => n == null || n === "" ? d : typeof n == "boolean" ? n : String(n).toLowerCase() === "true", oe = ({ strapi: n }) => ({
209
+ /** The content types Ferry can carry, for the picker. */
210
+ async catalogue(d) {
211
+ d.body = { contentTypes: I(n).service("schema").catalogue() };
212
+ },
213
+ /** What would travel, and what would not, before anyone commits to it. */
214
+ async plan(d) {
215
+ const { uid: t } = d.params;
216
+ try {
217
+ d.body = I(n).service("schema").plan(t);
218
+ } catch (r) {
219
+ d.throw(404, r.message);
220
+ }
221
+ },
222
+ async export(d) {
223
+ const t = d.request.body ?? {};
224
+ t.uid || d.throw(400, "Name the content type to export as `uid`.");
225
+ const r = {
226
+ uid: t.uid,
227
+ format: t.format === "csv" ? "csv" : "json",
228
+ status: t.status === "published" ? "published" : "draft",
229
+ locale: t.locale || void 0,
230
+ documentIds: Array.isArray(t.documentIds) ? t.documentIds : void 0,
231
+ filters: t.filters,
232
+ sort: t.sort,
233
+ limit: t.limit ? Number(t.limit) : void 0,
234
+ relations: E(t.relations, !0),
235
+ media: E(t.media, !0)
236
+ };
237
+ try {
238
+ const s = await I(n).service("exporter").run(r);
239
+ if (E(t.envelope, !1)) {
240
+ d.body = {
241
+ filename: s.filename,
242
+ mime: s.mime,
243
+ body: s.body,
244
+ count: s.count,
245
+ warnings: s.warnings
246
+ };
247
+ return;
248
+ }
249
+ s.warnings.length > 0 && d.set("X-Ferry-Warnings", encodeURIComponent(JSON.stringify(s.warnings))), d.set("X-Ferry-Count", String(s.count)), d.set("Access-Control-Expose-Headers", "X-Ferry-Warnings, X-Ferry-Count"), d.attachment(s.filename), d.type = s.mime, d.body = s.body;
250
+ } catch (s) {
251
+ d.throw(400, s.message);
252
+ }
253
+ },
254
+ /** The dry run. Reads everything, writes nothing. */
255
+ async preview(d) {
256
+ await M(d, n, !1);
257
+ },
258
+ async apply(d) {
259
+ await M(d, n, !0);
260
+ }
261
+ }), M = async (n, d, t) => {
262
+ const r = n.request.body ?? {}, s = r.uid;
263
+ s || n.throw(400, "Name the content type to import into as `uid`.");
264
+ let o;
265
+ try {
266
+ o = I(d).service("schema").plan(s);
267
+ } catch (u) {
268
+ return n.throw(404, u.message);
269
+ }
270
+ const a = await ee(n), e = ne(n, te(n)), c = {
271
+ uid: s,
272
+ status: r.status === "published" ? "published" : "draft",
273
+ locale: r.locale || void 0,
274
+ onExisting: r.onExisting === "skip" ? "skip" : "update",
275
+ onMissingRelation: r.onMissingRelation === "fail" ? "fail" : "skip",
276
+ publish: E(r.publish, !0),
277
+ continueOnError: E(r.continueOnError, !1)
278
+ };
279
+ try {
280
+ const u = K(a, e, o), l = I(d).service("importer");
281
+ n.body = t ? await l.apply(c, u) : await l.preview(c, u);
282
+ } catch (u) {
283
+ n.throw(400, u.message);
284
+ }
285
+ }, se = {
286
+ transfer: oe
287
+ }, v = (n) => (d) => {
288
+ const t = d.state?.userAbility;
289
+ return t ? t.can(`plugin::${S}.${n}`) : !1;
290
+ }, re = {
291
+ canExport: v("export"),
292
+ canImport: v("import")
293
+ }, ie = ({ strapi: n }) => {
294
+ const d = [
295
+ {
296
+ section: "plugins",
297
+ displayName: "Export content",
298
+ uid: "export",
299
+ pluginName: S
300
+ },
301
+ {
302
+ section: "plugins",
303
+ displayName: "Import content",
304
+ uid: "import",
305
+ pluginName: S
306
+ }
307
+ ];
308
+ n.admin?.services?.permission?.actionProvider?.registerMany?.(d);
309
+ }, P = ["plugin::ferry.canExport"], N = ["plugin::ferry.canImport"], ae = {
310
+ type: "admin",
311
+ routes: [
312
+ {
313
+ method: "GET",
314
+ path: "/content-types",
315
+ handler: "transfer.catalogue",
316
+ config: { policies: P }
317
+ },
318
+ {
319
+ method: "GET",
320
+ path: "/plan/:uid",
321
+ handler: "transfer.plan",
322
+ config: { policies: P }
323
+ },
324
+ {
325
+ method: "POST",
326
+ path: "/export",
327
+ handler: "transfer.export",
328
+ config: { policies: P }
329
+ },
330
+ {
331
+ // The dry run is guarded as an import even though it writes nothing: it
332
+ // reads the file against the database and reports what is there.
333
+ method: "POST",
334
+ path: "/import/preview",
335
+ handler: "transfer.preview",
336
+ config: { policies: N }
337
+ },
338
+ {
339
+ method: "POST",
340
+ path: "/import",
341
+ handler: "transfer.apply",
342
+ config: { policies: N }
343
+ }
344
+ ]
345
+ }, ce = {
346
+ admin: ae
347
+ }, de = 10, le = ["name", "url", "hash", "ext", "mime", "size"], j = (n, d, t, r = 0) => {
348
+ const s = {};
349
+ if (r > de) return s;
350
+ const o = n.plugin("ferry").service("schema");
351
+ if (t.relations)
352
+ for (const a of d.relations)
353
+ s[a.name] = { fields: ["documentId"] };
354
+ if (t.media)
355
+ for (const a of d.media)
356
+ s[a.name] = { fields: le };
357
+ for (const a of d.components) {
358
+ const e = j(
359
+ n,
360
+ o.componentPlan(a.component),
361
+ t,
362
+ r + 1
363
+ );
364
+ s[a.name] = Object.keys(e).length > 0 ? { populate: e } : { populate: {} };
365
+ }
366
+ for (const a of d.dynamicZones) {
367
+ const e = {};
368
+ for (const c of a.components) {
369
+ const u = j(n, o.componentPlan(c), t, r + 1);
370
+ e[c] = Object.keys(u).length > 0 ? { populate: u } : {};
371
+ }
372
+ s[a.name] = { on: e };
373
+ }
374
+ return s;
375
+ }, z = (n) => n == null ? null : typeof n == "string" ? n : typeof n == "object" && "documentId" in n ? String(n.documentId) : null, ue = (n) => {
376
+ const d = () => n.plugin("ferry").service("schema"), t = (e, c, u) => {
377
+ if (e == null) return null;
378
+ const l = {};
379
+ for (const i of c.scalars)
380
+ i.name in e && (l[i.name] = e[i.name]);
381
+ for (const i of c.components) {
382
+ if (!(i.name in e)) continue;
383
+ const m = d().componentPlan(i.component);
384
+ l[i.name] = i.repeatable ? (e[i.name] ?? []).map(
385
+ (p) => t(p, m, u)
386
+ ) : t(e[i.name], m, u);
387
+ }
388
+ for (const i of c.dynamicZones)
389
+ i.name in e && (l[i.name] = r(e[i.name], u));
390
+ if (u.relations)
391
+ for (const i of c.relations)
392
+ i.name in e && (l[i.name] = s(e[i.name], i.many));
393
+ if (u.media)
394
+ for (const i of c.media)
395
+ i.name in e && (l[i.name] = o(e[i.name], i.many));
396
+ return l;
397
+ }, r = (e, c) => Array.isArray(e) ? e.map((u) => {
398
+ const l = u?.__component;
399
+ if (!l) return {};
400
+ const i = d().componentPlan(l);
401
+ return { __component: l, ...t(u, i, c) };
402
+ }) : [], s = (e, c) => c ? (Array.isArray(e) ? e : e ? [e] : []).map(z).filter((l) => l !== null).sort() : z(Array.isArray(e) ? e[0] : e), o = (e, c) => {
403
+ const u = (l) => l ? {
404
+ name: l.name ?? null,
405
+ url: l.url ?? null,
406
+ hash: l.hash ?? null,
407
+ ext: l.ext ?? null,
408
+ mime: l.mime ?? null,
409
+ size: l.size ?? null
410
+ } : null;
411
+ return c ? (Array.isArray(e) ? e : e ? [e] : []).map(u) : u(Array.isArray(e) ? e[0] : e);
412
+ };
413
+ return { document: (e, c, u) => {
414
+ const l = { documentId: e.documentId };
415
+ for (const i of c.scalars)
416
+ i.name in e && (l[i.name] = e[i.name]);
417
+ for (const i of c.components) {
418
+ if (!(i.name in e)) continue;
419
+ const m = d().componentPlan(i.component);
420
+ l[i.name] = i.repeatable ? (e[i.name] ?? []).map(
421
+ (p) => t(p, m, u)
422
+ ) : t(e[i.name], m, u);
423
+ }
424
+ for (const i of c.dynamicZones)
425
+ i.name in e && (l[i.name] = r(e[i.name], u));
426
+ if (u.relations)
427
+ for (const i of c.relations)
428
+ i.name in e && (l[i.name] = s(e[i.name], i.many));
429
+ if (u.media)
430
+ for (const i of c.media)
431
+ i.name in e && (l[i.name] = o(e[i.name], i.many));
432
+ return c.localized && e.locale && (l.locale = e.locale), l;
433
+ }, componentValue: t, zoneValue: r, relationValue: s, mediaValue: o };
434
+ }, me = {
435
+ format: "json",
436
+ status: "draft",
437
+ relations: !0,
438
+ media: !0
439
+ }, R = (n) => n.split(".").pop() ?? n, fe = (n, d) => [
440
+ "documentId",
441
+ ...n.localized ? ["locale"] : [],
442
+ ...n.scalars.map((t) => t.name),
443
+ ...d.relations ? n.relations.map((t) => t.name) : []
444
+ ], pe = (n, d) => {
445
+ const t = {};
446
+ for (const r of d) {
447
+ const s = n[r];
448
+ t[r] = Array.isArray(s) ? s.join(",") : s;
449
+ }
450
+ return t;
451
+ }, he = ({ strapi: n }) => {
452
+ const d = ue(n), t = () => n.plugin("ferry").service("schema"), r = {
453
+ /** Everything of one content type that matches, as a file. */
454
+ async run(s) {
455
+ const o = { ...me, ...s };
456
+ if (!t().allowed(o.uid))
457
+ throw new Error(`[ferry] ${o.uid} is not available for transfer.`);
458
+ const a = t().plan(o.uid), e = [], c = Number(n.plugin("ferry").config("maxExport")) || 1e4, l = { populate: j(n, a, {
459
+ relations: o.relations,
460
+ media: o.media
461
+ }) };
462
+ a.draftAndPublish && (l.status = o.status), o.locale && (l.locale = o.locale);
463
+ const i = { ...o.filters ?? {} };
464
+ o.documentIds?.length && (i.documentId = { $in: o.documentIds }), Object.keys(i).length > 0 && (l.filters = i), o.sort && (l.sort = o.sort);
465
+ const m = Math.min(o.limit ?? c, c);
466
+ l.limit = m;
467
+ const f = (a.kind === "singleType" ? await r.readSingle(o.uid, l) : await n.documents(o.uid).findMany(l)).map(
468
+ (g) => d.document(g, a, { relations: o.relations, media: o.media })
469
+ );
470
+ if (o.sort || f.sort(
471
+ (g, A) => g.documentId.localeCompare(A.documentId)
472
+ ), f.length >= m && e.push(
473
+ `Stopped at ${m} documents, which is this environment's export limit. There may be more - narrow the export with filters, or raise \`maxExport\` in the plugin config.`
474
+ ), a.media.length > 0 && o.media && e.push(
475
+ `Media fields (${a.media.map((g) => g.name).join(", ")}) travel as a reference, not as the file. The import reconnects them when the same upload already exists.`
476
+ ), o.format === "csv") {
477
+ const g = [
478
+ ...a.components.map((w) => w.name),
479
+ ...a.dynamicZones.map((w) => w.name),
480
+ ...o.media ? a.media.map((w) => w.name) : []
481
+ ];
482
+ g.length > 0 && e.push(
483
+ `CSV carries flat fields only, so ${g.join(", ")} ${g.length === 1 ? "is" : "are"} not in this file. Export as JSON to keep them.`
484
+ );
485
+ const A = fe(a, o);
486
+ return {
487
+ filename: `${R(o.uid)}.csv`,
488
+ mime: "text/csv; charset=utf-8",
489
+ body: X(
490
+ f.map((w) => pe(w, A)),
491
+ A
492
+ ),
493
+ count: f.length,
494
+ warnings: e
495
+ };
496
+ }
497
+ const h = {
498
+ ferry: 1,
499
+ contentType: o.uid,
500
+ status: o.status,
501
+ ...o.locale ? { locale: o.locale } : {},
502
+ count: f.length,
503
+ documents: f
504
+ };
505
+ return {
506
+ filename: `${R(o.uid)}.json`,
507
+ mime: "application/json",
508
+ body: `${JSON.stringify(h, null, 2)}
509
+ `,
510
+ count: f.length,
511
+ warnings: e
512
+ };
513
+ },
514
+ /**
515
+ * A single type is one document, or none at all before anyone has saved it.
516
+ * The document service still answers `findFirst`, so the only difference
517
+ * from a collection is the shape of the answer.
518
+ */
519
+ async readSingle(s, o) {
520
+ const a = await n.documents(s).findFirst(o);
521
+ return a ? [a] : [];
522
+ }
523
+ };
524
+ return r;
525
+ }, F = (n) => n == null ? [] : (Array.isArray(n) ? n : [n]).map(
526
+ (t) => typeof t == "string" ? t : t && typeof t == "object" && "documentId" in t ? String(t.documentId) : null
527
+ ).filter((t) => !!t), _ = (n, d, t, r, s) => {
528
+ for (const o of d.components) {
529
+ const a = n?.[o.name];
530
+ if (!a) continue;
531
+ const e = o.repeatable ? Array.isArray(a) ? a : [] : [a], c = t(o.component);
532
+ e.forEach((u, l) => {
533
+ if (!u || typeof u != "object") return;
534
+ const i = o.repeatable ? `${r}${o.name}[${l}].` : `${r}${o.name}.`;
535
+ s(u, c, i), _(u, c, t, i, s);
536
+ });
537
+ }
538
+ for (const o of d.dynamicZones) {
539
+ const a = n?.[o.name];
540
+ Array.isArray(a) && a.forEach((e, c) => {
541
+ if (!e?.__component) return;
542
+ const u = t(e.__component), l = `${r}${o.name}[${c}].`;
543
+ s(e, u, l), _(e, u, t, l, s);
544
+ });
545
+ }
546
+ }, C = (n, d, t) => {
547
+ const r = [], s = (o, a, e) => {
548
+ for (const c of a.relations) {
549
+ if (!(c.name in o)) continue;
550
+ const u = F(o[c.name]);
551
+ u.length !== 0 && r.push({ path: `${e}${c.name}`, target: c.target, keys: u });
552
+ }
553
+ };
554
+ return s(n, d, ""), _(n, d, t, "", s), r;
555
+ }, k = (n, d, t) => {
556
+ const r = {};
557
+ for (const [s, o] of Object.entries(n))
558
+ d.relations.some((a) => a.name === s) || (r[s] = o);
559
+ for (const s of d.components) {
560
+ if (!(s.name in r)) continue;
561
+ const o = t(s.component), a = r[s.name];
562
+ r[s.name] = s.repeatable ? (Array.isArray(a) ? a : []).map(
563
+ (e) => e && typeof e == "object" ? k(e, o, t) : e
564
+ ) : a && typeof a == "object" ? k(a, o, t) : a;
565
+ }
566
+ for (const s of d.dynamicZones) {
567
+ if (!(s.name in r)) continue;
568
+ const o = r[s.name];
569
+ r[s.name] = (Array.isArray(o) ? o : []).map(
570
+ (a) => a?.__component ? {
571
+ __component: a.__component,
572
+ ...k(a, t(a.__component), t)
573
+ } : a
574
+ );
575
+ }
576
+ return r;
577
+ }, $ = (n, d, t, r) => {
578
+ const s = { ...n };
579
+ for (const o of d.relations) {
580
+ if (!(o.name in s)) continue;
581
+ const a = F(s[o.name]).filter((e) => r(o.target, e));
582
+ o.many ? s[o.name] = a : s[o.name] = a[0] ?? null;
583
+ }
584
+ for (const o of d.components) {
585
+ if (!(o.name in s)) continue;
586
+ const a = t(o.component), e = s[o.name];
587
+ s[o.name] = o.repeatable ? (Array.isArray(e) ? e : []).map(
588
+ (c) => c && typeof c == "object" ? $(c, a, t, r) : c
589
+ ) : e && typeof e == "object" ? $(e, a, t, r) : e;
590
+ }
591
+ for (const o of d.dynamicZones) {
592
+ if (!(o.name in s)) continue;
593
+ const a = s[o.name];
594
+ s[o.name] = (Array.isArray(a) ? a : []).map(
595
+ (e) => e?.__component ? {
596
+ __component: e.__component,
597
+ ...$(e, t(e.__component), t, r)
598
+ } : e
599
+ );
600
+ }
601
+ return s;
602
+ }, ye = {
603
+ status: "draft",
604
+ onExisting: "update",
605
+ onMissingRelation: "skip",
606
+ publish: !0,
607
+ continueOnError: !1
608
+ }, ge = 10;
609
+ class D extends Error {
610
+ }
611
+ const we = ({ strapi: n }) => {
612
+ const d = () => n.plugin("ferry").service("schema"), t = (s) => d().componentPlan(s), r = {
613
+ /**
614
+ * A dry run: the whole resolution, none of the writes.
615
+ *
616
+ * Same code path as the real thing, so what it promises is what happens.
617
+ */
618
+ preview(s, o) {
619
+ return r.run(s, o, !1);
620
+ },
621
+ apply(s, o) {
622
+ return r.run(s, o, !0);
623
+ },
624
+ async run(s, o, a) {
625
+ const e = { ...ye, ...s };
626
+ if (!d().allowed(e.uid))
627
+ throw new Error(`[ferry] ${e.uid} is not available for transfer.`);
628
+ const c = d().plan(e.uid), u = [...o.warnings];
629
+ o.contentType && o.contentType !== e.uid && u.push(
630
+ `This file was exported from ${o.contentType} and is being imported into ${e.uid}.`
631
+ );
632
+ const l = o.documents.map((y, b) => ({
633
+ row: b + 1,
634
+ documentId: typeof y.documentId == "string" ? y.documentId : void 0,
635
+ data: y
636
+ })), i = {
637
+ contentType: e.uid,
638
+ applied: !1,
639
+ total: l.length,
640
+ created: 0,
641
+ updated: 0,
642
+ skipped: 0,
643
+ errored: 0,
644
+ rows: [],
645
+ unresolved: [],
646
+ warnings: u
647
+ };
648
+ if (l.length === 0)
649
+ return u.push("The file holds no documents."), i;
650
+ const m = await r.existingDocuments(c, e, l), p = await r.resolveRelations(c, e, l, i), f = await r.resolveMedia(c, l, i), h = new Set(
651
+ l.map((y) => y.documentId).filter(Boolean)
652
+ ), g = (y, b) => p.get(y)?.has(b) === !0 || y === e.uid && h.has(b), A = l.some(
653
+ (y) => C(y.data, c, t).length > 0
654
+ ), w = l.map((y) => ({
655
+ entry: y,
656
+ outcome: r.decide(y, c, m, e)
657
+ }));
658
+ for (const { entry: y, outcome: b } of w)
659
+ b === "create" ? i.created += 1 : b === "update" ? i.updated += 1 : i.skipped += 1, i.rows.push({
660
+ row: y.row,
661
+ documentId: y.documentId,
662
+ outcome: b,
663
+ ...b === "skip" ? { message: "already present, and existing rows are kept" } : {}
664
+ });
665
+ if (e.onMissingRelation === "fail" && i.unresolved.length > 0)
666
+ return u.push(
667
+ "Nothing was written: a relation points at a document that is not in the file and not in this environment."
668
+ ), i;
669
+ if (!a) return i;
670
+ try {
671
+ await n.db.transaction(async () => {
672
+ if (await r.write(w, c, e, { media: f, exists: g, hasRelations: A }, i), i.errored > 0 && !e.continueOnError)
673
+ throw new D();
674
+ }), i.applied = !0;
675
+ } catch (y) {
676
+ if (!(y instanceof D)) throw y;
677
+ i.applied = !1, u.push(
678
+ `Rolled back: ${i.errored} row(s) failed and the import is set to all or nothing.`
679
+ );
680
+ }
681
+ return i;
682
+ },
683
+ decide(s, o, a, e) {
684
+ return o.kind === "singleType" ? a.size > 0 ? "update" : "create" : !s.documentId || !a.has(s.documentId) ? "create" : e.onExisting === "skip" ? "skip" : "update";
685
+ },
686
+ /** Which of the documents in the file are already here. */
687
+ async existingDocuments(s, o, a) {
688
+ const e = /* @__PURE__ */ new Set(), c = { fields: ["documentId"] };
689
+ if (s.draftAndPublish && (c.status = "draft"), s.localized && o.locale && (c.locale = o.locale), s.kind === "singleType") {
690
+ const i = await n.documents(s.uid).findFirst(c);
691
+ return i && e.add(i.documentId), e;
692
+ }
693
+ const u = a.map((i) => i.documentId).filter(Boolean);
694
+ if (u.length === 0) return e;
695
+ const l = await n.documents(s.uid).findMany({ ...c, filters: { documentId: { $in: u } } });
696
+ for (const i of l) e.add(i.documentId);
697
+ return e;
698
+ },
699
+ /**
700
+ * Which relation targets are really there.
701
+ *
702
+ * One query per target content type rather than one per key. A file of a
703
+ * thousand articles all pointing at the same twenty authors asks about
704
+ * twenty authors once, not a thousand times - and the abandoned plugin's
705
+ * habit of resolving inside the row loop is why importing anything sizeable
706
+ * takes minutes.
707
+ */
708
+ async resolveRelations(s, o, a, e) {
709
+ const c = /* @__PURE__ */ new Map(), u = [];
710
+ for (const m of a)
711
+ for (const p of C(m.data, s, t)) {
712
+ u.push({ entry: m, ...p });
713
+ const f = c.get(p.target) ?? /* @__PURE__ */ new Set();
714
+ p.keys.forEach((h) => f.add(h)), c.set(p.target, f);
715
+ }
716
+ const l = /* @__PURE__ */ new Map();
717
+ for (const [m, p] of c) {
718
+ const f = d().model(m);
719
+ if (!f) {
720
+ e.warnings.push(`Relations point at ${m}, which does not exist here.`), l.set(m, /* @__PURE__ */ new Set());
721
+ continue;
722
+ }
723
+ const h = {
724
+ fields: ["documentId"],
725
+ filters: { documentId: { $in: [...p] } }
726
+ };
727
+ f.options?.draftAndPublish && (h.status = "draft");
728
+ const g = await n.documents(m).findMany(h);
729
+ l.set(m, new Set(g.map((A) => A.documentId)));
730
+ }
731
+ const i = new Set(
732
+ a.map((m) => m.documentId).filter(Boolean)
733
+ );
734
+ for (const m of u) {
735
+ const p = l.get(m.target) ?? /* @__PURE__ */ new Set(), f = m.keys.filter(
736
+ (h) => !p.has(h) && !(m.target === s.uid && i.has(h))
737
+ );
738
+ f.length !== 0 && e.unresolved.push({
739
+ row: m.entry.row,
740
+ documentId: m.entry.documentId,
741
+ field: m.path,
742
+ target: m.target,
743
+ keys: f.slice(0, ge)
744
+ });
745
+ }
746
+ return l;
747
+ },
748
+ /**
749
+ * Media, matched by hash.
750
+ *
751
+ * The hash is derived from the file's contents, so the same image uploaded
752
+ * to two environments matches even when the filenames and the ids differ.
753
+ * Ferry does not carry the bytes; when the upload is not there, the field is
754
+ * left unset and said so, rather than pointing at somebody else's file.
755
+ */
756
+ async resolveMedia(s, o, a) {
757
+ const e = /* @__PURE__ */ new Map();
758
+ if (s.media.length === 0) return e;
759
+ const c = /* @__PURE__ */ new Set();
760
+ for (const i of o)
761
+ for (const m of s.media) {
762
+ const p = i.data[m.name], f = Array.isArray(p) ? p : p ? [p] : [];
763
+ for (const h of f) {
764
+ const g = h?.hash;
765
+ typeof g == "string" && c.add(g);
766
+ }
767
+ }
768
+ if (c.size === 0) return e;
769
+ const u = await n.db.query(L).findMany({ select: ["id", "hash"], where: { hash: { $in: [...c] } } });
770
+ for (const i of u) e.set(i.hash, i.id);
771
+ const l = [...c].filter((i) => !e.has(i));
772
+ return l.length > 0 && a.warnings.push(
773
+ `${l.length} media file(s) referenced in this file are not in this Media Library, so those fields are left empty. Upload them first and import again to attach them.`
774
+ ), e;
775
+ },
776
+ /** The payload for the first pass: content, with nothing pointing outward. */
777
+ content(s, o, a) {
778
+ const e = k(s.data, o, t);
779
+ return delete e.documentId, delete e.locale, r.withMedia(e, o, a);
780
+ },
781
+ /** The payload for the second pass: the same content, now wired up. */
782
+ wiring(s, o, a, e) {
783
+ const c = $(s.data, o, t, e);
784
+ return delete c.documentId, delete c.locale, r.withMedia(c, o, a);
785
+ },
786
+ withMedia(s, o, a) {
787
+ for (const e of o.media) {
788
+ if (!(e.name in s)) continue;
789
+ const c = s[e.name], l = (Array.isArray(c) ? c : c ? [c] : []).map((i) => typeof i?.hash == "string" ? a.get(i.hash) : void 0).filter((i) => typeof i == "number");
790
+ l.length === 0 ? delete s[e.name] : s[e.name] = e.many ? l : l[0];
791
+ }
792
+ return s;
793
+ },
794
+ /**
795
+ * The writes, in two passes.
796
+ *
797
+ * Everything exists before anything points at it. There is no ordering of a
798
+ * single pass that works: two documents that refer to each other cannot both
799
+ * be written second.
800
+ */
801
+ async write(s, o, a, e, c) {
802
+ const u = [], l = o.localized && a.locale ? { locale: a.locale } : {}, i = (m, p) => {
803
+ const f = c.rows.find((h) => h.row === m);
804
+ f && (f.outcome === "create" && (c.created -= 1), f.outcome === "update" && (c.updated -= 1), f.outcome = "error", f.message = p), c.errored += 1;
805
+ };
806
+ for (const { entry: m, outcome: p } of s)
807
+ if (p !== "skip")
808
+ try {
809
+ const f = r.content(m, o, e.media);
810
+ if (p === "create") {
811
+ const h = await n.documents(o.uid).create({
812
+ data: { ...f, ...m.documentId ? { documentId: m.documentId } : {} },
813
+ ...l
814
+ });
815
+ u.push({ entry: m, documentId: h.documentId });
816
+ } else {
817
+ const h = m.documentId ?? await r.singleTypeId(o, a);
818
+ if (!h) throw new Error("no document to update");
819
+ await n.documents(o.uid).update({ documentId: h, data: f, ...l }), u.push({ entry: m, documentId: h });
820
+ }
821
+ } catch (f) {
822
+ i(m.row, f.message);
823
+ }
824
+ if (e.hasRelations)
825
+ for (const { entry: m, documentId: p } of u)
826
+ try {
827
+ await n.documents(o.uid).update({
828
+ documentId: p,
829
+ data: r.wiring(m, o, e.media, e.exists),
830
+ ...l
831
+ });
832
+ } catch (f) {
833
+ i(m.row, `relations: ${f.message}`);
834
+ }
835
+ if (a.publish && o.draftAndPublish && a.status === "published")
836
+ for (const { entry: m, documentId: p } of u)
837
+ try {
838
+ await n.documents(o.uid).publish({ documentId: p, ...l });
839
+ } catch (f) {
840
+ i(m.row, `publish: ${f.message}`);
841
+ }
842
+ },
843
+ async singleTypeId(s, o) {
844
+ const a = { fields: ["documentId"] };
845
+ return s.draftAndPublish && (a.status = "draft"), s.localized && o.locale && (a.locale = o.locale), (await n.documents(s.uid).findFirst(a))?.documentId;
846
+ }
847
+ };
848
+ return r;
849
+ }, be = /* @__PURE__ */ new Set([
850
+ "id",
851
+ "createdAt",
852
+ "updatedAt",
853
+ "publishedAt",
854
+ "createdBy",
855
+ "updatedBy",
856
+ "locale",
857
+ "localizations",
858
+ "strapi_stage",
859
+ "strapi_assignee"
860
+ ]), Ae = /* @__PURE__ */ new Set(["password"]), Ie = (n) => n.info?.displayName ?? n.info?.singularName ?? n.uid, Ee = (n) => n.type === "media" || n.type === "relation" && n.target === L, q = (n) => {
861
+ const d = [], t = [], r = [], s = [], o = [], a = [];
862
+ for (const [e, c] of Object.entries(n)) {
863
+ if (be.has(e)) {
864
+ a.push({ name: e, reason: "managed by Strapi" });
865
+ continue;
866
+ }
867
+ if (Ae.has(c.type)) {
868
+ a.push({ name: e, reason: `${c.type} fields are never exported` });
869
+ continue;
870
+ }
871
+ if (Ee(c)) {
872
+ o.push({
873
+ name: e,
874
+ many: c.multiple === !0 || (c.relation ?? "").endsWith("Many")
875
+ });
876
+ continue;
877
+ }
878
+ if (c.type === "relation") {
879
+ if (!c.target) {
880
+ a.push({ name: e, reason: "relation without a target" });
881
+ continue;
882
+ }
883
+ t.push({
884
+ name: e,
885
+ target: c.target,
886
+ many: (c.relation ?? "").endsWith("Many")
887
+ });
888
+ continue;
889
+ }
890
+ if (c.type === "component") {
891
+ if (!c.component) {
892
+ a.push({ name: e, reason: "component without a target" });
893
+ continue;
894
+ }
895
+ r.push({
896
+ name: e,
897
+ component: c.component,
898
+ repeatable: c.repeatable === !0
899
+ });
900
+ continue;
901
+ }
902
+ if (c.type === "dynamiczone") {
903
+ s.push({ name: e, components: c.components ?? [] });
904
+ continue;
905
+ }
906
+ d.push({ name: e, type: c.type });
907
+ }
908
+ return { scalars: d, relations: t, components: r, dynamicZones: s, media: o, skipped: a };
909
+ }, Se = ({ strapi: n }) => {
910
+ const d = {
911
+ model(t) {
912
+ return n.contentTypes[t];
913
+ },
914
+ component(t) {
915
+ return n.components?.[t];
916
+ },
917
+ /**
918
+ * Sort one content type's attributes into how each has to be carried.
919
+ *
920
+ * Everything downstream - the populate call, the CSV header, the two import
921
+ * passes - reads this and nothing else, so there is exactly one place that
922
+ * decides what a field is.
923
+ */
924
+ plan(t) {
925
+ const r = d.model(t);
926
+ if (!r) throw new Error(`[ferry] unknown content type: ${t}`);
927
+ return {
928
+ uid: t,
929
+ kind: r.kind === "singleType" ? "singleType" : "collectionType",
930
+ draftAndPublish: r.options?.draftAndPublish === !0,
931
+ localized: r.pluginOptions?.i18n?.localized === !0,
932
+ ...q(r.attributes ?? {})
933
+ };
934
+ },
935
+ /**
936
+ * The same sorting for a component, which needs it too: components nest,
937
+ * and a component three levels down still has relations to resolve.
938
+ */
939
+ componentPlan(t) {
940
+ const r = d.component(t);
941
+ if (!r) throw new Error(`[ferry] unknown component: ${t}`);
942
+ return {
943
+ uid: t,
944
+ kind: "collectionType",
945
+ draftAndPublish: !1,
946
+ localized: !1,
947
+ ...q(r.attributes ?? {})
948
+ };
949
+ },
950
+ /** Uids the project has put out of bounds in its plugin config. */
951
+ excluded() {
952
+ const t = n.plugin(S)?.config("exclude");
953
+ return new Set(Array.isArray(t) ? t : []);
954
+ },
955
+ /**
956
+ * Whether Ferry may touch this content type at all.
957
+ *
958
+ * Checked by the endpoints as well as the picker. A hidden thing that can
959
+ * still be fetched by typing its uid is not hidden, and the reason someone
960
+ * excludes a content type is usually that its contents must not leave.
961
+ */
962
+ allowed(t) {
963
+ return t.startsWith("api::") && !d.excluded().has(t);
964
+ },
965
+ /**
966
+ * What Ferry will carry, for the picker in the admin panel.
967
+ *
968
+ * Only the project's own types. Strapi's internal tables are real content
969
+ * types, but exporting the admin user table or the upload file index moves
970
+ * infrastructure rather than content, and importing it into another
971
+ * environment breaks that environment.
972
+ */
973
+ catalogue() {
974
+ return Object.values(n.contentTypes).filter((t) => d.allowed(t.uid)).map((t) => ({
975
+ uid: t.uid,
976
+ label: Ie(t),
977
+ kind: t.kind === "singleType" ? "singleType" : "collectionType"
978
+ })).sort((t, r) => t.label.localeCompare(r.label));
979
+ }
980
+ };
981
+ return d;
982
+ }, ke = {
983
+ schema: Se,
984
+ exporter: he,
985
+ importer: we
986
+ }, xe = {
987
+ register: ie,
988
+ config: J,
989
+ controllers: se,
990
+ policies: re,
991
+ routes: ce,
992
+ services: ke
993
+ };
994
+ export {
995
+ xe as default
996
+ };