@legenki/print2medusa 0.2.0 → 0.4.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/.medusa/server/src/admin/index.js +29 -8
- package/.medusa/server/src/admin/index.mjs +29 -8
- package/.medusa/server/src/api/admin/printful/sync/route.js +36 -6
- package/.medusa/server/src/jobs/sync-products.js +17 -3
- package/.medusa/server/src/modules/printful/migrations/Migration20260802000000.js +40 -0
- package/.medusa/server/src/modules/printful/models/printful-sync-log.js +4 -1
- package/.medusa/server/src/modules/printful/service.js +67 -1
- package/.medusa/server/src/providers/printful-fulfillment/service.js +272 -15
- package/.medusa/server/src/utils/mappers.js +19 -3
- package/.medusa/server/src/utils/orphans.js +47 -0
- package/.medusa/server/src/utils/printful-client.js +15 -1
- package/.medusa/server/src/utils/shipping-rates.js +156 -0
- package/.medusa/server/src/utils/stock.js +85 -0
- package/.medusa/server/src/workflows/sync-products.js +61 -22
- package/README.md +117 -19
- package/package.json +2 -2
|
@@ -100,6 +100,13 @@ const PrintfulSyncWidget = () => {
|
|
|
100
100
|
void loadStatus();
|
|
101
101
|
void loadWebhook();
|
|
102
102
|
}, []);
|
|
103
|
+
react.useEffect(() => {
|
|
104
|
+
if (!(status == null ? void 0 : status.running)) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const t = setInterval(() => void loadStatus(), 3e3);
|
|
108
|
+
return () => clearInterval(t);
|
|
109
|
+
}, [status == null ? void 0 : status.running]);
|
|
103
110
|
const onRegisterWebhook = async () => {
|
|
104
111
|
setRegistering(true);
|
|
105
112
|
try {
|
|
@@ -126,7 +133,6 @@ const PrintfulSyncWidget = () => {
|
|
|
126
133
|
}
|
|
127
134
|
};
|
|
128
135
|
const onSync = async () => {
|
|
129
|
-
var _a, _b, _c;
|
|
130
136
|
setSyncing(true);
|
|
131
137
|
try {
|
|
132
138
|
const res = await fetch("/admin/printful/sync", {
|
|
@@ -135,13 +141,20 @@ const PrintfulSyncWidget = () => {
|
|
|
135
141
|
headers: { "Content-Type": "application/json" },
|
|
136
142
|
body: JSON.stringify({})
|
|
137
143
|
});
|
|
144
|
+
if (res.status === 409) {
|
|
145
|
+
const body = await res.json();
|
|
146
|
+
ui.toast.info("A Printful sync is already running", {
|
|
147
|
+
description: body.started_at ? `Started ${new Date(body.started_at).toLocaleString()}` : void 0
|
|
148
|
+
});
|
|
149
|
+
await loadStatus();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
138
152
|
if (!res.ok) {
|
|
139
153
|
const text = await res.text();
|
|
140
154
|
throw new Error(text || `Sync failed (${res.status})`);
|
|
141
155
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
description: `Created ${((_a = data.counters) == null ? void 0 : _a.created) ?? 0}, updated ${((_b = data.counters) == null ? void 0 : _b.updated) ?? 0}, failed ${((_c = data.counters) == null ? void 0 : _c.failed) ?? 0}`
|
|
156
|
+
ui.toast.success("Printful sync started", {
|
|
157
|
+
description: "Progress will appear below as products are processed."
|
|
145
158
|
});
|
|
146
159
|
await loadStatus();
|
|
147
160
|
} catch (e) {
|
|
@@ -153,6 +166,7 @@ const PrintfulSyncWidget = () => {
|
|
|
153
166
|
}
|
|
154
167
|
};
|
|
155
168
|
const latest = status == null ? void 0 : status.latest_sync;
|
|
169
|
+
const running = (status == null ? void 0 : status.running) === true;
|
|
156
170
|
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
|
|
157
171
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
|
|
158
172
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
@@ -164,9 +178,9 @@ const PrintfulSyncWidget = () => {
|
|
|
164
178
|
{
|
|
165
179
|
size: "small",
|
|
166
180
|
onClick: () => void onSync(),
|
|
167
|
-
isLoading: syncing,
|
|
168
|
-
disabled: loading || syncing,
|
|
169
|
-
children: "Sync Now"
|
|
181
|
+
isLoading: syncing || running,
|
|
182
|
+
disabled: loading || syncing || running,
|
|
183
|
+
children: running ? "Syncing…" : "Sync Now"
|
|
170
184
|
}
|
|
171
185
|
)
|
|
172
186
|
] }),
|
|
@@ -180,7 +194,14 @@ const PrintfulSyncWidget = () => {
|
|
|
180
194
|
new Date(latest.started_at).toLocaleString(),
|
|
181
195
|
latest.finished_at ? ` · Finished: ${new Date(latest.finished_at).toLocaleString()}` : ""
|
|
182
196
|
] }),
|
|
183
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
197
|
+
running ? /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
198
|
+
"Processed ",
|
|
199
|
+
latest.products_processed ?? 0,
|
|
200
|
+
" of",
|
|
201
|
+
" ",
|
|
202
|
+
latest.products_total ?? 0,
|
|
203
|
+
" products"
|
|
204
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
184
205
|
"Created ",
|
|
185
206
|
latest.products_created ?? 0,
|
|
186
207
|
" · Updated",
|
|
@@ -99,6 +99,13 @@ const PrintfulSyncWidget = () => {
|
|
|
99
99
|
void loadStatus();
|
|
100
100
|
void loadWebhook();
|
|
101
101
|
}, []);
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (!(status == null ? void 0 : status.running)) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const t = setInterval(() => void loadStatus(), 3e3);
|
|
107
|
+
return () => clearInterval(t);
|
|
108
|
+
}, [status == null ? void 0 : status.running]);
|
|
102
109
|
const onRegisterWebhook = async () => {
|
|
103
110
|
setRegistering(true);
|
|
104
111
|
try {
|
|
@@ -125,7 +132,6 @@ const PrintfulSyncWidget = () => {
|
|
|
125
132
|
}
|
|
126
133
|
};
|
|
127
134
|
const onSync = async () => {
|
|
128
|
-
var _a, _b, _c;
|
|
129
135
|
setSyncing(true);
|
|
130
136
|
try {
|
|
131
137
|
const res = await fetch("/admin/printful/sync", {
|
|
@@ -134,13 +140,20 @@ const PrintfulSyncWidget = () => {
|
|
|
134
140
|
headers: { "Content-Type": "application/json" },
|
|
135
141
|
body: JSON.stringify({})
|
|
136
142
|
});
|
|
143
|
+
if (res.status === 409) {
|
|
144
|
+
const body = await res.json();
|
|
145
|
+
toast.info("A Printful sync is already running", {
|
|
146
|
+
description: body.started_at ? `Started ${new Date(body.started_at).toLocaleString()}` : void 0
|
|
147
|
+
});
|
|
148
|
+
await loadStatus();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
137
151
|
if (!res.ok) {
|
|
138
152
|
const text = await res.text();
|
|
139
153
|
throw new Error(text || `Sync failed (${res.status})`);
|
|
140
154
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
description: `Created ${((_a = data.counters) == null ? void 0 : _a.created) ?? 0}, updated ${((_b = data.counters) == null ? void 0 : _b.updated) ?? 0}, failed ${((_c = data.counters) == null ? void 0 : _c.failed) ?? 0}`
|
|
155
|
+
toast.success("Printful sync started", {
|
|
156
|
+
description: "Progress will appear below as products are processed."
|
|
144
157
|
});
|
|
145
158
|
await loadStatus();
|
|
146
159
|
} catch (e) {
|
|
@@ -152,6 +165,7 @@ const PrintfulSyncWidget = () => {
|
|
|
152
165
|
}
|
|
153
166
|
};
|
|
154
167
|
const latest = status == null ? void 0 : status.latest_sync;
|
|
168
|
+
const running = (status == null ? void 0 : status.running) === true;
|
|
155
169
|
return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
|
|
156
170
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
|
|
157
171
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
@@ -163,9 +177,9 @@ const PrintfulSyncWidget = () => {
|
|
|
163
177
|
{
|
|
164
178
|
size: "small",
|
|
165
179
|
onClick: () => void onSync(),
|
|
166
|
-
isLoading: syncing,
|
|
167
|
-
disabled: loading || syncing,
|
|
168
|
-
children: "Sync Now"
|
|
180
|
+
isLoading: syncing || running,
|
|
181
|
+
disabled: loading || syncing || running,
|
|
182
|
+
children: running ? "Syncing…" : "Sync Now"
|
|
169
183
|
}
|
|
170
184
|
)
|
|
171
185
|
] }),
|
|
@@ -179,7 +193,14 @@ const PrintfulSyncWidget = () => {
|
|
|
179
193
|
new Date(latest.started_at).toLocaleString(),
|
|
180
194
|
latest.finished_at ? ` · Finished: ${new Date(latest.finished_at).toLocaleString()}` : ""
|
|
181
195
|
] }),
|
|
182
|
-
/* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
196
|
+
running ? /* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
197
|
+
"Processed ",
|
|
198
|
+
latest.products_processed ?? 0,
|
|
199
|
+
" of",
|
|
200
|
+
" ",
|
|
201
|
+
latest.products_total ?? 0,
|
|
202
|
+
" products"
|
|
203
|
+
] }) : /* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
183
204
|
"Created ",
|
|
184
205
|
latest.products_created ?? 0,
|
|
185
206
|
" · Updated",
|
|
@@ -4,22 +4,52 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.POST = POST;
|
|
7
|
+
const utils_1 = require("@medusajs/framework/utils");
|
|
8
|
+
const printful_1 = require("../../../../modules/printful");
|
|
7
9
|
const sync_products_1 = __importDefault(require("../../../../workflows/sync-products"));
|
|
10
|
+
/**
|
|
11
|
+
* Start a catalog sync.
|
|
12
|
+
*
|
|
13
|
+
* The claim completes before this responds. Doing it inside the background
|
|
14
|
+
* step would let two requests both answer 202 before either reached the
|
|
15
|
+
* insert, which is exactly the race the unique index exists to prevent.
|
|
16
|
+
*/
|
|
8
17
|
async function POST(req, res) {
|
|
18
|
+
const printful = req.scope.resolve(printful_1.PRINTFUL_MODULE);
|
|
19
|
+
const logger = req.scope.resolve(utils_1.ContainerRegistrationKeys.LOGGER);
|
|
20
|
+
const options = await printful.getOptions();
|
|
9
21
|
const limit = typeof req.body === "object" &&
|
|
10
22
|
req.body &&
|
|
11
23
|
"limit" in req.body &&
|
|
12
24
|
req.body.limit != null
|
|
13
25
|
? Number(req.body.limit)
|
|
14
26
|
: undefined;
|
|
15
|
-
const
|
|
27
|
+
const claim = await printful.claimSyncLog(options.syncStaleMinutes ?? 60);
|
|
28
|
+
if (!claim) {
|
|
29
|
+
const running = await printful.getRunningSyncLog();
|
|
30
|
+
res.status(409).json({
|
|
31
|
+
message: "A Printful sync is already running",
|
|
32
|
+
running_sync_id: running?.id,
|
|
33
|
+
started_at: running?.started_at,
|
|
34
|
+
heartbeat_at: running?.heartbeat_at,
|
|
35
|
+
});
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
// Deliberately not awaited: the step is backgroundExecution, and the point
|
|
39
|
+
// of this release is that the admin gets an answer immediately rather than
|
|
40
|
+
// holding the request open for a 500-product catalog. The scheduled job in
|
|
41
|
+
// src/jobs/sync-products.ts DOES await the same workflow — it serves no HTTP
|
|
42
|
+
// response and wants the counters to log.
|
|
43
|
+
void (0, sync_products_1.default)(req.scope)
|
|
44
|
+
.run({
|
|
16
45
|
input: {
|
|
46
|
+
sync_log_id: claim.id,
|
|
17
47
|
limit: Number.isFinite(limit) ? limit : undefined,
|
|
18
48
|
},
|
|
49
|
+
})
|
|
50
|
+
.catch((err) => {
|
|
51
|
+
logger.error(`Printful sync ${claim.id} failed to start: ${err instanceof Error ? err.message : String(err)}`);
|
|
19
52
|
});
|
|
20
|
-
res.status(
|
|
21
|
-
sync_log: result.sync_log,
|
|
22
|
-
counters: result.counters,
|
|
23
|
-
});
|
|
53
|
+
res.status(202).json({ sync_id: claim.id });
|
|
24
54
|
}
|
|
25
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
55
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicm91dGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi8uLi9zcmMvYXBpL2FkbWluL3ByaW50ZnVsL3N5bmMvcm91dGUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7QUFhQSxvQkFpREM7QUE3REQscURBQXFFO0FBQ3JFLDJEQUE4RDtBQUU5RCx3RkFBc0U7QUFFdEU7Ozs7OztHQU1HO0FBQ0ksS0FBSyxVQUFVLElBQUksQ0FBQyxHQUFrQixFQUFFLEdBQW1CO0lBQ2hFLE1BQU0sUUFBUSxHQUEwQixHQUFHLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQywwQkFBZSxDQUFDLENBQUE7SUFDMUUsTUFBTSxNQUFNLEdBQXFDLEdBQUcsQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUNoRSxpQ0FBeUIsQ0FBQyxNQUFNLENBQ2pDLENBQUE7SUFDRCxNQUFNLE9BQU8sR0FBRyxNQUFNLFFBQVEsQ0FBQyxVQUFVLEVBQUUsQ0FBQTtJQUUzQyxNQUFNLEtBQUssR0FDVCxPQUFPLEdBQUcsQ0FBQyxJQUFJLEtBQUssUUFBUTtRQUM1QixHQUFHLENBQUMsSUFBSTtRQUNSLE9BQU8sSUFBSSxHQUFHLENBQUMsSUFBSTtRQUNuQixHQUFHLENBQUMsSUFBSSxDQUFDLEtBQUssSUFBSSxJQUFJO1FBQ3BCLENBQUMsQ0FBQyxNQUFNLENBQUUsR0FBRyxDQUFDLElBQTJCLENBQUMsS0FBSyxDQUFDO1FBQ2hELENBQUMsQ0FBQyxTQUFTLENBQUE7SUFFZixNQUFNLEtBQUssR0FBRyxNQUFNLFFBQVEsQ0FBQyxZQUFZLENBQUMsT0FBTyxDQUFDLGdCQUFnQixJQUFJLEVBQUUsQ0FBQyxDQUFBO0lBRXpFLElBQUksQ0FBQyxLQUFLLEVBQUUsQ0FBQztRQUNYLE1BQU0sT0FBTyxHQUFHLE1BQU0sUUFBUSxDQUFDLGlCQUFpQixFQUFFLENBQUE7UUFDbEQsR0FBRyxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQyxJQUFJLENBQUM7WUFDbkIsT0FBTyxFQUFFLG9DQUFvQztZQUM3QyxlQUFlLEVBQUUsT0FBTyxFQUFFLEVBQUU7WUFDNUIsVUFBVSxFQUFFLE9BQU8sRUFBRSxVQUFVO1lBQy9CLFlBQVksRUFBRSxPQUFPLEVBQUUsWUFBWTtTQUNwQyxDQUFDLENBQUE7UUFDRixPQUFNO0lBQ1IsQ0FBQztJQUVELDJFQUEyRTtJQUMzRSwyRUFBMkU7SUFDM0UsMkVBQTJFO0lBQzNFLDZFQUE2RTtJQUM3RSwwQ0FBMEM7SUFDMUMsS0FBSyxJQUFBLHVCQUFvQixFQUFDLEdBQUcsQ0FBQyxLQUFLLENBQUM7U0FDakMsR0FBRyxDQUFDO1FBQ0gsS0FBSyxFQUFFO1lBQ0wsV0FBVyxFQUFFLEtBQUssQ0FBQyxFQUFFO1lBQ3JCLEtBQUssRUFBRSxNQUFNLENBQUMsUUFBUSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLFNBQVM7U0FDbEQ7S0FDRixDQUFDO1NBQ0QsS0FBSyxDQUFDLENBQUMsR0FBRyxFQUFFLEVBQUU7UUFDYixNQUFNLENBQUMsS0FBSyxDQUNWLGlCQUFpQixLQUFLLENBQUMsRUFBRSxxQkFDdkIsR0FBRyxZQUFZLEtBQUssQ0FBQyxDQUFDLENBQUMsR0FBRyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FDakQsRUFBRSxDQUNILENBQUE7SUFDSCxDQUFDLENBQUMsQ0FBQTtJQUVKLEdBQUcsQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLENBQUMsSUFBSSxDQUFDLEVBQUUsT0FBTyxFQUFFLEtBQUssQ0FBQyxFQUFFLEVBQUUsQ0FBQyxDQUFBO0FBQzdDLENBQUMifQ==
|
|
@@ -5,18 +5,32 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.config = void 0;
|
|
7
7
|
exports.default = syncPrintfulProductsJob;
|
|
8
|
+
const utils_1 = require("@medusajs/framework/utils");
|
|
8
9
|
const sync_products_1 = __importDefault(require("../workflows/sync-products"));
|
|
10
|
+
const printful_1 = require("../modules/printful");
|
|
9
11
|
/**
|
|
10
12
|
* Optional scheduled full sync.
|
|
11
13
|
* Enable by ensuring jobs are loaded (default in plugins).
|
|
12
14
|
* Schedule: daily at 03:00 UTC — adjust as needed.
|
|
13
15
|
*/
|
|
14
16
|
async function syncPrintfulProductsJob(container) {
|
|
15
|
-
const logger = container.resolve(
|
|
17
|
+
const logger = container.resolve(utils_1.ContainerRegistrationKeys.LOGGER);
|
|
18
|
+
const printful = container.resolve(printful_1.PRINTFUL_MODULE);
|
|
19
|
+
const options = await printful.getOptions();
|
|
20
|
+
const claim = await printful.claimSyncLog(options.syncStaleMinutes ?? 60);
|
|
21
|
+
if (!claim) {
|
|
22
|
+
// A manual sync is already covering the catalog. Not a problem.
|
|
23
|
+
logger.info("Printful scheduled sync skipped: another sync is running");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
16
26
|
logger.info("Printful scheduled sync starting");
|
|
17
27
|
try {
|
|
28
|
+
// Awaited on purpose, unlike the admin route in
|
|
29
|
+
// src/api/admin/printful/sync/route.ts. That route must answer 202 without
|
|
30
|
+
// waiting for a 500-product catalog; this job serves no HTTP response, and
|
|
31
|
+
// awaiting is what gives it counters worth logging.
|
|
18
32
|
const { result } = await (0, sync_products_1.default)(container).run({
|
|
19
|
-
input: {},
|
|
33
|
+
input: { sync_log_id: claim.id },
|
|
20
34
|
});
|
|
21
35
|
logger.info(`Printful scheduled sync done: created=${result.counters.created} updated=${result.counters.updated} failed=${result.counters.failed}`);
|
|
22
36
|
}
|
|
@@ -28,4 +42,4 @@ exports.config = {
|
|
|
28
42
|
name: "printful-sync-products",
|
|
29
43
|
schedule: "0 3 * * *",
|
|
30
44
|
};
|
|
31
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
45
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3luYy1wcm9kdWN0cy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3NyYy9qb2JzL3N5bmMtcHJvZHVjdHMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7O0FBV0EsMENBcUNDO0FBL0NELHFEQUFxRTtBQUNyRSwrRUFBNkQ7QUFDN0Qsa0RBQXFEO0FBR3JEOzs7O0dBSUc7QUFDWSxLQUFLLFVBQVUsdUJBQXVCLENBQ25ELFNBQTBCO0lBRTFCLE1BQU0sTUFBTSxHQUdSLFNBQVMsQ0FBQyxPQUFPLENBQUMsaUNBQXlCLENBQUMsTUFBTSxDQUFDLENBQUE7SUFFdkQsTUFBTSxRQUFRLEdBQTBCLFNBQVMsQ0FBQyxPQUFPLENBQUMsMEJBQWUsQ0FBQyxDQUFBO0lBQzFFLE1BQU0sT0FBTyxHQUFHLE1BQU0sUUFBUSxDQUFDLFVBQVUsRUFBRSxDQUFBO0lBRTNDLE1BQU0sS0FBSyxHQUFHLE1BQU0sUUFBUSxDQUFDLFlBQVksQ0FBQyxPQUFPLENBQUMsZ0JBQWdCLElBQUksRUFBRSxDQUFDLENBQUE7SUFDekUsSUFBSSxDQUFDLEtBQUssRUFBRSxDQUFDO1FBQ1gsZ0VBQWdFO1FBQ2hFLE1BQU0sQ0FBQyxJQUFJLENBQUMsMERBQTBELENBQUMsQ0FBQTtRQUN2RSxPQUFNO0lBQ1IsQ0FBQztJQUVELE1BQU0sQ0FBQyxJQUFJLENBQUMsa0NBQWtDLENBQUMsQ0FBQTtJQUMvQyxJQUFJLENBQUM7UUFDSCxnREFBZ0Q7UUFDaEQsMkVBQTJFO1FBQzNFLDJFQUEyRTtRQUMzRSxvREFBb0Q7UUFDcEQsTUFBTSxFQUFFLE1BQU0sRUFBRSxHQUFHLE1BQU0sSUFBQSx1QkFBb0IsRUFBQyxTQUFTLENBQUMsQ0FBQyxHQUFHLENBQUM7WUFDM0QsS0FBSyxFQUFFLEVBQUUsV0FBVyxFQUFFLEtBQUssQ0FBQyxFQUFFLEVBQUU7U0FDakMsQ0FBQyxDQUFBO1FBQ0YsTUFBTSxDQUFDLElBQUksQ0FDVCx5Q0FBeUMsTUFBTSxDQUFDLFFBQVEsQ0FBQyxPQUFPLFlBQVksTUFBTSxDQUFDLFFBQVEsQ0FBQyxPQUFPLFdBQVcsTUFBTSxDQUFDLFFBQVEsQ0FBQyxNQUFNLEVBQUUsQ0FDdkksQ0FBQTtJQUNILENBQUM7SUFBQyxPQUFPLEdBQUcsRUFBRSxDQUFDO1FBQ2IsTUFBTSxDQUFDLEtBQUssQ0FDVixtQ0FDRSxHQUFHLFlBQVksS0FBSyxDQUFDLENBQUMsQ0FBQyxHQUFHLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUNqRCxFQUFFLENBQ0gsQ0FBQTtJQUNILENBQUM7QUFDSCxDQUFDO0FBRVksUUFBQSxNQUFNLEdBQUc7SUFDcEIsSUFBSSxFQUFFLHdCQUF3QjtJQUM5QixRQUFRLEVBQUUsV0FBVztDQUN0QixDQUFBIn0=
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Migration20260802000000 = void 0;
|
|
4
|
+
const migrations_1 = require("@medusajs/framework/mikro-orm/migrations");
|
|
5
|
+
class Migration20260802000000 extends migrations_1.Migration {
|
|
6
|
+
async up() {
|
|
7
|
+
this.addSql(`
|
|
8
|
+
alter table "printful_sync_log"
|
|
9
|
+
add column if not exists "heartbeat_at" timestamptz null,
|
|
10
|
+
add column if not exists "products_processed" integer not null default 0,
|
|
11
|
+
add column if not exists "products_total" integer not null default 0;
|
|
12
|
+
`);
|
|
13
|
+
// At most one running sync, whatever the state of finished ones. Indexing
|
|
14
|
+
// the constant `true` under a status predicate is what makes the claim
|
|
15
|
+
// atomic: a second concurrent claim collides here rather than racing
|
|
16
|
+
// between a read and a write.
|
|
17
|
+
this.addSql(`
|
|
18
|
+
create unique index if not exists "IDX_printful_sync_log_one_running"
|
|
19
|
+
on "printful_sync_log" ((true))
|
|
20
|
+
where status = 'running' and deleted_at is null;
|
|
21
|
+
`);
|
|
22
|
+
this.addSql(`
|
|
23
|
+
create index if not exists "IDX_printful_sync_log_heartbeat"
|
|
24
|
+
on "printful_sync_log" ("heartbeat_at")
|
|
25
|
+
where status = 'running' and deleted_at is null;
|
|
26
|
+
`);
|
|
27
|
+
}
|
|
28
|
+
async down() {
|
|
29
|
+
this.addSql(`drop index if exists "IDX_printful_sync_log_heartbeat";`);
|
|
30
|
+
this.addSql(`drop index if exists "IDX_printful_sync_log_one_running";`);
|
|
31
|
+
this.addSql(`
|
|
32
|
+
alter table "printful_sync_log"
|
|
33
|
+
drop column if exists "products_total",
|
|
34
|
+
drop column if exists "products_processed",
|
|
35
|
+
drop column if exists "heartbeat_at";
|
|
36
|
+
`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
exports.Migration20260802000000 = Migration20260802000000;
|
|
40
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiTWlncmF0aW9uMjAyNjA4MDIwMDAwMDAuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi9zcmMvbW9kdWxlcy9wcmludGZ1bC9taWdyYXRpb25zL01pZ3JhdGlvbjIwMjYwODAyMDAwMDAwLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQUFBLHlFQUFvRTtBQUVwRSxNQUFhLHVCQUF3QixTQUFRLHNCQUFTO0lBQzNDLEtBQUssQ0FBQyxFQUFFO1FBQ2YsSUFBSSxDQUFDLE1BQU0sQ0FBQzs7Ozs7S0FLWCxDQUFDLENBQUE7UUFFRiwwRUFBMEU7UUFDMUUsdUVBQXVFO1FBQ3ZFLHFFQUFxRTtRQUNyRSw4QkFBOEI7UUFDOUIsSUFBSSxDQUFDLE1BQU0sQ0FBQzs7OztLQUlYLENBQUMsQ0FBQTtRQUVGLElBQUksQ0FBQyxNQUFNLENBQUM7Ozs7S0FJWCxDQUFDLENBQUE7SUFDSixDQUFDO0lBRVEsS0FBSyxDQUFDLElBQUk7UUFDakIsSUFBSSxDQUFDLE1BQU0sQ0FBQyx5REFBeUQsQ0FBQyxDQUFBO1FBQ3RFLElBQUksQ0FBQyxNQUFNLENBQUMsMkRBQTJELENBQUMsQ0FBQTtRQUN4RSxJQUFJLENBQUMsTUFBTSxDQUFDOzs7OztLQUtYLENBQUMsQ0FBQTtJQUNKLENBQUM7Q0FDRjtBQXBDRCwwREFvQ0MifQ==
|
|
@@ -10,6 +10,9 @@ const PrintfulSyncLog = utils_1.model.define("printful_sync_log", {
|
|
|
10
10
|
products_updated: utils_1.model.number().default(0),
|
|
11
11
|
products_failed: utils_1.model.number().default(0),
|
|
12
12
|
error_message: utils_1.model.text().nullable(),
|
|
13
|
+
heartbeat_at: utils_1.model.dateTime().nullable(),
|
|
14
|
+
products_processed: utils_1.model.number().default(0),
|
|
15
|
+
products_total: utils_1.model.number().default(0),
|
|
13
16
|
});
|
|
14
17
|
exports.default = PrintfulSyncLog;
|
|
15
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
18
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHJpbnRmdWwtc3luYy1sb2cuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi9zcmMvbW9kdWxlcy9wcmludGZ1bC9tb2RlbHMvcHJpbnRmdWwtc3luYy1sb2cudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFBQSxxREFBaUQ7QUFFakQsTUFBTSxlQUFlLEdBQUcsYUFBSyxDQUFDLE1BQU0sQ0FBQyxtQkFBbUIsRUFBRTtJQUN4RCxFQUFFLEVBQUUsYUFBSyxDQUFDLEVBQUUsRUFBRSxDQUFDLFVBQVUsRUFBRTtJQUMzQixNQUFNLEVBQUUsYUFBSyxDQUFDLElBQUksRUFBRSxDQUFDLE9BQU8sQ0FBQyxTQUFTLENBQUM7SUFDdkMsVUFBVSxFQUFFLGFBQUssQ0FBQyxRQUFRLEVBQUU7SUFDNUIsV0FBVyxFQUFFLGFBQUssQ0FBQyxRQUFRLEVBQUUsQ0FBQyxRQUFRLEVBQUU7SUFDeEMsZ0JBQWdCLEVBQUUsYUFBSyxDQUFDLE1BQU0sRUFBRSxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUM7SUFDM0MsZ0JBQWdCLEVBQUUsYUFBSyxDQUFDLE1BQU0sRUFBRSxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUM7SUFDM0MsZUFBZSxFQUFFLGFBQUssQ0FBQyxNQUFNLEVBQUUsQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDO0lBQzFDLGFBQWEsRUFBRSxhQUFLLENBQUMsSUFBSSxFQUFFLENBQUMsUUFBUSxFQUFFO0lBQ3RDLFlBQVksRUFBRSxhQUFLLENBQUMsUUFBUSxFQUFFLENBQUMsUUFBUSxFQUFFO0lBQ3pDLGtCQUFrQixFQUFFLGFBQUssQ0FBQyxNQUFNLEVBQUUsQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDO0lBQzdDLGNBQWMsRUFBRSxhQUFLLENBQUMsTUFBTSxFQUFFLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQztDQUMxQyxDQUFDLENBQUE7QUFFRixrQkFBZSxlQUFlLENBQUEifQ==
|
|
@@ -241,6 +241,72 @@ class PrintfulModuleService extends (0, utils_1.MedusaService)({
|
|
|
241
241
|
const logs = await this.listPrintfulSyncLogs({}, { take: 1, order: { started_at: "DESC" } });
|
|
242
242
|
return logs[0] ?? null;
|
|
243
243
|
}
|
|
244
|
+
/**
|
|
245
|
+
* Mark abandoned running syncs as failed.
|
|
246
|
+
*
|
|
247
|
+
* One statement whose predicate does the selecting, so there is no
|
|
248
|
+
* read-then-decide window. A row is abandoned when its heartbeat is older
|
|
249
|
+
* than the stale window, or when it never got one.
|
|
250
|
+
*/
|
|
251
|
+
async reapStaleSyncLogs(staleMinutes) {
|
|
252
|
+
const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000);
|
|
253
|
+
const stale = await this.listPrintfulSyncLogs({
|
|
254
|
+
status: "running",
|
|
255
|
+
$or: [{ heartbeat_at: { $lt: cutoff } }, { heartbeat_at: null }],
|
|
256
|
+
});
|
|
257
|
+
for (const log of stale) {
|
|
258
|
+
await this.updatePrintfulSyncLogs({
|
|
259
|
+
id: log.id,
|
|
260
|
+
status: "failed",
|
|
261
|
+
error_message: "stale_running",
|
|
262
|
+
finished_at: new Date(),
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
return stale.length;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Claim the right to run a sync.
|
|
269
|
+
*
|
|
270
|
+
* Returns the new log on success, or null when another sync already holds
|
|
271
|
+
* the claim. The unique index on `status = 'running'` is what makes this
|
|
272
|
+
* atomic — a second concurrent claim collides in the database rather than
|
|
273
|
+
* racing between a read and a write.
|
|
274
|
+
*/
|
|
275
|
+
async claimSyncLog(staleMinutes) {
|
|
276
|
+
await this.reapStaleSyncLogs(staleMinutes);
|
|
277
|
+
const now = new Date();
|
|
278
|
+
try {
|
|
279
|
+
return await this.createPrintfulSyncLogs({
|
|
280
|
+
status: "running",
|
|
281
|
+
started_at: now,
|
|
282
|
+
heartbeat_at: now,
|
|
283
|
+
products_created: 0,
|
|
284
|
+
products_updated: 0,
|
|
285
|
+
products_failed: 0,
|
|
286
|
+
products_processed: 0,
|
|
287
|
+
products_total: 0,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
catch (err) {
|
|
291
|
+
if (isUniqueViolation(err)) {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
throw err;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/** The sync currently holding the claim, for a 409 body. */
|
|
298
|
+
async getRunningSyncLog() {
|
|
299
|
+
const [log] = await this.listPrintfulSyncLogs({ status: "running" });
|
|
300
|
+
return log ?? null;
|
|
301
|
+
}
|
|
302
|
+
/** Refresh the claim so it is not reaped, and report progress. */
|
|
303
|
+
async heartbeatSyncLog(id, progress = {}) {
|
|
304
|
+
return this.updatePrintfulSyncLogs({
|
|
305
|
+
id,
|
|
306
|
+
heartbeat_at: new Date(),
|
|
307
|
+
...progress,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
244
310
|
}
|
|
245
311
|
exports.default = PrintfulModuleService;
|
|
246
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
312
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VydmljZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uL3NyYy9tb2R1bGVzL3ByaW50ZnVsL3NlcnZpY2UudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7O0FBZ0RBLDRDQUlDO0FBYUQsZ0NBR0M7QUFpQkQsOENBMkJDO0FBaEhELHFEQUF5RDtBQUN6RCxtQ0FBbUM7QUFDbkMsaUVBQTREO0FBRTVELHVGQUE0RDtBQUM1RCwyRkFBZ0U7QUFDaEUsbUZBQXdEO0FBQ3hELDJGQUFnRTtBQUNoRSw2RkFBa0U7QUEyQmxFOzs7Ozs7R0FNRztBQUNVLFFBQUEseUJBQXlCLEdBQUcsU0FBUyxDQUFBO0FBRWxELGdFQUFnRTtBQUNuRCxRQUFBLG9CQUFvQixHQUFHLEVBQUUsQ0FBQTtBQUV0QyxrRUFBa0U7QUFDbEUsU0FBZ0IsZ0JBQWdCLENBQUMsUUFBZ0I7SUFDL0MsTUFBTSxZQUFZLEdBQUcsQ0FBQyxHQUFHLEVBQUUsR0FBRyxJQUFJLENBQUE7SUFDbEMsTUFBTSxTQUFTLEdBQUcsQ0FBQyxHQUFHLEVBQUUsR0FBRyxFQUFFLEdBQUcsSUFBSSxDQUFBO0lBQ3BDLE9BQU8sSUFBSSxDQUFDLEdBQUcsQ0FBQyxZQUFZLEdBQUcsSUFBSSxDQUFDLEdBQUcsQ0FBQyxDQUFDLEVBQUUsUUFBUSxDQUFDLEVBQUUsU0FBUyxDQUFDLENBQUE7QUFDbEUsQ0FBQztBQUVEOzs7Ozs7Ozs7O0dBVUc7QUFDSCxTQUFnQixVQUFVLENBQUMsZUFBdUI7SUFDaEQsTUFBTSxNQUFNLEdBQUcsSUFBQSxtQkFBVSxFQUFDLFFBQVEsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxNQUFNLENBQUMsZUFBZSxDQUFDLENBQUMsQ0FBQyxNQUFNLEVBQUUsQ0FBQTtJQUM1RSxPQUFPLE1BQU0sQ0FBQyxXQUFXLENBQUMsQ0FBQyxDQUFDLENBQUE7QUFDOUIsQ0FBQztBQUVEOzs7Ozs7Ozs7Ozs7OztHQWNHO0FBQ0gsU0FBZ0IsaUJBQWlCLENBQUMsR0FB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
|