├── src ├── index.ts ├── .npmignore ├── api │ ├── feed │ │ ├── get-xml.ts │ │ ├── get-json.ts │ │ └── index.ts │ ├── libs │ │ └── feeds.ts │ ├── index.ts │ └── README.md ├── admin │ └── routes │ │ └── feed │ │ ├── types │ │ └── table.ts │ │ ├── page.tsx │ │ └── components │ │ ├── FeedXmlView.tsx │ │ ├── FeedJsonView.tsx │ │ └── Table.tsx ├── loaders │ └── README.md ├── migrations │ └── README.md ├── models │ └── README.md ├── subscribers │ └── README.md └── services │ ├── README.md │ └── feed.ts ├── tsconfig.spec.json ├── tsconfig.admin.json ├── .gitignore ├── .npmignore ├── tsconfig.server.json ├── tsconfig.json ├── README.md ├── medusa-config.js └── package.json /src/index.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src"], 4 | "exclude": ["dist", "node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /src/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | yarn.lock 3 | tsconfig.json 4 | tsconfig.spec.json 5 | .eslintignore 6 | .gitignore 7 | src/ 8 | 9 | .idea/ 10 | .github/ 11 | .env 12 | *.log 13 | -------------------------------------------------------------------------------- /tsconfig.admin.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "esnext" 5 | }, 6 | "include": ["src/admin"], 7 | "exclude": ["**/*.spec.js"] 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | 3 | .env 4 | .DS_Store 5 | .yalc 6 | 7 | node_modules/ 8 | build 9 | .cache 10 | yalc.lock 11 | yarn.lock 12 | package-lock.json 13 | pnpm-lock.yaml 14 | .github/ 15 | .idea/ 16 | .eslintignore 17 | 18 | -------------------------------------------------------------------------------- /src/api/feed/get-xml.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { handleFeed } from '../libs/feeds'; 3 | 4 | export default async (req: Request, res: Response): Promise => { 5 | handleFeed(req, res, 'xml'); 6 | }; 7 | -------------------------------------------------------------------------------- /src/api/feed/get-json.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { handleFeed } from '../libs/feeds'; 3 | 4 | export default async (req: Request, res: Response): Promise => { 5 | handleFeed(req, res, 'json'); 6 | }; 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | yarn.lock 3 | tsconfig.json 4 | tsconfig.spec.json 5 | tsconfig.server.json 6 | tsconfig.admin.json 7 | .eslintignore 8 | .gitignore 9 | src/ 10 | .cache 11 | build 12 | .idea/ 13 | .github/ 14 | .env 15 | *.log 16 | -------------------------------------------------------------------------------- /tsconfig.server.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | /* Emit a single file with source maps instead of having a separate file. */ 5 | "inlineSourceMap": true 6 | }, 7 | "exclude": ["src/admin", "**/*.spec.js"] 8 | } 9 | -------------------------------------------------------------------------------- /src/admin/routes/feed/types/table.ts: -------------------------------------------------------------------------------- 1 | type FeedQuery = {}; 2 | 3 | type FeedResponseJson = { 4 | rss: { 5 | channel: { 6 | item: any; 7 | }; 8 | }; 9 | }; 10 | 11 | type FeedResponse = { 12 | xml: string; 13 | }; 14 | 15 | export { FeedQuery, FeedResponseJson, FeedResponse }; 16 | -------------------------------------------------------------------------------- /src/api/feed/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { wrapHandler } from '@medusajs/medusa'; 3 | 4 | import getPublicXml from './get-xml'; 5 | import getPublicJson from './get-json'; 6 | 7 | const attachFeedRoutes = (feedRouter: Router) => { 8 | feedRouter.get('/xml', wrapHandler(getPublicXml)); 9 | feedRouter.get('/json', wrapHandler(getPublicJson)); 10 | }; 11 | 12 | export { attachFeedRoutes }; 13 | -------------------------------------------------------------------------------- /src/admin/routes/feed/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { RouteConfig } from '@medusajs/admin'; 3 | 4 | import FeedTable from './components/Table'; 5 | import FeedJsonView from './components/FeedJsonView'; 6 | import FeedXmlView from './components/FeedXmlView'; 7 | 8 | const FeedPage: React.FC = () => { 9 | return ( 10 |
11 |
12 | 13 | 14 |
15 | 16 |
17 | ); 18 | }; 19 | 20 | export const config: RouteConfig = { 21 | link: { 22 | label: 'Product Feed', 23 | }, 24 | }; 25 | export default FeedPage; 26 | -------------------------------------------------------------------------------- /src/loaders/README.md: -------------------------------------------------------------------------------- 1 | # Custom loader 2 | 3 | The loader allows you have access to the Medusa service container. This allows you to access the database and the services registered on the container. 4 | you can register custom registrations in the container or run custom code on startup. 5 | 6 | ```ts 7 | // src/loaders/my-loader.ts 8 | 9 | import { AwilixContainer } from 'awilix' 10 | 11 | /** 12 | * 13 | * @param container The container in which the registrations are made 14 | * @param config The options of the plugin or the entire config object 15 | */ 16 | export default (container: AwilixContainer, config: Record): void | Promise => { 17 | /* Implement your own loader. */ 18 | } 19 | ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "module": "commonjs", 5 | "allowJs": true, 6 | "checkJs": false, 7 | "jsx": "react-jsx", 8 | "declaration": true, 9 | "outDir": "./dist", 10 | "rootDir": "./src", 11 | "experimentalDecorators": true, 12 | "emitDecoratorMetadata": true, 13 | "noEmit": false, 14 | "strict": false, 15 | "moduleResolution": "node", 16 | "esModuleInterop": true, 17 | "resolveJsonModule": true, 18 | "skipLibCheck": true, 19 | "forceConsistentCasingInFileNames": true 20 | }, 21 | "include": ["src/"], 22 | "exclude": [ 23 | "dist", 24 | "build", 25 | ".cache", 26 | "tests", 27 | "**/*.spec.js", 28 | "**/*.spec.ts", 29 | "node_modules", 30 | ".eslintrc.js" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # medusa-plugin-feeds 2 | 3 | This is a plugin for MedusaJS designed to create basic Shopping feeds for Google and Facebook. The project is just getting started and there's still lots of work to be done. 4 | 5 | ## Features 6 | 7 | - Exposes custom endpoints to fetch feeds in XML or JSON format: 8 | - `/feed/xml` 9 | - `/feed/json` 10 | - `/admin/feed/xml` 11 | - `/admin/feed/json` 12 | 13 | The last two endpoints are used internally by the admin panel to fetch the feeds. 14 | 15 | ## Usage 16 | 17 | ### Plugin Options 18 | 19 | To enable Admin UI extensions, please add the below into your plugins object inside medusa-config.js 20 | 21 | `{ resolve: 'medusa-plugin-feeds', options: { enableUI: true } }`, 22 | 23 | ## Contributing 24 | 25 | The project is in its early stages. Contributions are welcome 💚🙏. 26 | -------------------------------------------------------------------------------- /src/migrations/README.md: -------------------------------------------------------------------------------- 1 | # Custom migrations 2 | 3 | You may define custom models (entities) that will be registered on the global container by creating files in the `src/models` directory that export an instance of `BaseEntity`. 4 | In that case you also need to provide a migration in order to create the table in the database. 5 | 6 | ## Example 7 | 8 | ### 1. Create the migration 9 | 10 | See [How to Create Migrations](https://docs.medusajs.com/advanced/backend/migrations/) in the documentation. 11 | 12 | ```ts 13 | // src/migration/my-migration.ts 14 | 15 | import { MigrationInterface, QueryRunner } from "typeorm" 16 | 17 | export class MyMigration1617703530229 implements MigrationInterface { 18 | name = "myMigration1617703530229" 19 | 20 | public async up(queryRunner: QueryRunner): Promise { 21 | // write you migration here 22 | } 23 | 24 | public async down(queryRunner: QueryRunner): Promise { 25 | // write you migration here 26 | } 27 | } 28 | 29 | ``` -------------------------------------------------------------------------------- /src/api/libs/feeds.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import xml2js from 'xml2js'; 3 | 4 | const handleFeed = async ( 5 | req: Request, 6 | res: Response, 7 | format: 'xml' | 'json', 8 | isAdmin: boolean = false 9 | ) => { 10 | try { 11 | const feedService = req.scope.resolve('feedService'); 12 | 13 | if (!feedService) { 14 | return res.status(500).send('Feed Service not resolved.'); 15 | } 16 | 17 | const xml = await feedService.createFeed(); 18 | 19 | if (format === 'xml') { 20 | res.set('Content-Type', 'text/xml'); 21 | res.send(xml); 22 | } else { 23 | xml2js.parseString(xml, (err, result) => { 24 | if (err) { 25 | console.error(err); 26 | return res.status(500).send('Error converting XML to JSON.'); 27 | } 28 | res.json(result); 29 | }); 30 | } 31 | 32 | return; 33 | } catch (error) { 34 | console.error(error); 35 | res.status(500).send('An error occurred while generating the feed.'); 36 | return; 37 | } 38 | }; 39 | 40 | export { handleFeed }; 41 | -------------------------------------------------------------------------------- /src/admin/routes/feed/components/FeedXmlView.tsx: -------------------------------------------------------------------------------- 1 | import { useAdminCustomQuery } from 'medusa-react'; 2 | import { FeedQuery, FeedResponse } from '../types/table'; 3 | import { Container, Heading } from '@medusajs/ui'; 4 | import XMLViewer from 'react-xml-viewer'; 5 | 6 | const FeedXmlView: React.FC = () => { 7 | const { data, isLoading } = useAdminCustomQuery( 8 | 'feed/xml', 9 | ['feed'], 10 | {} 11 | ); 12 | 13 | let feedXML = data?.xml; 14 | 15 | // TODO: Hack 16 | if (typeof data === 'object' && !feedXML) { 17 | feedXML = Object.values(data).join(''); 18 | } 19 | return ( 20 | 21 |
22 | Product XML Feed 23 | {isLoading && Loading...} 24 | {feedXML && ( 25 |
26 | 31 |
32 | )} 33 |
34 |
35 | ); 36 | }; 37 | 38 | export default FeedXmlView; 39 | -------------------------------------------------------------------------------- /src/admin/routes/feed/components/FeedJsonView.tsx: -------------------------------------------------------------------------------- 1 | import { CodeBlock, Container, Heading } from '@medusajs/ui'; 2 | import { useAdminCustomQuery } from 'medusa-react'; 3 | 4 | import { FeedQuery, FeedResponseJson } from '../types/table'; 5 | 6 | const FeedJsonView: React.FC = () => { 7 | const { data, isLoading } = useAdminCustomQuery( 8 | 'feed/json', // path 9 | ['feed'], // queryKey 10 | { 11 | // any query parameters you want to send 12 | } 13 | ); 14 | 15 | const feedItems = data?.rss?.channel[0]?.item || []; 16 | 17 | const snippets = [ 18 | { 19 | label: 'Feed', 20 | language: 'json', 21 | code: JSON.stringify(feedItems, null, 2), 22 | }, 23 | ]; 24 | 25 | return ( 26 | 27 |
28 | Product JSON Feed 29 | {isLoading && Loading...} 30 | {feedItems.length > 0 && ( 31 | 35 | 36 | 37 | 38 | )} 39 |
40 |
41 | ); 42 | }; 43 | 44 | export default FeedJsonView; 45 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { getConfigFile } from 'medusa-core-utils'; 3 | import { ConfigModule } from '@medusajs/medusa/dist/types/global'; 4 | import cors from 'cors'; 5 | import { handleFeed } from './libs/feeds'; 6 | import { attachFeedRoutes } from './feed'; 7 | 8 | const router: (rootDirectory: string, options: any) => Router = ( 9 | rootDirectory, 10 | options 11 | ) => { 12 | const router = Router(); 13 | const { configModule } = getConfigFile( 14 | rootDirectory, 15 | 'medusa-config' 16 | ); 17 | const { projectConfig } = configModule; 18 | 19 | const adminCorsOptions = { 20 | origin: projectConfig.admin_cors.split(','), 21 | credentials: true, 22 | }; 23 | 24 | const feedRouter = Router(); 25 | 26 | router.use('/feed', feedRouter); 27 | 28 | attachFeedRoutes(feedRouter); 29 | 30 | router.options('/admin/feed/xml', cors(adminCorsOptions)); 31 | router.options('/admin/feed/json', cors(adminCorsOptions)); 32 | 33 | router.get( 34 | '/admin/feed/xml', 35 | cors(adminCorsOptions), 36 | async (req, res, next) => handleFeed(req, res, 'xml', true) 37 | ); 38 | 39 | router.get( 40 | '/admin/feed/json', 41 | cors(adminCorsOptions), 42 | async (req, res, next) => handleFeed(req, res, 'json', true) 43 | ); 44 | 45 | return router; 46 | }; 47 | 48 | export default router; 49 | -------------------------------------------------------------------------------- /src/api/README.md: -------------------------------------------------------------------------------- 1 | # Custom endpoints 2 | 3 | You may define custom endpoints by putting files in the `/api` directory that export functions returning an express router or a collection of express routers. 4 | 5 | ```ts 6 | import { Router } from "express" 7 | import { getConfigFile } from "medusa-core-utils" 8 | import { getStoreRouter } from "./routes/store" 9 | import { ConfigModule } from "@medusajs/medusa/dist/types/global"; 10 | 11 | export default (rootDirectory) => { 12 | const { configModule: { projectConfig } } = getConfigFile( 13 | rootDirectory, 14 | "medusa-config" 15 | ) as { configModule: ConfigModule } 16 | 17 | const storeCorsOptions = { 18 | origin: projectConfig.store_cors.split(","), 19 | credentials: true, 20 | } 21 | 22 | const storeRouter = getStoreRouter(storeCorsOptions) 23 | 24 | return [storeRouter] 25 | } 26 | ``` 27 | 28 | A global container is available on `req.scope` to allow you to use any of the registered services from the core, installed plugins or your local project: 29 | ```js 30 | import { Router } from "express" 31 | 32 | export default () => { 33 | const router = Router() 34 | 35 | router.get("/hello-product", async (req, res) => { 36 | const productService = req.scope.resolve("productService") 37 | 38 | const [product] = await productService.list({}, { take: 1 }) 39 | 40 | res.json({ 41 | message: `Welcome to ${product.title}!` 42 | }) 43 | }) 44 | 45 | return router; 46 | } 47 | ``` 48 | -------------------------------------------------------------------------------- /src/models/README.md: -------------------------------------------------------------------------------- 1 | # Custom models 2 | 3 | You may define custom models (entities) that will be registered on the global container by creating files in the `src/models` directory that export an instance of `BaseEntity`. 4 | 5 | ## Example 6 | 7 | ### 1. Create the Entity 8 | 9 | ```ts 10 | // src/models/post.ts 11 | 12 | import { BeforeInsert, Column, Entity, PrimaryColumn } from "typeorm"; 13 | import { generateEntityId } from "@medusajs/utils"; 14 | import { BaseEntity } from "@medusajs/medusa"; 15 | 16 | @Entity() 17 | export class Post extends BaseEntity { 18 | @Column({type: 'varchar'}) 19 | title: string | null; 20 | 21 | @BeforeInsert() 22 | private beforeInsert(): void { 23 | this.id = generateEntityId(this.id, "post") 24 | } 25 | } 26 | ``` 27 | 28 | ### 2. Create the Migration 29 | 30 | You also need to create a Migration to create the new table in the database. See [How to Create Migrations](https://docs.medusajs.com/advanced/backend/migrations/) in the documentation. 31 | 32 | ### 3. Create a Repository 33 | Entities data can be easily accessed and modified using [TypeORM Repositories](https://typeorm.io/working-with-repository). To create a repository, create a file in `src/repositories`. For example, here’s a repository `PostRepository` for the `Post` entity: 34 | 35 | ```ts 36 | // src/repositories/post.ts 37 | 38 | import { EntityRepository, Repository } from "typeorm" 39 | 40 | import { Post } from "../models/post" 41 | 42 | @EntityRepository(Post) 43 | export class PostRepository extends Repository { } 44 | ``` 45 | 46 | See more about defining and accesing your custom [Entities](https://docs.medusajs.com/advanced/backend/entities/overview) in the documentation. -------------------------------------------------------------------------------- /src/subscribers/README.md: -------------------------------------------------------------------------------- 1 | # Custom subscribers 2 | 3 | You may define custom eventhandlers, `subscribers` by creating files in the `/subscribers` directory. 4 | 5 | ```ts 6 | import MyCustomService from "../services/my-custom"; 7 | import { EntityManager } from "typeorm"; 8 | import { OrderService } from "@medusajs/medusa"; 9 | import { IEventBusService } from "@medusajs/types"; 10 | 11 | export default class MySubscriber { 12 | protected readonly manager_: EntityManager; 13 | protected readonly myCustomService_: MyCustomService 14 | 15 | constructor( 16 | { 17 | manager, 18 | eventBusService, 19 | myCustomService, 20 | }: { 21 | manager: EntityManager; 22 | eventBusService: IEventBusService; 23 | myCustomService: MyCustomService; 24 | } 25 | ) { 26 | this.manager_ = manager; 27 | this.myCustomService_ = myCustomService; 28 | 29 | eventBusService.subscribe(OrderService.Events.PLACED, this.handleOrderPlaced); 30 | } 31 | 32 | handleOrderPlaced = async (data): Promise => { 33 | return true; 34 | } 35 | } 36 | 37 | ``` 38 | 39 | A subscriber is defined as a `class` which is registered as a subscriber by invoking `eventBusService.subscribe` in the `constructor` of the class. 40 | 41 | The type of event that the subscriber subscribes to is passed as the first parameter to the `eventBusService.subscribe` and the eventhandler is passed as the second parameter. The types of events a service can emmit are described in the individual service. 42 | 43 | An eventhandler has one parameter; a data `object` which contain information relating to the event, including relevant `id's`. The `id` can be used to fetch the appropriate entity in the eventhandler. 44 | -------------------------------------------------------------------------------- /src/services/README.md: -------------------------------------------------------------------------------- 1 | # Custom services 2 | 3 | You may define custom services that will be registered on the global container by creating files in the `/services` directory that export an instance of `BaseService`. 4 | 5 | ```ts 6 | // src/services/my-custom.ts 7 | 8 | import { Lifetime } from "awilix" 9 | import { TransactionBaseService } from "@medusajs/medusa"; 10 | import { IEventBusService } from "@medusajs/types"; 11 | 12 | export default class MyCustomService extends TransactionBaseService { 13 | static LIFE_TIME = Lifetime.SCOPED 14 | protected readonly eventBusService_: IEventBusService 15 | 16 | constructor( 17 | { eventBusService }: { eventBusService: IEventBusService }, 18 | options: Record 19 | ) { 20 | // @ts-ignore 21 | super(...arguments) 22 | 23 | this.eventBusService_ = eventBusService 24 | } 25 | } 26 | 27 | ``` 28 | 29 | The first argument to the `constructor` is the global giving you access to easy dependency injection. The container holds all registered services from the core, installed plugins and from other files in the `/services` directory. The registration name is a camelCased version of the file name with the type appended i.e.: `my-custom.js` is registered as `myCustomService`, `custom-thing.js` is registered as `customThingService`. 30 | 31 | You may use the services you define here in custom endpoints by resolving the services defined. 32 | 33 | ```js 34 | import { Router } from "express" 35 | 36 | export default () => { 37 | const router = Router() 38 | 39 | router.get("/hello-product", async (req, res) => { 40 | const myService = req.scope.resolve("myCustomService") 41 | 42 | res.json({ 43 | message: await myService.getProductMessage() 44 | }) 45 | }) 46 | 47 | return router; 48 | } 49 | ``` 50 | -------------------------------------------------------------------------------- /src/admin/routes/feed/components/Table.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Heading, Table } from '@medusajs/ui'; 2 | import { useAdminCustomQuery } from 'medusa-react'; 3 | import { FeedQuery, FeedResponseJson } from '../types/table'; 4 | 5 | const FeedTable: React.FC = () => { 6 | const { data, isLoading } = useAdminCustomQuery( 7 | 'feed/json', 8 | ['feed'], 9 | {} 10 | ); 11 | 12 | const items = data?.rss?.channel[0]?.item || []; 13 | 14 | return ( 15 | 16 |
17 | Feed Table 18 | {isLoading && Loading...} 19 | {items.length > 0 && ( 20 | 21 | 22 | 23 | # 24 | ID 25 | Item Group ID 26 | Title 27 | Link 28 | Image Link 29 | Availability 30 | Condition 31 | 32 | 33 | 34 | {items.map((item, index) => ( 35 | 36 | {index + 1} 37 | {item['g:id']?.[0] ?? ''} 38 | {item['g:item_group_id']?.[0] ?? ''} 39 | {item.title?.[0] ?? ''} 40 | {item.link?.[0] ?? ''} 41 | {item['g:image_link']?.[0] ?? ''} 42 | {item['g:availability']?.[0] ?? ''} 43 | {item['g:condition']?.[0] ?? ''} 44 | 45 | ))} 46 | 47 |
48 | )} 49 |
50 |
51 | ); 52 | }; 53 | 54 | export default FeedTable; 55 | -------------------------------------------------------------------------------- /medusa-config.js: -------------------------------------------------------------------------------- 1 | const dotenv = require("dotenv"); 2 | 3 | let ENV_FILE_NAME = ""; 4 | switch (process.env.NODE_ENV) { 5 | case "production": 6 | ENV_FILE_NAME = ".env.production"; 7 | break; 8 | case "staging": 9 | ENV_FILE_NAME = ".env.staging"; 10 | break; 11 | case "test": 12 | ENV_FILE_NAME = ".env.test"; 13 | break; 14 | case "development": 15 | default: 16 | ENV_FILE_NAME = ".env"; 17 | break; 18 | } 19 | 20 | try { 21 | dotenv.config({ path: process.cwd() + "/" + ENV_FILE_NAME }); 22 | } catch (e) {} 23 | 24 | // CORS when consuming Medusa from admin 25 | const ADMIN_CORS = 26 | process.env.ADMIN_CORS || "http://localhost:7000,http://localhost:7001"; 27 | 28 | // CORS to avoid issues when consuming Medusa from a client 29 | const STORE_CORS = process.env.STORE_CORS || "http://localhost:8000"; 30 | 31 | const DATABASE_URL = 32 | process.env.DATABASE_URL || "postgres://localhost/medusa-store"; 33 | 34 | const REDIS_URL = process.env.REDIS_URL || "redis://localhost:6379"; 35 | 36 | const plugins = [ 37 | `medusa-fulfillment-manual`, 38 | `medusa-payment-manual`, 39 | { 40 | resolve: `@medusajs/file-local`, 41 | options: { 42 | upload_dir: "uploads", 43 | }, 44 | }, 45 | { 46 | resolve: "@medusajs/admin", 47 | /** @type {import('@medusajs/admin').PluginOptions} */ 48 | options: { 49 | autoRebuild: true, 50 | develop: { 51 | open: process.env.OPEN_BROWSER !== "false", 52 | }, 53 | }, 54 | }, 55 | ]; 56 | 57 | const modules = { 58 | /*eventBus: { 59 | resolve: "@medusajs/event-bus-redis", 60 | options: { 61 | redisUrl: REDIS_URL 62 | } 63 | }, 64 | cacheService: { 65 | resolve: "@medusajs/cache-redis", 66 | options: { 67 | redisUrl: REDIS_URL 68 | } 69 | },*/ 70 | }; 71 | 72 | /** @type {import('@medusajs/medusa').ConfigModule["projectConfig"]} */ 73 | const projectConfig = { 74 | jwtSecret: process.env.JWT_SECRET, 75 | cookieSecret: process.env.COOKIE_SECRET, 76 | store_cors: STORE_CORS, 77 | database_url: DATABASE_URL, 78 | admin_cors: ADMIN_CORS, 79 | // Uncomment the following lines to enable REDIS 80 | // redis_url: REDIS_URL 81 | }; 82 | 83 | /** @type {import('@medusajs/medusa').ConfigModule} */ 84 | module.exports = { 85 | projectConfig, 86 | plugins, 87 | modules, 88 | }; 89 | -------------------------------------------------------------------------------- /src/services/feed.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TransactionBaseService, 3 | ProductService, 4 | Product as MedusaProduct, 5 | ProductVariantService, 6 | ProductVariant, 7 | } from '@medusajs/medusa'; 8 | import { Product as FeedProduct, FeedBuilder } from 'node-product-catalog-feed'; 9 | 10 | class FeedService extends TransactionBaseService { 11 | private productService: ProductService; 12 | private productVariantService: ProductVariantService; 13 | private readonly pathToProduct: string; 14 | 15 | constructor(container, options) { 16 | super(container); 17 | this.productService = container.productService; 18 | this.productVariantService = container.productVariantService; 19 | this.pathToProduct = options.pathToProduct ?? 'http://localhost:3000/shop/'; 20 | } 21 | 22 | async createFeed() { 23 | const products: MedusaProduct[] = await this.productService.list({}); 24 | const feedProducts = []; 25 | 26 | for (const parentProduct of products) { 27 | const variants: ProductVariant[] = 28 | await this.productService.retrieveVariants(parentProduct.id); 29 | 30 | if (variants && variants.length > 0) { 31 | const parentFeedProduct = new FeedProduct(); 32 | parentFeedProduct.id = parentProduct.id; 33 | parentFeedProduct.title = parentProduct.title; 34 | parentFeedProduct.description = parentProduct.description; 35 | parentFeedProduct.link = `${this.pathToProduct}${parentProduct.handle}`; 36 | parentFeedProduct.imageLink = parentProduct.thumbnail; 37 | parentFeedProduct.condition = 'new'; 38 | 39 | feedProducts.push(parentFeedProduct); 40 | 41 | for (const variant of variants) { 42 | console.log(variant); 43 | const variantFeedProduct = new FeedProduct(); 44 | variantFeedProduct.id = variant.id; 45 | variantFeedProduct.title = variant.title; 46 | variantFeedProduct.description = parentFeedProduct.description; 47 | variantFeedProduct.link = `${this.pathToProduct}${parentProduct.handle}`; 48 | variantFeedProduct.condition = parentFeedProduct.condition; 49 | variantFeedProduct.availability = variant.allow_backorder 50 | ? variant.inventory_quantity > 0 51 | ? 'in_stock' 52 | : 'backorder' 53 | : variant.inventory_quantity > 0 54 | ? 'in_stock' 55 | : 'out_of_stock'; 56 | variantFeedProduct.itemGroupId = parentFeedProduct.id; 57 | feedProducts.push(variantFeedProduct); 58 | } 59 | } 60 | } 61 | 62 | console.log(feedProducts); 63 | // 5. Create a new FeedBuilder and populate it with feed products. 64 | const feedBuilder = new FeedBuilder() 65 | .withTitle('Your Product Feed Title') 66 | .withLink('https://your-link.com') 67 | .withDescription('Your Feed Description'); 68 | 69 | // 6. Add each feed product to the feed builder. 70 | feedProducts.forEach((product) => { 71 | feedBuilder.withProduct(product); 72 | }); 73 | 74 | // 7. Build XML. 75 | return feedBuilder.buildXml(); 76 | } 77 | } 78 | 79 | export default FeedService; 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "medusa-plugin-feeds", 3 | "version": "0.0.1", 4 | "description": "A Medusa plugin to provide product feeds.", 5 | "author": "Marco Freiberger ", 6 | "license": "MIT", 7 | "keywords": [ 8 | "sqlite", 9 | "postgres", 10 | "typescript", 11 | "ecommerce", 12 | "headless", 13 | "medusa" 14 | ], 15 | "scripts": { 16 | "clean": "cross-env ./node_modules/.bin/rimraf dist", 17 | "watch": "tsc --watch", 18 | "build": "cross-env pnpm run clean && pnpm run build:server && pnpm run build:admin", 19 | "build:server": "cross-env pnpm run clean && tsc -p tsconfig.json", 20 | "build:admin": "cross-env medusa-admin build", 21 | "prepare": "cross-env NODE_ENV=production pnpm run build:server && medusa-admin bundle", 22 | "lint": "eslint .", 23 | "lint:fix": "eslint . --fix", 24 | "publishL": "rm -r .yalc && rm -r dist && yalc publish --push" 25 | }, 26 | "dependencies": { 27 | "@medusajs/admin": "^7.0.1", 28 | "@medusajs/cache-redis": "^1.8.8", 29 | "@medusajs/event-bus-local": "^1.9.6", 30 | "@medusajs/event-bus-redis": "^1.8.9", 31 | "@medusajs/file-local": "^1.0.2", 32 | "@medusajs/ui": "^1.0.0", 33 | "@tanstack/react-query": "4.22.0", 34 | "babel-preset-medusa-package": "^1.1.13", 35 | "body-parser": "^1.19.1", 36 | "cors": "^2.8.5", 37 | "dotenv": "16.0.3", 38 | "express": "^4.17.2", 39 | "medusa-interfaces": "^1.3.7", 40 | "medusa-react": "^9.0.4", 41 | "node-product-catalog-feed": "^1.0.0", 42 | "prism-react-renderer": "^2.0.4", 43 | "react-xml-viewer": "^2.0.0", 44 | "typeorm": "^0.3.16", 45 | "xml2js": "^0.6.2" 46 | }, 47 | "devDependencies": { 48 | "@babel/core": "^7.14.3", 49 | "@babel/preset-typescript": "^7.21.4", 50 | "@medusajs/medusa": "^1.15.0", 51 | "@medusajs/medusa-cli": "^1.3.16", 52 | "@types/express": "^4.17.13", 53 | "@types/jest": "^27.4.0", 54 | "@types/node": "^17.0.8", 55 | "@types/react": "^18.2.21", 56 | "babel-preset-medusa-package": "^1.1.13", 57 | "cross-env": "^7.0.3", 58 | "eslint": "^6.8.0", 59 | "jest": "^27.3.1", 60 | "medusa-core-utils": "^1.2.0", 61 | "mongoose": "^5.13.14", 62 | "rimraf": "^3.0.2", 63 | "ts-jest": "^27.0.7", 64 | "ts-loader": "^9.2.6", 65 | "typescript": "^4.5.2" 66 | }, 67 | "peerDependencies": { 68 | "@medusajs/medusa": "^1.15.0", 69 | "react": "^18.2.0", 70 | "react-router-dom": "^6.13.0" 71 | }, 72 | "jest": { 73 | "globals": { 74 | "ts-jest": { 75 | "tsconfig": "tsconfig.spec.json" 76 | } 77 | }, 78 | "moduleFileExtensions": [ 79 | "js", 80 | "json", 81 | "ts" 82 | ], 83 | "testPathIgnorePatterns": [ 84 | "/node_modules/", 85 | "/node_modules/" 86 | ], 87 | "rootDir": "src", 88 | "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|js)$", 89 | "transform": { 90 | ".ts": "ts-jest" 91 | }, 92 | "collectCoverageFrom": [ 93 | "**/*.(t|j)s" 94 | ], 95 | "coverageDirectory": "./coverage", 96 | "testEnvironment": "node" 97 | } 98 | } 99 | --------------------------------------------------------------------------------