@legenki/print2medusa 0.8.1 → 0.9.2
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/.medusa/server/src/admin/index.js +358 -2
- package/.medusa/server/src/admin/index.mjs +361 -5
- package/.medusa/server/src/api/admin/printful/bundles/route.js +79 -0
- package/.medusa/server/src/api/admin/printful/design/route.js +38 -0
- package/.medusa/server/src/api/admin/printful/health/route.js +47 -0
- package/.medusa/server/src/api/admin/printful/history/route.js +51 -0
- package/.medusa/server/src/api/admin/printful/prompts/route.js +55 -0
- package/.medusa/server/src/api/admin/printful/statistics/route.js +59 -0
- package/.medusa/server/src/api/admin/printful/sync/clear/route.js +93 -0
- package/.medusa/server/src/modules/printful/service.js +39 -1
- package/.medusa/server/src/utils/bundle.js +243 -0
- package/.medusa/server/src/utils/clear-sync.js +91 -0
- package/.medusa/server/src/utils/design.js +438 -0
- package/.medusa/server/src/utils/printful-client.js +45 -1
- package/.medusa/server/src/utils/prompt.js +423 -0
- package/.medusa/server/src/utils/webhook-health.js +73 -0
- package/.medusa/server/src/workflows/apply-order-status.js +16 -5
- package/.medusa/server/src/workflows/create-printful-order.js +20 -3
- package/.medusa/server/src/workflows/sync-products.js +68 -1
- package/README.md +121 -0
- package/package.json +1 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { defineWidgetConfig } from "@medusajs/admin-sdk";
|
|
3
|
-
import { Container, Heading, Badge, Text, Button, toast } from "@medusajs/ui";
|
|
2
|
+
import { defineWidgetConfig, defineRouteConfig } from "@medusajs/admin-sdk";
|
|
3
|
+
import { Container, Heading, Badge, Text, Button, toast, Prompt, Select, Input, Copy, Table } from "@medusajs/ui";
|
|
4
4
|
import { defaultCurrencies } from "@medusajs/framework/utils";
|
|
5
|
-
import { useState, useEffect } from "react";
|
|
5
|
+
import { useState, useEffect, useCallback } from "react";
|
|
6
6
|
function minorUnitFactor(currencyCode) {
|
|
7
7
|
return isZeroDecimalCurrency(currencyCode) ? 1 : 100;
|
|
8
8
|
}
|
|
@@ -306,6 +306,347 @@ const PrintfulSyncWidget = () => {
|
|
|
306
306
|
defineWidgetConfig({
|
|
307
307
|
zone: "product.list.before"
|
|
308
308
|
});
|
|
309
|
+
const CLASS_LABEL = {
|
|
310
|
+
apparel: "Apparel",
|
|
311
|
+
embroidery: "Embroidered",
|
|
312
|
+
print_media: "Print media"
|
|
313
|
+
};
|
|
314
|
+
const CLASS_COLOUR = {
|
|
315
|
+
apparel: "blue",
|
|
316
|
+
embroidery: "orange",
|
|
317
|
+
print_media: "green"
|
|
318
|
+
};
|
|
319
|
+
const STATUS_COLOUR = {
|
|
320
|
+
success: "green",
|
|
321
|
+
running: "orange",
|
|
322
|
+
partial: "orange",
|
|
323
|
+
failed: "red"
|
|
324
|
+
};
|
|
325
|
+
const looksStuck = (row, staleMinutes) => {
|
|
326
|
+
if (!row || row.status !== "running") {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
const beat = row.heartbeat_at ?? row.started_at;
|
|
330
|
+
if (!beat) {
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
return Date.now() - new Date(beat).getTime() > staleMinutes * 6e4;
|
|
334
|
+
};
|
|
335
|
+
const minutesSince = (iso) => {
|
|
336
|
+
if (!iso) {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
return Math.floor((Date.now() - new Date(iso).getTime()) / 6e4);
|
|
340
|
+
};
|
|
341
|
+
const PrintfulPage = () => {
|
|
342
|
+
var _a, _b, _c;
|
|
343
|
+
const [syncs, setSyncs] = useState([]);
|
|
344
|
+
const [health, setHealth] = useState(null);
|
|
345
|
+
const [stats, setStats] = useState(null);
|
|
346
|
+
const [designs, setDesigns] = useState([]);
|
|
347
|
+
const [bundles, setBundles] = useState([]);
|
|
348
|
+
const [promptProducts, setPromptProducts] = useState([]);
|
|
349
|
+
const [styles, setStyles] = useState([]);
|
|
350
|
+
const [style, setStyle] = useState("editorial");
|
|
351
|
+
const [artwork, setArtwork] = useState("");
|
|
352
|
+
const [loading, setLoading] = useState(true);
|
|
353
|
+
const [clearing, setClearing] = useState(false);
|
|
354
|
+
const load = useCallback(async () => {
|
|
355
|
+
var _a2;
|
|
356
|
+
const [h, w, s, d, b] = await Promise.allSettled([
|
|
357
|
+
fetch("/admin/printful/history", { credentials: "include" }),
|
|
358
|
+
fetch("/admin/printful/health", { credentials: "include" }),
|
|
359
|
+
fetch("/admin/printful/statistics", { credentials: "include" }),
|
|
360
|
+
fetch("/admin/printful/design", { credentials: "include" }),
|
|
361
|
+
fetch("/admin/printful/bundles", { credentials: "include" })
|
|
362
|
+
]);
|
|
363
|
+
if (h.status === "fulfilled" && h.value.ok) {
|
|
364
|
+
const body = await h.value.json();
|
|
365
|
+
setSyncs(body.syncs ?? []);
|
|
366
|
+
}
|
|
367
|
+
if (w.status === "fulfilled" && w.value.ok) {
|
|
368
|
+
setHealth(await w.value.json());
|
|
369
|
+
}
|
|
370
|
+
if (s.status === "fulfilled" && s.value.ok) {
|
|
371
|
+
const body = await s.value.json();
|
|
372
|
+
setStats(((_a2 = body.statistics) == null ? void 0 : _a2[0]) ?? null);
|
|
373
|
+
}
|
|
374
|
+
if (d.status === "fulfilled" && d.value.ok) {
|
|
375
|
+
const body = await d.value.json();
|
|
376
|
+
setDesigns(body.designs ?? []);
|
|
377
|
+
}
|
|
378
|
+
if (b.status === "fulfilled" && b.value.ok) {
|
|
379
|
+
const body = await b.value.json();
|
|
380
|
+
setBundles(body.bundles ?? []);
|
|
381
|
+
}
|
|
382
|
+
setLoading(false);
|
|
383
|
+
}, []);
|
|
384
|
+
const loadPrompts = useCallback(async () => {
|
|
385
|
+
const params = new URLSearchParams({ style });
|
|
386
|
+
if (artwork.trim()) {
|
|
387
|
+
params.set("artwork", artwork.trim());
|
|
388
|
+
}
|
|
389
|
+
try {
|
|
390
|
+
const res = await fetch(`/admin/printful/prompts?${params}`, {
|
|
391
|
+
credentials: "include"
|
|
392
|
+
});
|
|
393
|
+
if (!res.ok) {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
const body = await res.json();
|
|
397
|
+
setPromptProducts(body.products ?? []);
|
|
398
|
+
setStyles(body.styles ?? []);
|
|
399
|
+
} catch {
|
|
400
|
+
}
|
|
401
|
+
}, [style, artwork]);
|
|
402
|
+
useEffect(() => {
|
|
403
|
+
void load();
|
|
404
|
+
}, [load]);
|
|
405
|
+
useEffect(() => {
|
|
406
|
+
void loadPrompts();
|
|
407
|
+
}, [loadPrompts]);
|
|
408
|
+
const latest = syncs[0];
|
|
409
|
+
const running = (latest == null ? void 0 : latest.status) === "running";
|
|
410
|
+
useEffect(() => {
|
|
411
|
+
if (!running) {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
const t = setInterval(() => void load(), 3e3);
|
|
415
|
+
return () => clearInterval(t);
|
|
416
|
+
}, [running, load]);
|
|
417
|
+
const clearStuck = async () => {
|
|
418
|
+
setClearing(true);
|
|
419
|
+
try {
|
|
420
|
+
const res = await fetch("/admin/printful/sync/clear", {
|
|
421
|
+
method: "POST",
|
|
422
|
+
credentials: "include",
|
|
423
|
+
headers: { "content-type": "application/json" },
|
|
424
|
+
body: JSON.stringify({ confirm: "clear-stuck-sync" })
|
|
425
|
+
});
|
|
426
|
+
const body = await res.json().catch(() => ({}));
|
|
427
|
+
if (res.ok) {
|
|
428
|
+
toast.success("Cleared the stuck sync", {
|
|
429
|
+
description: body.error_message
|
|
430
|
+
});
|
|
431
|
+
} else if (res.status === 409) {
|
|
432
|
+
toast.info("The sync is still alive", {
|
|
433
|
+
description: "It checked in while you were looking. Nothing cleared."
|
|
434
|
+
});
|
|
435
|
+
} else {
|
|
436
|
+
toast.error("Could not clear the sync", { description: body.message });
|
|
437
|
+
}
|
|
438
|
+
await load();
|
|
439
|
+
} catch (err) {
|
|
440
|
+
toast.error("Could not clear the sync", {
|
|
441
|
+
description: err instanceof Error ? err.message : String(err)
|
|
442
|
+
});
|
|
443
|
+
} finally {
|
|
444
|
+
setClearing(false);
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
const stuck = looksStuck(latest, (health == null ? void 0 : health.window) ?? 60);
|
|
448
|
+
const silentFor = minutesSince((latest == null ? void 0 : latest.heartbeat_at) ?? (latest == null ? void 0 : latest.started_at));
|
|
449
|
+
return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
|
|
450
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
|
|
451
|
+
/* @__PURE__ */ jsx(Heading, { level: "h1", children: "Printful" }),
|
|
452
|
+
/* @__PURE__ */ jsx(
|
|
453
|
+
Button,
|
|
454
|
+
{
|
|
455
|
+
size: "small",
|
|
456
|
+
variant: "secondary",
|
|
457
|
+
onClick: () => void load(),
|
|
458
|
+
disabled: loading,
|
|
459
|
+
children: "Refresh"
|
|
460
|
+
}
|
|
461
|
+
)
|
|
462
|
+
] }),
|
|
463
|
+
stuck && /* @__PURE__ */ jsxs("div", { className: "px-6 py-4 flex flex-col gap-2", children: [
|
|
464
|
+
/* @__PURE__ */ jsx(Text, { size: "small", weight: "plus", children: "This sync looks stuck" }),
|
|
465
|
+
/* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
466
|
+
"It has not checked in",
|
|
467
|
+
silentFor != null ? ` for ${silentFor} minutes` : "",
|
|
468
|
+
". Until it is cleared or reclaimed, no other sync can start."
|
|
469
|
+
] }),
|
|
470
|
+
/* @__PURE__ */ jsxs(Prompt, { children: [
|
|
471
|
+
/* @__PURE__ */ jsx(Prompt.Trigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { size: "small", variant: "danger", disabled: clearing, children: "Clear it" }) }),
|
|
472
|
+
/* @__PURE__ */ jsxs(Prompt.Content, { children: [
|
|
473
|
+
/* @__PURE__ */ jsxs(Prompt.Header, { children: [
|
|
474
|
+
/* @__PURE__ */ jsx(Prompt.Title, { children: "Clear the stuck sync?" }),
|
|
475
|
+
/* @__PURE__ */ jsx(Prompt.Description, { children: "This marks the run as failed so a new sync can start. If it turns out to be alive, nothing is cleared. Products already imported are not touched." })
|
|
476
|
+
] }),
|
|
477
|
+
/* @__PURE__ */ jsxs(Prompt.Footer, { children: [
|
|
478
|
+
/* @__PURE__ */ jsx(Prompt.Cancel, { children: "Cancel" }),
|
|
479
|
+
/* @__PURE__ */ jsx(Prompt.Action, { onClick: () => void clearStuck(), children: "Clear" })
|
|
480
|
+
] })
|
|
481
|
+
] })
|
|
482
|
+
] })
|
|
483
|
+
] }),
|
|
484
|
+
/* @__PURE__ */ jsxs("div", { className: "px-6 py-4 flex flex-col gap-2", children: [
|
|
485
|
+
/* @__PURE__ */ jsx(Text, { size: "small", weight: "plus", children: "Sales" }),
|
|
486
|
+
stats ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
487
|
+
/* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
488
|
+
"Paid orders: ",
|
|
489
|
+
((_a = stats.total_paid_orders) == null ? void 0 : _a.value) ?? "—"
|
|
490
|
+
] }),
|
|
491
|
+
/* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
492
|
+
"Profit: ",
|
|
493
|
+
((_b = stats.profit) == null ? void 0 : _b.value) ?? "—",
|
|
494
|
+
" ",
|
|
495
|
+
((_c = stats.currency) == null ? void 0 : _c.toUpperCase()) ?? ""
|
|
496
|
+
] })
|
|
497
|
+
] }) : /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: loading ? "Loading…" : "Printful reported no statistics." })
|
|
498
|
+
] }),
|
|
499
|
+
/* @__PURE__ */ jsxs("div", { className: "px-6 py-4 flex flex-col gap-2", children: [
|
|
500
|
+
/* @__PURE__ */ jsx(Text, { size: "small", weight: "plus", children: "Webhooks" }),
|
|
501
|
+
health && !health.ever_received && // The state a misconfigured webhook leaves behind, and the reason
|
|
502
|
+
// this panel exists: a store can look fine and never hear back.
|
|
503
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: health.secret_set ? "A secret is configured, but nothing has arrived yet." : "No webhook secret is configured." }),
|
|
504
|
+
health && health.ever_received && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
505
|
+
/* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
506
|
+
"Last event:",
|
|
507
|
+
" ",
|
|
508
|
+
health.last_event_at ? new Date(health.last_event_at).toLocaleString() : "—"
|
|
509
|
+
] }),
|
|
510
|
+
/* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
511
|
+
health.processed,
|
|
512
|
+
" processed · ",
|
|
513
|
+
health.deferred,
|
|
514
|
+
" waiting ·",
|
|
515
|
+
" ",
|
|
516
|
+
health.failed,
|
|
517
|
+
" failed"
|
|
518
|
+
] })
|
|
519
|
+
] })
|
|
520
|
+
] }),
|
|
521
|
+
/* @__PURE__ */ jsxs("div", { className: "px-6 py-4 flex flex-col gap-3", children: [
|
|
522
|
+
/* @__PURE__ */ jsx(Text, { size: "small", weight: "plus", children: "Design parameters" }),
|
|
523
|
+
designs.length === 0 ? /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: loading ? "Loading…" : "No products carry design parameters yet. Run a sync." }) : designs.map((d) => /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
524
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
525
|
+
/* @__PURE__ */ jsx(Text, { size: "small", weight: "plus", children: d.title }),
|
|
526
|
+
/* @__PURE__ */ jsx(Badge, { color: CLASS_COLOUR[d.product_class] ?? "grey", children: CLASS_LABEL[d.product_class] ?? d.product_class })
|
|
527
|
+
] }),
|
|
528
|
+
/* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
529
|
+
d.technique ?? "—",
|
|
530
|
+
d.brand ? ` · ${d.brand}${d.model ? ` ${d.model}` : ""}` : "",
|
|
531
|
+
d.material ? ` · ${d.material}` : ""
|
|
532
|
+
] }),
|
|
533
|
+
/* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
534
|
+
"Design goes on: ",
|
|
535
|
+
d.core_placements.join(", ") || "—",
|
|
536
|
+
d.placements.length > d.core_placements.length ? ` (${d.placements.length - d.core_placements.length} more available)` : ""
|
|
537
|
+
] }),
|
|
538
|
+
d.colors.length > 0 && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 flex-wrap", children: [
|
|
539
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Base colours:" }),
|
|
540
|
+
d.colors.slice(0, 12).map((c) => /* @__PURE__ */ jsx(
|
|
541
|
+
"span",
|
|
542
|
+
{
|
|
543
|
+
title: `${c.name}${c.hex ? ` ${c.hex}` : ""}`,
|
|
544
|
+
className: "inline-block h-4 w-4 rounded border border-ui-border-base",
|
|
545
|
+
style: c.hex ? { backgroundColor: c.hex } : void 0
|
|
546
|
+
},
|
|
547
|
+
c.name
|
|
548
|
+
)),
|
|
549
|
+
d.colors.length > 12 && /* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
550
|
+
"+",
|
|
551
|
+
d.colors.length - 12
|
|
552
|
+
] })
|
|
553
|
+
] }),
|
|
554
|
+
d.sizes.length > 0 && /* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
555
|
+
d.product_class === "print_media" ? "Sizes" : "Sizes",
|
|
556
|
+
":",
|
|
557
|
+
" ",
|
|
558
|
+
d.sizes.slice(0, 8).join(", "),
|
|
559
|
+
d.sizes.length > 8 ? ` +${d.sizes.length - 8}` : ""
|
|
560
|
+
] })
|
|
561
|
+
] }, d.product_id))
|
|
562
|
+
] }),
|
|
563
|
+
/* @__PURE__ */ jsxs("div", { className: "px-6 py-4 flex flex-col gap-3", children: [
|
|
564
|
+
/* @__PURE__ */ jsx(Text, { size: "small", weight: "plus", children: "Bundles" }),
|
|
565
|
+
bundles.length === 0 ? /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: loading ? "Loading…" : "No bundles yet. Add printful_bundle_members to a variant's metadata to make it a bundle." }) : bundles.map((b) => /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
566
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
567
|
+
/* @__PURE__ */ jsx(Text, { size: "small", weight: "plus", children: b.title }),
|
|
568
|
+
/* @__PURE__ */ jsx(Badge, { color: b.available ? "green" : "red", children: b.available ? "all members in stock" : "member unavailable" }),
|
|
569
|
+
/* @__PURE__ */ jsx(Badge, { color: b.status === "published" ? "green" : "grey", children: b.status }),
|
|
570
|
+
b.missing_count > 0 && /* @__PURE__ */ jsxs(Badge, { color: "orange", children: [
|
|
571
|
+
b.missing_count,
|
|
572
|
+
" member",
|
|
573
|
+
b.missing_count === 1 ? "" : "s",
|
|
574
|
+
" not found"
|
|
575
|
+
] })
|
|
576
|
+
] }),
|
|
577
|
+
b.members.map((m) => /* @__PURE__ */ jsxs(
|
|
578
|
+
Text,
|
|
579
|
+
{
|
|
580
|
+
size: "small",
|
|
581
|
+
className: "text-ui-fg-subtle pl-3",
|
|
582
|
+
children: [
|
|
583
|
+
m.quantity,
|
|
584
|
+
"× ",
|
|
585
|
+
m.product_title ?? "—",
|
|
586
|
+
m.title ? ` · ${m.title}` : "",
|
|
587
|
+
m.status === "missing" ? " — variant not found" : m.status === "active" || m.status === "unknown" ? "" : ` — ${m.status.replace(/_/g, " ")}`
|
|
588
|
+
]
|
|
589
|
+
},
|
|
590
|
+
m.variant_id
|
|
591
|
+
))
|
|
592
|
+
] }, b.variant_id))
|
|
593
|
+
] }),
|
|
594
|
+
/* @__PURE__ */ jsxs("div", { className: "px-6 py-4 flex flex-col gap-3", children: [
|
|
595
|
+
/* @__PURE__ */ jsx(Text, { size: "small", weight: "plus", children: "Mockup prompts" }),
|
|
596
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Paste these into an image model. Printful’s own generator renders a product on a plain background — right for a catalogue thumbnail, not for an editorial mockup." }),
|
|
597
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [
|
|
598
|
+
/* @__PURE__ */ jsxs(Select, { value: style, onValueChange: setStyle, children: [
|
|
599
|
+
/* @__PURE__ */ jsx(Select.Trigger, { className: "w-40", children: /* @__PURE__ */ jsx(Select.Value, { placeholder: "Style" }) }),
|
|
600
|
+
/* @__PURE__ */ jsx(Select.Content, { children: styles.map((s) => /* @__PURE__ */ jsx(Select.Item, { value: s.id, children: s.label }, s.id)) })
|
|
601
|
+
] }),
|
|
602
|
+
/* @__PURE__ */ jsx(
|
|
603
|
+
Input,
|
|
604
|
+
{
|
|
605
|
+
className: "flex-1 min-w-64",
|
|
606
|
+
placeholder: "What is the artwork? (optional)",
|
|
607
|
+
value: artwork,
|
|
608
|
+
onChange: (e) => setArtwork(e.target.value)
|
|
609
|
+
}
|
|
610
|
+
)
|
|
611
|
+
] }),
|
|
612
|
+
promptProducts.length === 0 ? /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: loading ? "Loading…" : "No prompts yet. Run a sync so products carry design parameters." }) : promptProducts.map((p) => /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
|
|
613
|
+
/* @__PURE__ */ jsx(Text, { size: "small", weight: "plus", children: p.title }),
|
|
614
|
+
p.prompts.map((prompt, i) => /* @__PURE__ */ jsxs(
|
|
615
|
+
"div",
|
|
616
|
+
{
|
|
617
|
+
className: "flex items-start gap-2 rounded-md border border-ui-border-base p-2",
|
|
618
|
+
children: [
|
|
619
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1 flex-1", children: [
|
|
620
|
+
/* @__PURE__ */ jsx(Text, { size: "xsmall", className: "text-ui-fg-muted", children: prompt.label }),
|
|
621
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: prompt.text })
|
|
622
|
+
] }),
|
|
623
|
+
/* @__PURE__ */ jsx(Copy, { content: prompt.text })
|
|
624
|
+
]
|
|
625
|
+
},
|
|
626
|
+
`${p.product_id}-${i}`
|
|
627
|
+
))
|
|
628
|
+
] }, p.product_id))
|
|
629
|
+
] }),
|
|
630
|
+
/* @__PURE__ */ jsxs("div", { className: "px-6 py-4 flex flex-col gap-2", children: [
|
|
631
|
+
/* @__PURE__ */ jsx(Text, { size: "small", weight: "plus", children: "Recent syncs" }),
|
|
632
|
+
syncs.length === 0 ? /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: loading ? "Loading…" : "No syncs yet." }) : /* @__PURE__ */ jsxs(Table, { children: [
|
|
633
|
+
/* @__PURE__ */ jsx(Table.Header, { children: /* @__PURE__ */ jsxs(Table.Row, { children: [
|
|
634
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { children: "Started" }),
|
|
635
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { children: "Status" }),
|
|
636
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { children: "Result" })
|
|
637
|
+
] }) }),
|
|
638
|
+
/* @__PURE__ */ jsx(Table.Body, { children: syncs.map((row) => /* @__PURE__ */ jsxs(Table.Row, { children: [
|
|
639
|
+
/* @__PURE__ */ jsx(Table.Cell, { children: row.started_at ? new Date(row.started_at).toLocaleString() : "—" }),
|
|
640
|
+
/* @__PURE__ */ jsx(Table.Cell, { children: /* @__PURE__ */ jsx(Badge, { color: STATUS_COLOUR[row.status] ?? "grey", children: row.status }) }),
|
|
641
|
+
/* @__PURE__ */ jsx(Table.Cell, { children: row.status === "running" ? `${row.products_processed ?? 0} of ${row.products_total ?? "?"}` : `${row.products_created ?? 0} created · ${row.products_updated ?? 0} updated · ${row.products_failed ?? 0} failed` })
|
|
642
|
+
] }, row.id)) })
|
|
643
|
+
] })
|
|
644
|
+
] })
|
|
645
|
+
] });
|
|
646
|
+
};
|
|
647
|
+
const config = defineRouteConfig({
|
|
648
|
+
label: "Printful"
|
|
649
|
+
});
|
|
309
650
|
const i18nTranslations0 = {};
|
|
310
651
|
const widgetModule = { widgets: [
|
|
311
652
|
{
|
|
@@ -320,10 +661,25 @@ const widgetModule = { widgets: [
|
|
|
320
661
|
}
|
|
321
662
|
] };
|
|
322
663
|
const routeModule = {
|
|
323
|
-
routes: [
|
|
664
|
+
routes: [
|
|
665
|
+
{
|
|
666
|
+
Component: PrintfulPage,
|
|
667
|
+
path: "/printful",
|
|
668
|
+
handle: { label: config.label, translationNs: config.translationNs }
|
|
669
|
+
}
|
|
670
|
+
]
|
|
324
671
|
};
|
|
325
672
|
const menuItemModule = {
|
|
326
|
-
menuItems: [
|
|
673
|
+
menuItems: [
|
|
674
|
+
{
|
|
675
|
+
label: config.label,
|
|
676
|
+
icon: void 0,
|
|
677
|
+
path: "/printful",
|
|
678
|
+
nested: void 0,
|
|
679
|
+
rank: void 0,
|
|
680
|
+
translationNs: void 0
|
|
681
|
+
}
|
|
682
|
+
]
|
|
327
683
|
};
|
|
328
684
|
const formModule = { customFields: {} };
|
|
329
685
|
const displayModule = {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GET = GET;
|
|
4
|
+
const utils_1 = require("@medusajs/framework/utils");
|
|
5
|
+
const bundle_1 = require("../../../../utils/bundle");
|
|
6
|
+
/**
|
|
7
|
+
* Bundles and whether each can currently be sold.
|
|
8
|
+
*
|
|
9
|
+
* A pure read of the database — no Printful call. Availability comes from the
|
|
10
|
+
* `printful_availability_status` the sync already stamped on every member
|
|
11
|
+
* variant, which is the same source the sync's own bundle pass decides on. A
|
|
12
|
+
* live lookup here could disagree with the status the catalogue was last
|
|
13
|
+
* published against, and show the owner a bundle as sellable that the next
|
|
14
|
+
* sync is about to draft.
|
|
15
|
+
*
|
|
16
|
+
* Every member is listed, available or not, because the question the owner has
|
|
17
|
+
* when a bundle goes off sale is *which* member ran out.
|
|
18
|
+
*/
|
|
19
|
+
async function GET(req, res) {
|
|
20
|
+
const productModule = req.scope.resolve(utils_1.Modules.PRODUCT);
|
|
21
|
+
const products = await productModule.listProducts({}, { relations: ["variants"], take: 1000 });
|
|
22
|
+
// Members are variant ids on other products, so the whole catalogue has to
|
|
23
|
+
// be indexed before any bundle can be resolved.
|
|
24
|
+
const variantIndex = new Map();
|
|
25
|
+
for (const product of products) {
|
|
26
|
+
for (const variant of product.variants ?? []) {
|
|
27
|
+
const metadata = (variant.metadata ?? {});
|
|
28
|
+
variantIndex.set(variant.id, {
|
|
29
|
+
title: variant.title ?? variant.id,
|
|
30
|
+
product_title: product.title,
|
|
31
|
+
status: typeof metadata.printful_availability_status === "string"
|
|
32
|
+
? metadata.printful_availability_status
|
|
33
|
+
: "unknown",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const bundles = [];
|
|
38
|
+
for (const product of products) {
|
|
39
|
+
for (const variant of product.variants ?? []) {
|
|
40
|
+
const members = (0, bundle_1.bundleMembersOf)({
|
|
41
|
+
id: variant.id,
|
|
42
|
+
quantity: 1,
|
|
43
|
+
metadata: (variant.metadata ?? {}),
|
|
44
|
+
});
|
|
45
|
+
if (!members.length) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const resolved = members.map((member) => {
|
|
49
|
+
const found = variantIndex.get(member.variant_id);
|
|
50
|
+
return {
|
|
51
|
+
variant_id: member.variant_id,
|
|
52
|
+
quantity: member.quantity ?? 1,
|
|
53
|
+
// A member whose variant no longer exists is reported as such rather
|
|
54
|
+
// than hidden: it is the likeliest reason a bundle fails at Printful,
|
|
55
|
+
// and it is invisible everywhere else in the admin.
|
|
56
|
+
title: found?.title ?? null,
|
|
57
|
+
product_title: found?.product_title ?? null,
|
|
58
|
+
status: found?.status ?? "missing",
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
const availability = (0, bundle_1.planBundleAvailability)(resolved
|
|
62
|
+
.filter((m) => m.status !== "missing")
|
|
63
|
+
.map((m) => ({ printful_availability_status: m.status })));
|
|
64
|
+
bundles.push({
|
|
65
|
+
product_id: product.id,
|
|
66
|
+
variant_id: variant.id,
|
|
67
|
+
title: product.title,
|
|
68
|
+
variant_title: variant.title,
|
|
69
|
+
status: product.status,
|
|
70
|
+
available: availability.available,
|
|
71
|
+
member_count: resolved.length,
|
|
72
|
+
missing_count: resolved.filter((m) => m.status === "missing").length,
|
|
73
|
+
members: resolved,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
res.status(200).json({ bundles });
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicm91dGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi8uLi9zcmMvYXBpL2FkbWluL3ByaW50ZnVsL2J1bmRsZXMvcm91dGUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFvQkEsa0JBMkVDO0FBOUZELHFEQUFtRDtBQUNuRCxxREFHaUM7QUFFakM7Ozs7Ozs7Ozs7OztHQVlHO0FBQ0ksS0FBSyxVQUFVLEdBQUcsQ0FBQyxHQUFrQixFQUFFLEdBQW1CO0lBQy9ELE1BQU0sYUFBYSxHQUFHLEdBQUcsQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLGVBQU8sQ0FBQyxPQUFPLENBQUMsQ0FBQTtJQUV4RCxNQUFNLFFBQVEsR0FBRyxNQUFNLGFBQWEsQ0FBQyxZQUFZLENBQy9DLEVBQUUsRUFDRixFQUFFLFNBQVMsRUFBRSxDQUFDLFVBQVUsQ0FBQyxFQUFFLElBQUksRUFBRSxJQUFJLEVBQUUsQ0FDeEMsQ0FBQTtJQUVELDJFQUEyRTtJQUMzRSxnREFBZ0Q7SUFDaEQsTUFBTSxZQUFZLEdBQUcsSUFBSSxHQUFHLEVBR3pCLENBQUE7SUFDSCxLQUFLLE1BQU0sT0FBTyxJQUFJLFFBQVEsRUFBRSxDQUFDO1FBQy9CLEtBQUssTUFBTSxPQUFPLElBQUksT0FBTyxDQUFDLFFBQVEsSUFBSSxFQUFFLEVBQUUsQ0FBQztZQUM3QyxNQUFNLFFBQVEsR0FBRyxDQUFDLE9BQU8sQ0FBQyxRQUFRLElBQUksRUFBRSxDQUE0QixDQUFBO1lBQ3BFLFlBQVksQ0FBQyxHQUFHLENBQUMsT0FBTyxDQUFDLEVBQUUsRUFBRTtnQkFDM0IsS0FBSyxFQUFFLE9BQU8sQ0FBQyxLQUFLLElBQUksT0FBTyxDQUFDLEVBQUU7Z0JBQ2xDLGFBQWEsRUFBRSxPQUFPLENBQUMsS0FBSztnQkFDNUIsTUFBTSxFQUNKLE9BQU8sUUFBUSxDQUFDLDRCQUE0QixLQUFLLFFBQVE7b0JBQ3ZELENBQUMsQ0FBQyxRQUFRLENBQUMsNEJBQTRCO29CQUN2QyxD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
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GET = GET;
|
|
4
|
+
const utils_1 = require("@medusajs/framework/utils");
|
|
5
|
+
const design_1 = require("../../../../utils/design");
|
|
6
|
+
/**
|
|
7
|
+
* Design parameters per product, read from the variant metadata the sync
|
|
8
|
+
* stamped.
|
|
9
|
+
*
|
|
10
|
+
* A pure read of the database — no Printful call. The sync already paid for
|
|
11
|
+
* the catalog lookups, and this page is opened far more often than the
|
|
12
|
+
* catalogue changes.
|
|
13
|
+
*
|
|
14
|
+
* Products whose variants carry no design metadata are omitted rather than
|
|
15
|
+
* listed empty: one was never enriched, the other has nothing to show, and
|
|
16
|
+
* rendering them the same way tells the owner something false.
|
|
17
|
+
*/
|
|
18
|
+
async function GET(req, res) {
|
|
19
|
+
const productModule = req.scope.resolve(utils_1.Modules.PRODUCT);
|
|
20
|
+
const products = await productModule.listProducts({}, { relations: ["variants"], take: 100 });
|
|
21
|
+
const designs = products
|
|
22
|
+
.map((product) => {
|
|
23
|
+
const summary = (0, design_1.summarizeProductDesign)(product.variants ?? []);
|
|
24
|
+
if (!summary) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
product_id: product.id,
|
|
29
|
+
title: product.title,
|
|
30
|
+
status: product.status,
|
|
31
|
+
variant_count: (product.variants ?? []).length,
|
|
32
|
+
...summary,
|
|
33
|
+
};
|
|
34
|
+
})
|
|
35
|
+
.filter((d) => d !== null);
|
|
36
|
+
res.status(200).json({ designs });
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicm91dGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi8uLi9zcmMvYXBpL2FkbWluL3ByaW50ZnVsL2Rlc2lnbi9yb3V0ZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQWdCQSxrQkF5QkM7QUF4Q0QscURBQW1EO0FBQ25ELHFEQUFpRTtBQUVqRTs7Ozs7Ozs7Ozs7R0FXRztBQUNJLEtBQUssVUFBVSxHQUFHLENBQUMsR0FBa0IsRUFBRSxHQUFtQjtJQUMvRCxNQUFNLGFBQWEsR0FBRyxHQUFHLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxlQUFPLENBQUMsT0FBTyxDQUFDLENBQUE7SUFFeEQsTUFBTSxRQUFRLEdBQUcsTUFBTSxhQUFhLENBQUMsWUFBWSxDQUMvQyxFQUFFLEVBQ0YsRUFBRSxTQUFTLEVBQUUsQ0FBQyxVQUFVLENBQUMsRUFBRSxJQUFJLEVBQUUsR0FBRyxFQUFFLENBQ3ZDLENBQUE7SUFFRCxNQUFNLE9BQU8sR0FBRyxRQUFRO1NBQ3JCLEdBQUcsQ0FBQyxDQUFDLE9BQU8sRUFBRSxFQUFFO1FBQ2YsTUFBTSxPQUFPLEdBQUcsSUFBQSwrQkFBc0IsRUFBQyxPQUFPLENBQUMsUUFBUSxJQUFJLEVBQUUsQ0FBQyxDQUFBO1FBQzlELElBQUksQ0FBQyxPQUFPLEVBQUUsQ0FBQztZQUNiLE9BQU8sSUFBSSxDQUFBO1FBQ2IsQ0FBQztRQUNELE9BQU87WUFDTCxVQUFVLEVBQUUsT0FBTyxDQUFDLEVBQUU7WUFDdEIsS0FBSyxFQUFFLE9BQU8sQ0FBQyxLQUFLO1lBQ3BCLE1BQU0sRUFBRSxPQUFPLENBQUMsTUFBTTtZQUN0QixhQUFhLEVBQUUsQ0FBQyxPQUFPLENBQUMsUUFBUSxJQUFJLEVBQUUsQ0FBQyxDQUFDLE1BQU07WUFDOUMsR0FBRyxPQUFPO1NBQ1gsQ0FBQTtJQUNILENBQUMsQ0FBQztTQUNELE1BQU0sQ0FBQyxDQUFDLENBQUMsRUFBOEIsRUFBRSxDQUFDLENBQUMsS0FBSyxJQUFJLENBQUMsQ0FBQTtJQUV4RCxHQUFHLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDLElBQUksQ0FBQyxFQUFFLE9BQU8sRUFBRSxDQUFDLENBQUE7QUFDbkMsQ0FBQyJ9
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GET = GET;
|
|
4
|
+
const printful_1 = require("../../../../modules/printful");
|
|
5
|
+
const webhook_health_1 = require("../../../../utils/webhook-health");
|
|
6
|
+
/**
|
|
7
|
+
* Rows the summary is computed over: the most recent slice of the event table.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately a window rather than the whole table. This is a health panel —
|
|
10
|
+
* "are deliveries arriving and are they landing" — and the answer lives in
|
|
11
|
+
* recent traffic. Reading every row ever received would grow unbounded with
|
|
12
|
+
* store age and make the page slower the longer the plugin has been installed,
|
|
13
|
+
* for an answer that gets less useful the further back it reaches.
|
|
14
|
+
*
|
|
15
|
+
* The counts are therefore explicitly counts within this window, and the
|
|
16
|
+
* response says so via `window`, so the page cannot present them as all-time
|
|
17
|
+
* totals.
|
|
18
|
+
*/
|
|
19
|
+
const HEALTH_WINDOW = 200;
|
|
20
|
+
/**
|
|
21
|
+
* Webhook delivery health.
|
|
22
|
+
*
|
|
23
|
+
* This route does NOT call Printful. `client.getWebhookConfig()` exists and
|
|
24
|
+
* would report the registered URL authoritatively, but a panel whose entire
|
|
25
|
+
* job is to tell the owner whether Printful is reaching them must not go dark
|
|
26
|
+
* when Printful is down — an outage would turn the one screen you check
|
|
27
|
+
* during an outage into an error. Everything here is answered from local rows
|
|
28
|
+
* and plugin options, so it renders during a Printful outage and, in fact,
|
|
29
|
+
* shows exactly the symptom (no recent events) that an outage produces.
|
|
30
|
+
*
|
|
31
|
+
* `/admin/printful/webhook` still exposes the live config for the moment an
|
|
32
|
+
* admin actually wants to inspect or overwrite the registration, and that
|
|
33
|
+
* route is the right place for a network call because the admin asked for it.
|
|
34
|
+
*/
|
|
35
|
+
async function GET(req, res) {
|
|
36
|
+
const printful = req.scope.resolve(printful_1.PRINTFUL_MODULE);
|
|
37
|
+
const options = await printful.getOptions();
|
|
38
|
+
const rows = (await printful.listPrintfulWebhookEvents({}, { take: HEALTH_WINDOW, order: { created_at: "DESC" } }));
|
|
39
|
+
const summary = (0, webhook_health_1.summarizeWebhookHealth)(rows, {
|
|
40
|
+
secret_set: Boolean(options.webhookSecret),
|
|
41
|
+
});
|
|
42
|
+
res.status(200).json({
|
|
43
|
+
...summary,
|
|
44
|
+
window: HEALTH_WINDOW,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicm91dGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi8uLi9zcmMvYXBpL2FkbWluL3ByaW50ZnVsL2hlYWx0aC9yb3V0ZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQXNDQSxrQkFpQkM7QUF0REQsMkRBQThEO0FBRTlELHFFQUd5QztBQUV6Qzs7Ozs7Ozs7Ozs7O0dBWUc7QUFDSCxNQUFNLGFBQWEsR0FBRyxHQUFHLENBQUE7QUFFekI7Ozs7Ozs7Ozs7Ozs7O0dBY0c7QUFDSSxLQUFLLFVBQVUsR0FBRyxDQUFDLEdBQWtCLEVBQUUsR0FBbUI7SUFDL0QsTUFBTSxRQUFRLEdBQTBCLEdBQUcsQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLDBCQUFlLENBQUMsQ0FBQTtJQUMxRSxNQUFNLE9BQU8sR0FBRyxNQUFNLFFBQVEsQ0FBQyxVQUFVLEVBQUUsQ0FBQTtJQUUzQyxNQUFNLElBQUksR0FBRyxDQUFDLE1BQU0sUUFBUSxDQUFDLHlCQUF5QixDQUNwRCxFQUFFLEVBQ0YsRUFBRSxJQUFJLEVBQUUsYUFBYSxFQUFFLEtBQUssRUFBRSxFQUFFLFVBQVUsRUFBRSxNQUFNLEVBQUUsRUFBRSxDQUN2RCxDQUF1QixDQUFBO0lBRXhCLE1BQU0sT0FBTyxHQUFHLElBQUEsdUNBQXNCLEVBQUMsSUFBSSxFQUFFO1FBQzNDLFVBQVUsRUFBRSxPQUFPLENBQUMsT0FBTyxDQUFDLGFBQWEsQ0FBQztLQUMzQyxDQUFDLENBQUE7SUFFRixHQUFHLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDLElBQUksQ0FBQztRQUNuQixHQUFHLE9BQU87UUFDVixNQUFNLEVBQUUsYUFBYTtLQUN0QixDQUFDLENBQUE7QUFDSixDQUFDIn0=
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GET = GET;
|
|
4
|
+
const printful_1 = require("../../../../modules/printful");
|
|
5
|
+
/** Rows per page. Enough to cover a week of hourly syncs on one screen. */
|
|
6
|
+
const DEFAULT_LIMIT = 20;
|
|
7
|
+
/**
|
|
8
|
+
* Hard ceiling on `limit`. The admin page never asks for more than the
|
|
9
|
+
* default; this only stops a hand-crafted `?limit=100000` from paging the
|
|
10
|
+
* whole table into memory to render a list nobody reads.
|
|
11
|
+
*/
|
|
12
|
+
const MAX_LIMIT = 100;
|
|
13
|
+
function parsePositiveInt(value, fallback, max) {
|
|
14
|
+
const parsed = Number(value);
|
|
15
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
16
|
+
return fallback;
|
|
17
|
+
}
|
|
18
|
+
return Math.min(Math.floor(parsed), max);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Recent sync runs, newest first.
|
|
22
|
+
*
|
|
23
|
+
* Ordered by `started_at DESC` to match `getLatestSyncLog`, so the first row
|
|
24
|
+
* here is always the same run the status widget calls "latest". Ordering by
|
|
25
|
+
* `finished_at` instead would sort a still-running sync (finished_at null) to
|
|
26
|
+
* an arbitrary end of the list.
|
|
27
|
+
*/
|
|
28
|
+
async function GET(req, res) {
|
|
29
|
+
const printful = req.scope.resolve(printful_1.PRINTFUL_MODULE);
|
|
30
|
+
const limit = parsePositiveInt(req.query.limit, DEFAULT_LIMIT, MAX_LIMIT);
|
|
31
|
+
const offset = Math.max(0, Number(req.query.offset) || 0);
|
|
32
|
+
const logs = await printful.listPrintfulSyncLogs({}, { take: limit, skip: offset, order: { started_at: "DESC" } });
|
|
33
|
+
res.status(200).json({
|
|
34
|
+
syncs: logs.map((log) => ({
|
|
35
|
+
id: log.id,
|
|
36
|
+
status: log.status,
|
|
37
|
+
started_at: log.started_at,
|
|
38
|
+
finished_at: log.finished_at,
|
|
39
|
+
heartbeat_at: log.heartbeat_at,
|
|
40
|
+
products_created: log.products_created,
|
|
41
|
+
products_updated: log.products_updated,
|
|
42
|
+
products_failed: log.products_failed,
|
|
43
|
+
products_processed: log.products_processed,
|
|
44
|
+
products_total: log.products_total,
|
|
45
|
+
error_message: log.error_message,
|
|
46
|
+
})),
|
|
47
|
+
limit,
|
|
48
|
+
offset,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicm91dGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi8uLi9zcmMvYXBpL2FkbWluL3ByaW50ZnVsL2hpc3Rvcnkvcm91dGUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFrQ0Esa0JBNEJDO0FBN0RELDJEQUE4RDtBQUc5RCwyRUFBMkU7QUFDM0UsTUFBTSxhQUFhLEdBQUcsRUFBRSxDQUFBO0FBRXhCOzs7O0dBSUc7QUFDSCxNQUFNLFNBQVMsR0FBRyxHQUFHLENBQUE7QUFFckIsU0FBUyxnQkFBZ0IsQ0FDdkIsS0FBYyxFQUNkLFFBQWdCLEVBQ2hCLEdBQVc7SUFFWCxNQUFNLE1BQU0sR0FBRyxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUE7SUFDNUIsSUFBSSxDQUFDLE1BQU0sQ0FBQyxRQUFRLENBQUMsTUFBTSxDQUFDLElBQUksTUFBTSxJQUFJLENBQUMsRUFBRSxDQUFDO1FBQzVDLE9BQU8sUUFBUSxDQUFBO0lBQ2pCLENBQUM7SUFDRCxPQUFPLElBQUksQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxNQUFNLENBQUMsRUFBRSxHQUFHLENBQUMsQ0FBQTtBQUMxQyxDQUFDO0FBRUQ7Ozs7Ozs7R0FPRztBQUNJLEtBQUssVUFBVSxHQUFHLENBQUMsR0FBa0IsRUFBRSxHQUFtQjtJQUMvRCxNQUFNLFFBQVEsR0FBMEIsR0FBRyxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQUMsMEJBQWUsQ0FBQyxDQUFBO0lBRTFFLE1BQU0sS0FBSyxHQUFHLGdCQUFnQixDQUFDLEdBQUcsQ0FBQyxLQUFLLENBQUMsS0FBSyxFQUFFLGFBQWEsRUFBRSxTQUFTLENBQUMsQ0FBQTtJQUN6RSxNQUFNLE1BQU0sR0FBRyxJQUFJLENBQUMsR0FBRyxDQUFDLENBQUMsRUFBRSxNQUFNLENBQUMsR0FBRyxDQUFDLEtBQUssQ0FBQyxNQUFNLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQTtJQUV6RCxNQUFNLElBQUksR0FBRyxNQUFNLFFBQVEsQ0FBQyxvQkFBb0IsQ0FDOUMsRUFBRSxFQUNGLEVBQUUsSUFBSSxFQUFFLEtBQUssRUFBRSxJQUFJLEVBQUUsTUFBTSxFQUFFLEtBQUssRUFBRSxFQUFFLFVBQVUsRUFBRSxNQUFNLEVBQUUsRUFBRSxDQUM3RCxDQUFBO0lBRUQsR0FBRyxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQyxJQUFJLENBQUM7UUFDbkIsS0FBSyxFQUFFLElBQUksQ0FBQyxHQUFHLENBQUMsQ0FBQyxHQUFHLEVBQUUsRUFBRSxDQUFDLENBQUM7WUFDeEIsRUFBRSxFQUFFLEdBQUcsQ0FBQyxFQUFFO1lBQ1YsTUFBTSxFQUFFLEdBQUcsQ0FBQyxNQUFNO1lBQ2xCLFVBQVUsRUFBRSxHQUFHLENBQUMsVUFBVTtZQUMxQixXQUFXLEVBQUUsR0FBRyxDQUFDLFdBQVc7WUFDNUIsWUFBWSxFQUFFLEdBQUcsQ0FBQyxZQUFZO1lBQzlCLGdCQUFnQixFQUFFLEdBQUcsQ0FBQyxnQkFBZ0I7WUFDdEMsZ0JBQWdCLEVBQUUsR0FBRyxDQUFDLGdCQUFnQjtZQUN0QyxlQUFlLEVBQUUsR0FBRyxDQUFDLGVBQWU7WUFDcEMsa0JBQWtCLEVBQUUsR0FBRyxDQUFDLGtCQUFrQjtZQUMxQyxjQUFjLEVBQUUsR0FBRyxDQUFDLGNBQWM7WUFDbEMsYUFBYSxFQUFFLEdBQUcsQ0FBQyxhQUFhO1NBQ2pDLENBQUMsQ0FBQztRQUNILEtBQUs7UUFDTCxNQUFNO0tBQ1AsQ0FBQyxDQUFBO0FBQ0osQ0FBQyJ9
|