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,113 @@
|
|
|
1
|
+
# Custom Module
|
|
2
|
+
|
|
3
|
+
A module is a package of reusable functionalities. It can be integrated into your Medusa application without affecting the overall system. You can create a module as part of a plugin.
|
|
4
|
+
|
|
5
|
+
Learn more about modules in [this documentation](https://docs.medusajs.com/learn/fundamentals/modules).
|
|
6
|
+
|
|
7
|
+
To create a module:
|
|
8
|
+
|
|
9
|
+
## 1. Create a Data Model
|
|
10
|
+
|
|
11
|
+
A data model represents a table in the database. You create a data model in a TypeScript or JavaScript file under the `models` directory of a module.
|
|
12
|
+
|
|
13
|
+
For example, create the file `src/modules/blog/models/post.ts` with the following content:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { model } from "@medusajs/framework/utils";
|
|
17
|
+
|
|
18
|
+
const Post = model.define("post", {
|
|
19
|
+
id: model.id().primaryKey(),
|
|
20
|
+
title: model.text(),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export default Post;
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 2. Create a Service
|
|
27
|
+
|
|
28
|
+
A module must define a service. A service is a TypeScript or JavaScript class holding methods related to a business logic or commerce functionality.
|
|
29
|
+
|
|
30
|
+
For example, create the file `src/modules/blog/service.ts` with the following content:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { MedusaService } from "@medusajs/framework/utils";
|
|
34
|
+
import Post from "./models/post";
|
|
35
|
+
|
|
36
|
+
class BlogModuleService extends MedusaService({
|
|
37
|
+
Post,
|
|
38
|
+
}) {}
|
|
39
|
+
|
|
40
|
+
export default BlogModuleService;
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 3. Export Module Definition
|
|
44
|
+
|
|
45
|
+
A module must have an `index.ts` file in its root directory that exports its definition. The definition specifies the main service of the module.
|
|
46
|
+
|
|
47
|
+
For example, create the file `src/modules/blog/index.ts` with the following content:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import BlogModuleService from "./service";
|
|
51
|
+
import { Module } from "@medusajs/framework/utils";
|
|
52
|
+
|
|
53
|
+
export const BLOG_MODULE = "blog";
|
|
54
|
+
|
|
55
|
+
export default Module(BLOG_MODULE, {
|
|
56
|
+
service: BlogModuleService,
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## 4. Generate Migrations
|
|
61
|
+
|
|
62
|
+
To generate migrations for your module, run the following command in the plugin's directory:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npx medusa plugin:db:generate
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Use Module
|
|
69
|
+
|
|
70
|
+
You can use the module in customizations within the plugin or within the Medusa application using this plugin. When the plugin is added to a Medusa application, all its modules are registered as well.
|
|
71
|
+
|
|
72
|
+
For example, to use the module in an API route:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { MedusaRequest, MedusaResponse } from "@medusajs/framework";
|
|
76
|
+
import BlogModuleService from "../../../modules/blog/service";
|
|
77
|
+
import { BLOG_MODULE } from "../../../modules/blog";
|
|
78
|
+
|
|
79
|
+
export async function GET(
|
|
80
|
+
req: MedusaRequest,
|
|
81
|
+
res: MedusaResponse,
|
|
82
|
+
): Promise<void> {
|
|
83
|
+
const blogModuleService: BlogModuleService = req.scope.resolve(BLOG_MODULE);
|
|
84
|
+
|
|
85
|
+
const posts = await blogModuleService.listPosts();
|
|
86
|
+
|
|
87
|
+
res.json({
|
|
88
|
+
posts,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Module Options
|
|
94
|
+
|
|
95
|
+
When you register the plugin in the Medusa application, it can accept options. These options are passed to the modules within the plugin:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
import { defineConfig } from "@medusajs/framework/utils";
|
|
99
|
+
|
|
100
|
+
module.exports = defineConfig({
|
|
101
|
+
// ...
|
|
102
|
+
plugins: [
|
|
103
|
+
{
|
|
104
|
+
resolve: "@myorg/plugin-name",
|
|
105
|
+
options: {
|
|
106
|
+
apiKey: process.env.API_KEY,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Learn more about module options in [this documentation](https://docs.medusajs.com/learn/fundamentals/modules/options).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Module } from "@medusajs/framework/utils";
|
|
2
|
+
import createContentModelsLoader from "./loader/create-content-models";
|
|
3
|
+
import StrapiModuleService from "./service";
|
|
4
|
+
|
|
5
|
+
export const STRAPI_MODULE = "strapi";
|
|
6
|
+
|
|
7
|
+
export default Module(STRAPI_MODULE, {
|
|
8
|
+
service: StrapiModuleService,
|
|
9
|
+
loaders: [createContentModelsLoader],
|
|
10
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { LoaderOptions } from "@medusajs/framework/types";
|
|
2
|
+
import {
|
|
3
|
+
ContainerRegistrationKeys,
|
|
4
|
+
MedusaError,
|
|
5
|
+
} from "@medusajs/framework/utils";
|
|
6
|
+
import qs from "qs";
|
|
7
|
+
|
|
8
|
+
export type ModuleOptions = {
|
|
9
|
+
base_url: string;
|
|
10
|
+
api_key: string;
|
|
11
|
+
default_locale?: string;
|
|
12
|
+
system_id_key?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default async function syncContentModelsLoader({
|
|
16
|
+
container,
|
|
17
|
+
options,
|
|
18
|
+
}: LoaderOptions<ModuleOptions>) {
|
|
19
|
+
const logger = container.resolve(ContainerRegistrationKeys.LOGGER);
|
|
20
|
+
|
|
21
|
+
if (!options?.base_url || !options?.api_key) {
|
|
22
|
+
throw new MedusaError(
|
|
23
|
+
MedusaError.Types.INVALID_DATA,
|
|
24
|
+
"Strapi api key and base URL are required",
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const systemIdKey = options.system_id_key || "systemId";
|
|
29
|
+
|
|
30
|
+
logger.debug(`Strapi baseURL: ${options.base_url}`);
|
|
31
|
+
|
|
32
|
+
// Validate that required content types exist with correct structure
|
|
33
|
+
await validateContentTypes(options, logger, systemIdKey);
|
|
34
|
+
|
|
35
|
+
logger.info("Connected to Strapi");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function validateContentTypes(
|
|
39
|
+
options: ModuleOptions,
|
|
40
|
+
logger: any,
|
|
41
|
+
systemIdKey: string,
|
|
42
|
+
) {
|
|
43
|
+
const requiredContentTypes = [
|
|
44
|
+
{
|
|
45
|
+
name: "products",
|
|
46
|
+
requiredFields: ["title", systemIdKey, "handle", "productType"],
|
|
47
|
+
relations: { variants: { fields: ["title", systemIdKey, "sku"] } },
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "product-variants",
|
|
51
|
+
requiredFields: ["title", systemIdKey, "sku"],
|
|
52
|
+
relations: { product: {} },
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "categories",
|
|
56
|
+
requiredFields: ["title", systemIdKey, "handle"],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "collections",
|
|
60
|
+
requiredFields: ["title", systemIdKey, "handle"],
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
for (const contentType of requiredContentTypes) {
|
|
65
|
+
await validateContentType(options, logger, contentType, systemIdKey);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function validateContentType(
|
|
70
|
+
options: ModuleOptions,
|
|
71
|
+
logger: any,
|
|
72
|
+
contentType: any,
|
|
73
|
+
systemIdKey: string,
|
|
74
|
+
) {
|
|
75
|
+
logger.debug(`Validating content type: ${contentType.name}`);
|
|
76
|
+
|
|
77
|
+
const params = qs.stringify({
|
|
78
|
+
fields: contentType.requiredFields,
|
|
79
|
+
populate: contentType.relations || {},
|
|
80
|
+
pagination: { limit: 1 },
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const response = await fetch(
|
|
84
|
+
`${options.base_url}/${contentType.name}?${params}`,
|
|
85
|
+
{
|
|
86
|
+
headers: {
|
|
87
|
+
Authorization: `Bearer ${options.api_key}`,
|
|
88
|
+
"Content-Type": "application/json",
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
let errorDetails = `HTTP ${response.status} ${response.statusText}`;
|
|
95
|
+
let errorMessage = `Content type '${contentType.name}' validation failed: ${errorDetails}`;
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const errorResponse = await response.json();
|
|
99
|
+
if (errorResponse.error) {
|
|
100
|
+
errorDetails += ` - ${JSON.stringify(errorResponse.error)}`;
|
|
101
|
+
logger.error(
|
|
102
|
+
`Strapi content type validation failed for '${contentType.name}':`,
|
|
103
|
+
errorResponse,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
if (response.status === 404) {
|
|
107
|
+
errorMessage = `Content type '${contentType.name}' does not exist in Strapi. Please create it using the schema files provided in the 'strapi-schemas' directory.`;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} catch (parseError) {
|
|
111
|
+
logger.error("Failed to parse error response from Strapi", parseError);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
throw new MedusaError(MedusaError.Types.INVALID_DATA, errorMessage);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const responseData = await response.json();
|
|
118
|
+
const { error } = responseData;
|
|
119
|
+
|
|
120
|
+
if (error) {
|
|
121
|
+
logger.error(
|
|
122
|
+
`Strapi returned an error for content type '${contentType.name}':`,
|
|
123
|
+
error,
|
|
124
|
+
);
|
|
125
|
+
logger.error("Full response:", responseData);
|
|
126
|
+
|
|
127
|
+
let errorMessage = `Content type '${contentType.name}' validation failed: ${
|
|
128
|
+
error.message || JSON.stringify(error)
|
|
129
|
+
}`;
|
|
130
|
+
|
|
131
|
+
// Check if it's a field-related error
|
|
132
|
+
if (error.message && error.message.includes("field")) {
|
|
133
|
+
errorMessage += `\n\nRequired fields for '${
|
|
134
|
+
contentType.name
|
|
135
|
+
}': ${contentType.requiredFields.join(", ")}`;
|
|
136
|
+
errorMessage += `\nPlease ensure all required fields exist in your Strapi content type.`;
|
|
137
|
+
errorMessage += `\nRefer to the schema files in the 'strapi-schemas' directory for the correct structure.`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
throw new MedusaError(MedusaError.Types.INVALID_DATA, errorMessage);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
logger.debug(`Content type '${contentType.name}' validation passed`);
|
|
144
|
+
}
|