@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.
- package/LICENSE +21 -0
- package/README.md +262 -0
- package/dist/_chunks/TransferPage-Bhp6vkZE.js +1 -0
- package/dist/_chunks/TransferPage-c8hdFQDQ.mjs +398 -0
- package/dist/admin/index.js +1 -0
- package/dist/admin/index.mjs +43 -0
- package/dist/admin/src/api.d.ts +114 -0
- package/dist/admin/src/components/ExportPanel.d.ts +7 -0
- package/dist/admin/src/components/ImportPanel.d.ts +15 -0
- package/dist/admin/src/components/ReportView.d.ts +6 -0
- package/dist/admin/src/index.d.ts +11 -0
- package/dist/admin/src/pages/TransferPage.d.ts +10 -0
- package/dist/admin/src/pluginId.d.ts +7 -0
- package/dist/server/index.js +6 -0
- package/dist/server/index.mjs +996 -0
- package/dist/server/src/config/index.d.ts +30 -0
- package/dist/server/src/controllers/index.d.ts +12 -0
- package/dist/server/src/controllers/transfer.d.ts +14 -0
- package/dist/server/src/index.d.ts +97 -0
- package/dist/server/src/policies/index.d.ts +5 -0
- package/dist/server/src/register.d.ts +12 -0
- package/dist/server/src/routes/admin.d.ts +12 -0
- package/dist/server/src/routes/index.d.ts +14 -0
- package/dist/server/src/services/csv.d.ts +59 -0
- package/dist/server/src/services/export.d.ts +24 -0
- package/dist/server/src/services/import.d.ts +78 -0
- package/dist/server/src/services/index.d.ts +54 -0
- package/dist/server/src/services/nested.d.ts +38 -0
- package/dist/server/src/services/parse.d.ts +25 -0
- package/dist/server/src/services/populate.d.ts +20 -0
- package/dist/server/src/services/schema.d.ts +81 -0
- package/dist/server/src/services/serialise.d.ts +14 -0
- package/dist/server/src/types.d.ts +145 -0
- package/dist/server/src/uids.d.ts +9 -0
- 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
|
+
};
|