medusa-strapi-plugin 0.0.13 → 0.0.16
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/modules/strapi/service.js +34 -1
- package/CHANGELOG.md +79 -0
- package/package.json +1 -1
- package/src/admin/README.md +34 -0
- package/src/admin/lib/sdk.ts +9 -0
- package/src/admin/routes/strapi/page.tsx +44 -0
- package/src/admin/tsconfig.json +24 -0
- package/src/admin/vite-env.d.ts +1 -0
- package/src/api/README.md +123 -0
- package/src/api/admin/strapi/sync/route.ts +38 -0
- package/src/jobs/README.md +36 -0
- package/src/links/README.md +26 -0
- package/src/links/strapi-categories.ts +23 -0
- package/src/links/strapi-collections.ts +23 -0
- package/src/links/strapi-product-variants.ts +23 -0
- package/src/links/strapi-products.ts +23 -0
- package/src/modules/README.md +113 -0
- package/src/modules/strapi/index.ts +10 -0
- package/src/modules/strapi/loader/create-content-models.ts +144 -0
- package/src/modules/strapi/service.ts +576 -0
- package/src/providers/README.md +30 -0
- package/src/subscribers/README.md +54 -0
- package/src/subscribers/create-category.ts +25 -0
- package/src/subscribers/create-collection.ts +25 -0
- package/src/subscribers/create-product.ts +25 -0
- package/src/subscribers/create-variant.ts +25 -0
- package/src/subscribers/delete-category.ts +25 -0
- package/src/subscribers/delete-collection.ts +25 -0
- package/src/subscribers/delete-product.ts +25 -0
- package/src/subscribers/delete-variant.ts +25 -0
- package/src/subscribers/sync-categories.ts +44 -0
- package/src/subscribers/sync-collections.ts +44 -0
- package/src/subscribers/sync-products.ts +44 -0
- package/src/subscribers/update-category.ts +25 -0
- package/src/subscribers/update-collection.ts +25 -0
- package/src/subscribers/update-product.ts +25 -0
- package/src/subscribers/update-variant.ts +25 -0
- package/src/workflows/README.md +69 -0
- package/src/workflows/delete-categories-strapi.ts +20 -0
- package/src/workflows/delete-collections-strapi.ts +20 -0
- package/src/workflows/delete-product-variants-strapi.ts +20 -0
- package/src/workflows/delete-products-strapi.ts +20 -0
- package/src/workflows/steps/delete-categories-strapi.ts +29 -0
- package/src/workflows/steps/delete-collections-strapi.ts +31 -0
- package/src/workflows/steps/delete-product-variants-strapi.ts +31 -0
- package/src/workflows/steps/delete-products-strapi.ts +29 -0
- package/src/workflows/steps/upsert-categories-strapi.ts +84 -0
- package/src/workflows/steps/upsert-collections-strapi.ts +84 -0
- package/src/workflows/steps/upsert-product-variants-strapi.ts +83 -0
- package/src/workflows/steps/upsert-products-strapi.ts +81 -0
- package/src/workflows/upsert-categories-strapi.ts +30 -0
- package/src/workflows/upsert-collections-strapi.ts +30 -0
- package/src/workflows/upsert-product-variants-strapi.ts +30 -0
- package/src/workflows/upsert-products-strapi.ts +36 -0
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Logger,
|
|
3
|
+
ProductCategoryDTO,
|
|
4
|
+
ProductCollectionDTO,
|
|
5
|
+
ProductDTO,
|
|
6
|
+
ProductVariantDTO,
|
|
7
|
+
} from "@medusajs/framework/types";
|
|
8
|
+
import { MedusaError } from "@medusajs/framework/utils";
|
|
9
|
+
import qs from "qs";
|
|
10
|
+
import { ModuleOptions } from "./loader/create-content-models";
|
|
11
|
+
|
|
12
|
+
type InjectedDependencies = {
|
|
13
|
+
logger: Logger;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type EntryProps = {
|
|
17
|
+
documentId: string;
|
|
18
|
+
[key: string]: unknown;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
enum StrapiEntity {
|
|
22
|
+
PRODUCT = "products",
|
|
23
|
+
VARIANT = "product-variants",
|
|
24
|
+
CATEGORY = "categories",
|
|
25
|
+
COLLECTION = "collections",
|
|
26
|
+
CATEGORY_ATTRIBUTE_DESCRIPTION = "category-attribute-descriptions",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default class StrapiModuleService {
|
|
30
|
+
private options: ModuleOptions;
|
|
31
|
+
protected logger: Logger;
|
|
32
|
+
private systemIdKey: string;
|
|
33
|
+
|
|
34
|
+
constructor({ logger }: InjectedDependencies, options: ModuleOptions) {
|
|
35
|
+
this.logger = logger;
|
|
36
|
+
this.options = {
|
|
37
|
+
...options,
|
|
38
|
+
default_locale: options.default_locale || "en",
|
|
39
|
+
system_id_key: options.system_id_key || "systemId",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
this.systemIdKey = (this.options.system_id_key ?? "systemId").trim();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static validateOptions(options: ModuleOptions) {
|
|
46
|
+
if (!options.base_url || !options.api_key) {
|
|
47
|
+
throw new MedusaError(
|
|
48
|
+
MedusaError.Types.INVALID_DATA,
|
|
49
|
+
"Strapi base URL and API key are required",
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Makes an API request to Strapi.
|
|
56
|
+
*/
|
|
57
|
+
private async makeRequest<T>(
|
|
58
|
+
endpoint: string,
|
|
59
|
+
options: Omit<RequestInit, "headers"> = {},
|
|
60
|
+
): Promise<T> {
|
|
61
|
+
const url = `${this.options.base_url}/${endpoint}`;
|
|
62
|
+
try {
|
|
63
|
+
const response = await fetch(url, {
|
|
64
|
+
headers: {
|
|
65
|
+
Authorization: `Bearer ${this.options.api_key}`,
|
|
66
|
+
"Content-Type": "application/json",
|
|
67
|
+
},
|
|
68
|
+
...options,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
let errorDetails = `HTTP ${response.status} ${response.statusText}`;
|
|
73
|
+
try {
|
|
74
|
+
const errorResponse = await response.json();
|
|
75
|
+
if (errorResponse.error) {
|
|
76
|
+
errorDetails += ` - ${JSON.stringify(errorResponse.error)}`;
|
|
77
|
+
this.logger.error(
|
|
78
|
+
`Strapi API request failed for ${url}:`,
|
|
79
|
+
errorResponse,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
} catch (parseError) {
|
|
83
|
+
this.logger.error(
|
|
84
|
+
"Failed to parse error response from Strapi",
|
|
85
|
+
parseError as Error,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
throw new Error(errorDetails);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const responseData = await response.json();
|
|
92
|
+
const { data, error } = responseData;
|
|
93
|
+
|
|
94
|
+
if (error) {
|
|
95
|
+
this.logger.error(`Strapi API returned error for ${url}:`, error);
|
|
96
|
+
this.logger.error("Full response:", responseData);
|
|
97
|
+
throw new Error(error.message || JSON.stringify(error));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return data as T;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
this.logger.error(`API request failed: ${url}`, error as Error);
|
|
103
|
+
throw new MedusaError(
|
|
104
|
+
MedusaError.Types.INVALID_DATA,
|
|
105
|
+
`API request failed: ${(error as Error).message || error}`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Fetches an entity by its system ID.
|
|
112
|
+
*/
|
|
113
|
+
private async getEntityBySystemId<T = Pick<EntryProps, "documentId">>(
|
|
114
|
+
entity: string,
|
|
115
|
+
systemId: string,
|
|
116
|
+
options?: Record<string, unknown>,
|
|
117
|
+
draftAndPublish: boolean = true,
|
|
118
|
+
): Promise<T | undefined> {
|
|
119
|
+
this.logger.debug(`Fetching entity ${entity} with systemId: ${systemId}`);
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const params = qs.stringify({
|
|
123
|
+
filters: { [this.systemIdKey]: { $eq: systemId } },
|
|
124
|
+
fields: ["documentId"],
|
|
125
|
+
...(draftAndPublish ? { status: "draft" } : {}),
|
|
126
|
+
...options,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const result = await this.makeRequest<EntryProps[]>(
|
|
130
|
+
`${entity}?${params}`,
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
return result?.[0] as T;
|
|
134
|
+
} catch (error) {
|
|
135
|
+
this.logger.error(
|
|
136
|
+
`Failed to fetch ${entity} with systemId: ${systemId}`,
|
|
137
|
+
error as Error,
|
|
138
|
+
);
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Creates a new entry in the specified Strapi entity.
|
|
145
|
+
*/
|
|
146
|
+
private async createEntry(
|
|
147
|
+
entity: string,
|
|
148
|
+
payload: Record<
|
|
149
|
+
string,
|
|
150
|
+
string | number | boolean | null | undefined | string[]
|
|
151
|
+
>,
|
|
152
|
+
draftAndPublish: boolean = true,
|
|
153
|
+
): Promise<EntryProps> {
|
|
154
|
+
this.logger.debug(`Creating ${entity} in Strapi`);
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
const queryString = draftAndPublish
|
|
158
|
+
? `?${qs.stringify({ status: "draft" })}`
|
|
159
|
+
: "";
|
|
160
|
+
|
|
161
|
+
return await this.makeRequest<EntryProps>(`${entity}${queryString}`, {
|
|
162
|
+
method: "POST",
|
|
163
|
+
body: JSON.stringify({
|
|
164
|
+
data: {
|
|
165
|
+
...payload,
|
|
166
|
+
},
|
|
167
|
+
}),
|
|
168
|
+
});
|
|
169
|
+
} catch (error) {
|
|
170
|
+
this.logger.error(`Failed to create ${entity} in Strapi`, error as Error);
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Updates an existing entry in the specified Strapi entity.
|
|
177
|
+
*/
|
|
178
|
+
private async updateEntry(
|
|
179
|
+
entity: string,
|
|
180
|
+
documentId: string,
|
|
181
|
+
payload: Record<
|
|
182
|
+
string,
|
|
183
|
+
string | number | boolean | null | undefined | string[]
|
|
184
|
+
>,
|
|
185
|
+
): Promise<EntryProps> {
|
|
186
|
+
this.logger.debug(`Updating ${entity} with documentId: ${documentId}`);
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
return await this.makeRequest<EntryProps>(`${entity}/${documentId}`, {
|
|
190
|
+
method: "PUT",
|
|
191
|
+
body: JSON.stringify({ data: payload }),
|
|
192
|
+
});
|
|
193
|
+
} catch (error) {
|
|
194
|
+
this.logger.error(`Failed to update ${entity} in Strapi`, error as Error);
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Deletes an entry in the specified Strapi entity.
|
|
201
|
+
*/
|
|
202
|
+
/**
|
|
203
|
+
* Deletes an entry in the specified Strapi entity.
|
|
204
|
+
*/
|
|
205
|
+
async deleteEntry(entity: string, documentId: string) {
|
|
206
|
+
try {
|
|
207
|
+
return await this.makeRequest(`${entity}/${documentId}`, {
|
|
208
|
+
method: "DELETE",
|
|
209
|
+
});
|
|
210
|
+
} catch (error) {
|
|
211
|
+
this.logger.error(`Failed to delete ${entity} in Strapi`, error as Error);
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Generic method to upsert an entity
|
|
218
|
+
*/
|
|
219
|
+
async upsertEntity(
|
|
220
|
+
entity: string,
|
|
221
|
+
systemId: string,
|
|
222
|
+
createPayload: Record<
|
|
223
|
+
string,
|
|
224
|
+
string | number | boolean | null | undefined | string[]
|
|
225
|
+
>,
|
|
226
|
+
updatePayload: Record<
|
|
227
|
+
string,
|
|
228
|
+
string | number | boolean | null | undefined | string[]
|
|
229
|
+
>,
|
|
230
|
+
draftAndPublish: boolean = true,
|
|
231
|
+
): Promise<EntryProps> {
|
|
232
|
+
const entry = await this.getEntityBySystemId(
|
|
233
|
+
entity,
|
|
234
|
+
systemId,
|
|
235
|
+
undefined,
|
|
236
|
+
draftAndPublish,
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
if (!entry) {
|
|
240
|
+
this.logger.debug(`No ${entity} found with ID: ${systemId}, creating it`);
|
|
241
|
+
return await this.createEntry(entity, createPayload, draftAndPublish);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
this.logger.debug(`Updating ${entity} with ID: ${systemId}`);
|
|
245
|
+
return await this.updateEntry(entity, entry.documentId, updatePayload);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Auto-populates attribute description relations on a category if they don't exist
|
|
250
|
+
*/
|
|
251
|
+
async populateCategoryOverrides(
|
|
252
|
+
categoryDocumentId: string,
|
|
253
|
+
attributeValueDocumentIds: string[],
|
|
254
|
+
): Promise<void> {
|
|
255
|
+
if (!attributeValueDocumentIds.length) return;
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
// 1. Fetch the category with its current descriptions populated
|
|
259
|
+
const category: any = await this.makeRequest(
|
|
260
|
+
`${StrapiEntity.CATEGORY}/${categoryDocumentId}?populate=attribute_descriptions.attribute_value`,
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
if (!category) return;
|
|
264
|
+
|
|
265
|
+
const existingDescriptions = category.attribute_descriptions || [];
|
|
266
|
+
|
|
267
|
+
// 2. Determine which attribute values are missing
|
|
268
|
+
const existingAttributeValueIds = existingDescriptions
|
|
269
|
+
.map((desc: any) => desc.attribute_value?.documentId)
|
|
270
|
+
.filter(Boolean);
|
|
271
|
+
|
|
272
|
+
const missingAttributeValueIds = attributeValueDocumentIds.filter(
|
|
273
|
+
(id) => !existingAttributeValueIds.includes(id),
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
if (missingAttributeValueIds.length === 0) {
|
|
277
|
+
this.logger.debug(
|
|
278
|
+
`No new descriptions needed for category ${categoryDocumentId}`,
|
|
279
|
+
);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// 3. Create missing relations individually instead of updating the whole Category
|
|
284
|
+
await Promise.all(
|
|
285
|
+
missingAttributeValueIds.map((missingId) =>
|
|
286
|
+
this.createEntry(StrapiEntity.CATEGORY_ATTRIBUTE_DESCRIPTION, {
|
|
287
|
+
category: categoryDocumentId,
|
|
288
|
+
attribute_value: missingId,
|
|
289
|
+
}),
|
|
290
|
+
),
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
this.logger.debug(
|
|
294
|
+
`Added ${missingAttributeValueIds.length} missing attribute descriptions to category ${categoryDocumentId}`,
|
|
295
|
+
);
|
|
296
|
+
} catch (error) {
|
|
297
|
+
this.logger.error(
|
|
298
|
+
`Failed to populate descriptions for category ${categoryDocumentId}`,
|
|
299
|
+
error as Error,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Lists entities based on the provided filter.
|
|
306
|
+
*/
|
|
307
|
+
async list(filter: {
|
|
308
|
+
productId?: string | string[];
|
|
309
|
+
collectionId?: string | string[];
|
|
310
|
+
categoryId?: string | string[];
|
|
311
|
+
variantId?: string | string[];
|
|
312
|
+
context?: {
|
|
313
|
+
locale?: string;
|
|
314
|
+
};
|
|
315
|
+
}) {
|
|
316
|
+
let entity = StrapiEntity.PRODUCT;
|
|
317
|
+
let systemKey = "productId";
|
|
318
|
+
let systemId = filter.productId;
|
|
319
|
+
|
|
320
|
+
if (filter.collectionId) {
|
|
321
|
+
entity = StrapiEntity.COLLECTION;
|
|
322
|
+
systemKey = "collectionId";
|
|
323
|
+
systemId = filter.collectionId;
|
|
324
|
+
}
|
|
325
|
+
if (filter.categoryId) {
|
|
326
|
+
entity = StrapiEntity.CATEGORY;
|
|
327
|
+
systemKey = "categoryId";
|
|
328
|
+
systemId = filter.categoryId;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (filter.variantId) {
|
|
332
|
+
entity = StrapiEntity.VARIANT;
|
|
333
|
+
systemKey = "variantId";
|
|
334
|
+
systemId = filter.variantId;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const systemIdFilter = Array.isArray(systemId) ? systemId : [systemId];
|
|
338
|
+
this.logger.debug(`Fetching entity ${entity} with ID: ${systemIdFilter}`);
|
|
339
|
+
|
|
340
|
+
if (!systemIdFilter.length) {
|
|
341
|
+
this.logger.debug(
|
|
342
|
+
`No system IDs provided for ${entity}, returning empty array`,
|
|
343
|
+
);
|
|
344
|
+
return [];
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const params = qs.stringify({
|
|
348
|
+
filters: { [this.systemIdKey]: { $in: systemIdFilter } },
|
|
349
|
+
// locale: filter.context?.locale || this.options.default_locale,
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
const entries = await this.makeRequest<any[]>(`${entity}?${params}`);
|
|
353
|
+
|
|
354
|
+
return entries.map(({ [this.systemIdKey]: systemId, ...rest }) => ({
|
|
355
|
+
...rest,
|
|
356
|
+
[systemKey]: systemId,
|
|
357
|
+
}));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Upserts a product in Strapi.
|
|
362
|
+
*/
|
|
363
|
+
async upsertProduct(product: ProductDTO) {
|
|
364
|
+
const createPayload = {
|
|
365
|
+
[this.systemIdKey]: product.id,
|
|
366
|
+
title: product.title || "",
|
|
367
|
+
handle: product.handle || "",
|
|
368
|
+
productType: product.type?.value || "",
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const updatePayload = {
|
|
372
|
+
handle: product.handle || "",
|
|
373
|
+
productType: product.type?.value || "",
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const entry = await this.upsertEntity(
|
|
377
|
+
StrapiEntity.PRODUCT,
|
|
378
|
+
product.id,
|
|
379
|
+
createPayload,
|
|
380
|
+
updatePayload,
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
// Create variants if they exist
|
|
384
|
+
if (product.variants?.length) {
|
|
385
|
+
await this.upsertProductVariants(product.variants, entry);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return entry;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Deletes a product in Strapi.
|
|
393
|
+
*/
|
|
394
|
+
async deleteProduct(productId: string) {
|
|
395
|
+
const productEntry = await this.getEntityBySystemId<EntryProps>(
|
|
396
|
+
StrapiEntity.PRODUCT,
|
|
397
|
+
productId,
|
|
398
|
+
{ populate: "variants", fields: ["*"] },
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
if (!productEntry) {
|
|
402
|
+
this.logger.log(`No product found in Strapi ${productId}`);
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
await this.deleteEntry(StrapiEntity.PRODUCT, productEntry.documentId);
|
|
407
|
+
|
|
408
|
+
if (Array.isArray(productEntry.variants)) {
|
|
409
|
+
// Delete variants in parallel
|
|
410
|
+
await Promise.all(
|
|
411
|
+
productEntry.variants.map((v: EntryProps) =>
|
|
412
|
+
this.deleteEntry(StrapiEntity.VARIANT, v.documentId),
|
|
413
|
+
),
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return productId;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Upserts product variants for a given product entry.
|
|
422
|
+
*/
|
|
423
|
+
private async upsertProductVariants(
|
|
424
|
+
variants: ProductVariantDTO[],
|
|
425
|
+
productEntry: EntryProps,
|
|
426
|
+
) {
|
|
427
|
+
// Process variants in parallel
|
|
428
|
+
await Promise.all(
|
|
429
|
+
variants.map(async (variant) => {
|
|
430
|
+
const createPayload = {
|
|
431
|
+
[this.systemIdKey]: variant.id,
|
|
432
|
+
title: variant.title || "",
|
|
433
|
+
product: productEntry.documentId,
|
|
434
|
+
sku: variant.sku || "",
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const updatePayload = {
|
|
438
|
+
product: productEntry.documentId,
|
|
439
|
+
sku: variant.sku || "",
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
await this.upsertEntity(
|
|
443
|
+
StrapiEntity.VARIANT,
|
|
444
|
+
variant.id,
|
|
445
|
+
createPayload,
|
|
446
|
+
updatePayload,
|
|
447
|
+
);
|
|
448
|
+
}),
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Upserts a product variant in Strapi.
|
|
454
|
+
*/
|
|
455
|
+
async upsertProductVariant(variant: ProductVariantDTO) {
|
|
456
|
+
const entry = await this.getEntityBySystemId(
|
|
457
|
+
StrapiEntity.VARIANT,
|
|
458
|
+
variant.id,
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
if (!entry) {
|
|
462
|
+
const productEntry = await this.getEntityBySystemId(
|
|
463
|
+
StrapiEntity.PRODUCT,
|
|
464
|
+
variant.product_id as string,
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
if (!productEntry) {
|
|
468
|
+
this.logger.log(
|
|
469
|
+
`No product found in Strapi (ID: ${variant.product_id}) — skipping creation of variant ${variant.id}`,
|
|
470
|
+
);
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return await this.createEntry(StrapiEntity.VARIANT, {
|
|
475
|
+
[this.systemIdKey]: variant.id,
|
|
476
|
+
title: variant.title || "",
|
|
477
|
+
product: productEntry.documentId,
|
|
478
|
+
sku: variant.sku || "",
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return await this.updateEntry(StrapiEntity.VARIANT, entry.documentId, {
|
|
483
|
+
sku: variant.sku || "",
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Deletes a product variant in Strapi.
|
|
489
|
+
*/
|
|
490
|
+
async deleteProductVariant(variantId: string) {
|
|
491
|
+
const entry = await this.getEntityBySystemId(
|
|
492
|
+
StrapiEntity.VARIANT,
|
|
493
|
+
variantId,
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
if (!entry) {
|
|
497
|
+
this.logger.log(`No product variant found in Strapi ${variantId}`);
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
await this.deleteEntry(StrapiEntity.VARIANT, entry.documentId);
|
|
502
|
+
return variantId;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Upserts a collection in Strapi.
|
|
507
|
+
*/
|
|
508
|
+
async upsertCollection(collection: ProductCollectionDTO) {
|
|
509
|
+
return await this.upsertEntity(
|
|
510
|
+
StrapiEntity.COLLECTION,
|
|
511
|
+
collection.id,
|
|
512
|
+
{
|
|
513
|
+
[this.systemIdKey]: collection.id,
|
|
514
|
+
title: collection.title || "",
|
|
515
|
+
handle: collection.handle || "",
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
handle: collection.handle || "",
|
|
519
|
+
},
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Deletes a collection in Strapi.
|
|
525
|
+
*/
|
|
526
|
+
async deleteCollection(collectionId: string) {
|
|
527
|
+
const entry = await this.getEntityBySystemId(
|
|
528
|
+
StrapiEntity.COLLECTION,
|
|
529
|
+
collectionId,
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
if (!entry) {
|
|
533
|
+
this.logger.log(`No collection found in Strapi ${collectionId}`);
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
await this.deleteEntry(StrapiEntity.COLLECTION, entry.documentId);
|
|
538
|
+
return collectionId;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Upserts a product category in Strapi.
|
|
543
|
+
*/
|
|
544
|
+
async upsertCategory(category: ProductCategoryDTO) {
|
|
545
|
+
return await this.upsertEntity(
|
|
546
|
+
StrapiEntity.CATEGORY,
|
|
547
|
+
category.id,
|
|
548
|
+
{
|
|
549
|
+
[this.systemIdKey]: category.id,
|
|
550
|
+
title: category.name || "",
|
|
551
|
+
handle: category.handle || "",
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
handle: category.handle || "",
|
|
555
|
+
},
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Deletes a product category in Strapi.
|
|
561
|
+
*/
|
|
562
|
+
async deleteCategory(categoryId: string) {
|
|
563
|
+
const entry = await this.getEntityBySystemId(
|
|
564
|
+
StrapiEntity.CATEGORY,
|
|
565
|
+
categoryId,
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
if (!entry) {
|
|
569
|
+
this.logger.log(`No category found in Strapi ${categoryId}`);
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
await this.deleteEntry(StrapiEntity.CATEGORY, entry.documentId);
|
|
574
|
+
return categoryId;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
## Module Providers
|
|
2
|
+
|
|
3
|
+
You can create module providers, such as Notification or File Module Providers, under a sub-directory of this directory. For example, `src/providers/my-notification`.
|
|
4
|
+
|
|
5
|
+
Then, you register them in the Medusa application as `plugin-name/providers/my-notification`:
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
module.exports = defineConfig({
|
|
9
|
+
// ...
|
|
10
|
+
modules: [
|
|
11
|
+
{
|
|
12
|
+
resolve: "@medusajs/medusa/notification",
|
|
13
|
+
options: {
|
|
14
|
+
providers: [
|
|
15
|
+
{
|
|
16
|
+
resolve: "@myorg/plugin-name/providers/my-notification",
|
|
17
|
+
id: "my-notification",
|
|
18
|
+
options: {
|
|
19
|
+
channels: ["email"],
|
|
20
|
+
// provider options...
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/plugins/create).
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Custom Subscribers
|
|
2
|
+
|
|
3
|
+
Subscribers handle events emitted in the Medusa application.
|
|
4
|
+
|
|
5
|
+
The subscriber is created in a TypeScript or JavaScript file under the `src/subscribers` directory.
|
|
6
|
+
|
|
7
|
+
For example, create the file `src/subscribers/product-created.ts` with the following content:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { type SubscriberConfig } from "@medusajs/framework";
|
|
11
|
+
|
|
12
|
+
// subscriber function
|
|
13
|
+
export default async function productCreateHandler() {
|
|
14
|
+
console.log("A product was created");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// subscriber config
|
|
18
|
+
export const config: SubscriberConfig = {
|
|
19
|
+
event: "product.created",
|
|
20
|
+
};
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
A subscriber file must export:
|
|
24
|
+
|
|
25
|
+
- The subscriber function that is an asynchronous function executed whenever the associated event is triggered.
|
|
26
|
+
- A configuration object defining the event this subscriber is listening to.
|
|
27
|
+
|
|
28
|
+
## Subscriber Parameters
|
|
29
|
+
|
|
30
|
+
A subscriber receives an object having the following properties:
|
|
31
|
+
|
|
32
|
+
- `event`: An object holding the event's details. It has a `data` property, which is the event's data payload.
|
|
33
|
+
- `container`: The Medusa container. Use it to resolve modules' main services and other registered resources.
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import type { SubscriberArgs, SubscriberConfig } from "@medusajs/framework";
|
|
37
|
+
|
|
38
|
+
export default async function productCreateHandler({
|
|
39
|
+
event: { data },
|
|
40
|
+
container,
|
|
41
|
+
}: SubscriberArgs<{ id: string }>) {
|
|
42
|
+
const productId = data.id;
|
|
43
|
+
|
|
44
|
+
const productModuleService = container.resolve("product");
|
|
45
|
+
|
|
46
|
+
const product = await productModuleService.retrieveProduct(productId);
|
|
47
|
+
|
|
48
|
+
console.log(`The product ${product.title} was created`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const config: SubscriberConfig = {
|
|
52
|
+
event: "product.created",
|
|
53
|
+
};
|
|
54
|
+
```
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type SubscriberArgs,
|
|
3
|
+
type SubscriberConfig,
|
|
4
|
+
} from "@medusajs/framework";
|
|
5
|
+
import { ContainerRegistrationKeys } from "@medusajs/framework/utils";
|
|
6
|
+
import { upsertCategoriesStrapiWorkflow } from "../workflows/upsert-categories-strapi";
|
|
7
|
+
|
|
8
|
+
export default async function handleCategoryCreate({
|
|
9
|
+
event: { data },
|
|
10
|
+
container,
|
|
11
|
+
}: SubscriberArgs<{ id: string }>) {
|
|
12
|
+
const logger = container.resolve(ContainerRegistrationKeys.LOGGER);
|
|
13
|
+
|
|
14
|
+
await upsertCategoriesStrapiWorkflow(container as any).run({
|
|
15
|
+
input: {
|
|
16
|
+
category_ids: [data.id],
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
logger.log("Category created in Strapi");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const config: SubscriberConfig = {
|
|
24
|
+
event: "product-category.created",
|
|
25
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type SubscriberArgs,
|
|
3
|
+
type SubscriberConfig,
|
|
4
|
+
} from "@medusajs/framework";
|
|
5
|
+
import { ContainerRegistrationKeys } from "@medusajs/framework/utils";
|
|
6
|
+
import { upsertCollectionsStrapiWorkflow } from "../workflows/upsert-collections-strapi";
|
|
7
|
+
|
|
8
|
+
export default async function handleCollectionCreate({
|
|
9
|
+
event: { data },
|
|
10
|
+
container,
|
|
11
|
+
}: SubscriberArgs<{ id: string }>) {
|
|
12
|
+
const logger = container.resolve(ContainerRegistrationKeys.LOGGER);
|
|
13
|
+
|
|
14
|
+
await upsertCollectionsStrapiWorkflow(container as any).run({
|
|
15
|
+
input: {
|
|
16
|
+
collection_ids: [data.id],
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
logger.log("Collection created in Strapi");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const config: SubscriberConfig = {
|
|
24
|
+
event: "product-collection.created",
|
|
25
|
+
};
|