Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/add product sync #44

Merged
merged 12 commits into from
Oct 30, 2023
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,9 @@ package-lock.json
yarn.lock
medusa-db.sql
build
.cache

*.json
.cache

.env.*
!.env.template
Binary file modified dump.rdb
Binary file not shown.
11 changes: 6 additions & 5 deletions medusa-config.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
const dotenv = require("dotenv");
const { existsSync } = require("fs");

let ENV_FILE_NAME = ".env";
switch (process.env.NODE_ENV) {
case "production":
ENV_FILE_NAME = ".env.production";
ENV_FILE_NAME = existsSync('./.env.production') ? '.env.production' : '.env'
break;
case "staging":
ENV_FILE_NAME = ".env.staging";
Expand All @@ -12,6 +13,8 @@ switch (process.env.NODE_ENV) {
ENV_FILE_NAME = ".env.test";
break;
case "development":
ENV_FILE_NAME = existsSync('./.env.development') ? '.env.development' : '.env'
break;
default:
ENV_FILE_NAME = ".env";
break;
Expand All @@ -32,6 +35,7 @@ const DATABASE_TYPE = process.env.DATABASE_TYPE || "sqlite";
const DATABASE_URL =
process.env.DATABASE_URL || "postgres://localhost/medusa-store";


const REDIS_URL = process.env.REDIS_URL || "redis://localhost:6379";

const plugins = [
Expand Down Expand Up @@ -82,9 +86,6 @@ const plugins = [
if (!prices || !id || Object.values(prices).length < 3) {
return null;
}

console.log("Updated:", id, prices);

return {
id,
prices,
Expand Down Expand Up @@ -164,7 +165,7 @@ const projectConfig = {
if (DATABASE_URL && DATABASE_TYPE === "postgres") {
projectConfig.database_url = DATABASE_URL;
delete projectConfig["database_database"];
}
}

/** @type {import('@medusajs/medusa').ConfigModule} */
module.exports = {
Expand Down
18 changes: 16 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"seed": "medusa seed -f ./data/seed.json",
"start": "npm run build && medusa migrations run && medusa develop -p=8080",
"dev": "npm run build && medusa develop",
"dev:prod": "cross-env NODE_ENV=production npm run build && medusa start",
"build:admin": "medusa-admin build",
"migration:run": "npm run build && npx typeorm migration:run -d datasource.js",
"migration:generate": "npx typeorm migration:generate -d datasource.js"
Expand All @@ -28,17 +29,26 @@
"@aws-sdk/client-s3": "^3.425.0",
"@aws-sdk/s3-request-presigner": "^3.425.0",
"@babel/preset-typescript": "^7.21.5",
"@google-cloud/local-auth": "^2.1.0",
"@medusajs/admin": "^7.1.3",
"@medusajs/cache-inmemory": "^1.8.7",
"@medusajs/cache-redis": "^1.8.7",
"@medusajs/event-bus-local": "^1.9.4",
"@medusajs/event-bus-redis": "^1.8.7",
"@medusajs/medusa": "^1.17.0",
"@medusajs/medusa": "1.16.1",
"@medusajs/medusa-cli": "^1.3.15",
"@types/node-fetch": "^2.6.6",
"async": "^3.2.4",
"axios": "^1.5.1",
"babel-preset-medusa-package": "^1.1.19",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"express": "^4.18.2",
"google-auth-library": "^9.1.0",
"google-spreadsheet": "^4.1.0",
"googleapis": "^105.0.0",
"ky": "^1.0.1",
"ky-universal": "^0.12.0",
"medusa-file-s3": "^1.2.0",
"medusa-fulfillment-manual": "^1.1.37",
"medusa-interfaces": "^1.3.7",
Expand All @@ -48,16 +58,20 @@
"medusa-plugin-segment": "^1.3.5",
"medusa-plugin-sendgrid": "^1.3.9",
"meilisearch": "^0.32.5",
"nanoid": "^5.0.1",
"node-fetch": "2",
"openai": "^3.2.1",
"typeorm": "^0.3.16",
"ulid": "^2.3.0"
},
"devDependencies": {
"@babel/cli": "^7.21.5",
"@babel/core": "^7.22.1",
"@types/async": "^3.2.21",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/multer": "^1.4.8",
"@types/nanoid": "^3.0.0",
"@types/node": "^20.2.5",
"babel-preset-medusa-package": "^1.1.19",
"cross-env": "^7.0.3",
Expand Down Expand Up @@ -98,4 +112,4 @@
"engines": {
"node": ">=18.0.0"
}
}
}
15 changes: 14 additions & 1 deletion src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import { listDeletedCustomers } from "./routes/admin/list-deleted";
import { restoreCustomer } from "./routes/admin/restore-customer";
import { uploadFile } from "./routes/global/uploads";
import { S3Client } from "@aws-sdk/client-s3";
import { SheetsRouter } from "./routes/admin/sheets";
import { SheetsSyncCategoriesRouter } from "./routes/admin/sheets/sync-categories";
import { SheetsSyncLocationsRouter } from "./routes/admin/sheets/sync-locations";
import { SheetsSyncProductsRouter } from "./routes/admin/sheets/sync-products";

export default (rootDirectory: string): Router | Router[] => {
const { configModule: { projectConfig } } = getConfigFile<ConfigModule>(rootDirectory, "medusa-config")
Expand All @@ -25,7 +29,16 @@ export default (rootDirectory: string): Router | Router[] => {
router.use(cors(storefrontCorsConfig))

// endpoints
const endpointHandlers = [deleteCustomer, listDeletedCustomers, restoreCustomer, uploadFile]
const endpointHandlers = [
deleteCustomer,
listDeletedCustomers,
restoreCustomer,
uploadFile,
SheetsRouter,
SheetsSyncCategoriesRouter,
SheetsSyncLocationsRouter,
SheetsSyncProductsRouter
]
endpointHandlers.forEach(endpointHandle => endpointHandle(router, { s3Client }))

return router
Expand Down
38 changes: 38 additions & 0 deletions src/api/routes/admin/sheets/helpers/mapSheetProduct.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Product, ProductStatus } from "@medusajs/medusa";
import { CreateProductInput } from "@medusajs/medusa/dist/types/product";
import { ulid } from "ulid";


type ExtraProps = {
stock: number,
categories: { id: string }[],
rowNumber: number;
}

export type CreateProduct = Omit<Partial<Product>, 'variants' | 'categories'> & ExtraProps

export type CreateProductResponse = CreateProductInput & { id: string, rowNumber: number }

export const mapSheetProduct = (product: CreateProduct): CreateProductResponse => {
return ({
id: product.id,
rowNumber: product.rowNumber,
title: product.title,
handle: `${product.title.toLowerCase().replace(/-/g, '').replace(/ /g, '-')}_${ulid()}`,
description: product.description,
status: !product.stock ? ProductStatus.DRAFT : product.status,
categories: product.categories,
variants: [
{
title: 'one size',
inventory_quantity: product.stock || 0,
prices: [
{
amount: 10000,
currency_code: 'usd'
}
]
}
],
})
}
65 changes: 65 additions & 0 deletions src/api/routes/admin/sheets/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { ProductStatus, authenticate } from "@medusajs/medusa"
import express, { Router } from "express";
import CategoryService from "../../../../services/category";
import GoogleSheetAPIService, { ProductData } from "../../../../services/google-sheet-api";
import ProductService from "../../../../services/product";
import { mapSheetProduct } from "./helpers/mapSheetProduct";

type CategoriesID = { id: string }[];

type SheetProductWithCategories = ProductData & { categories: CategoriesID };

export const SheetsRouter = (router: Router) => {
router.use('/admin/sheets', express.json(), authenticate());

router.get('/admin/sheets', async (req, res) => {
const googleSheetService = req.scope.resolve('googleSheetApiService') as GoogleSheetAPIService;
const categoryService = req.scope.resolve('categoryService') as CategoryService;
const productService = req.scope.resolve('productService') as ProductService

googleSheetService.sheetId = (req.query.sheetId as string) || '1TaiFMTqYGirhLrjUkEfCGbV3hCrX9po_tduFw_sETUg';

try {
const sheetData = await googleSheetService.getProductDataBySheetId();

const promises = sheetData.map(async (entry) => {
const location = await categoryService.getCategoryByName(entry.Location);
const category = await categoryService.getCategoryByName(entry.Category);

const categories: { id: string }[] = []

if (location) {
categories.push({ id: location.id })
}
if (category) {
categories.push({ id: category.id })
}

return { ...entry, categories }
});

let results = (await Promise.allSettled(promises)).filter(r => r.status === 'fulfilled').map((r: any) => r.value)

results = results.map((product: SheetProductWithCategories) => {
return mapSheetProduct({
id: product['Product ID'],
title: product['Product title'],
description: product.Description,
stock: product.Stocks,
categories: product.categories,
rowNumber: product.rowNumber,
status: product.Draft ? ProductStatus.DRAFT : ProductStatus.PUBLISHED
})
});

const bulkAddResult = await productService.addBulkProducts(results)
const savedProductIdAndRowNumber = bulkAddResult.saved.map((p) => ({ id: p.id, rowNumber: p.rowNumber }))

const affectedCellsCount = await googleSheetService.updateProductId(savedProductIdAndRowNumber)

return res.json({ status: 'ok', affectedCellsCount })
} catch (error) {
return res.status(500).json({ status: 500, message: 'An error occurred!', error: error instanceof Error ? error.message : error })
}
})
}
29 changes: 29 additions & 0 deletions src/api/routes/admin/sheets/sync-categories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { authenticate } from "@medusajs/medusa"
import express, { Router } from "express";
import CategoryService from "../../../../services/category";
import GoogleSheetAPIService from "../../../../services/google-sheet-api";


export const SheetsSyncCategoriesRouter = (router: Router) => {
router.use('/admin/sheets/sync-categories', express.json(), authenticate());

router.get('/admin/sheets/sync-categories', async (req, res) => {
const googleSheetService = req.scope.resolve('googleSheetApiService') as GoogleSheetAPIService;
const categoryService = req.scope.resolve('categoryService') as CategoryService;

googleSheetService.sheetId = (req.query.sheetId as string) || '1TaiFMTqYGirhLrjUkEfCGbV3hCrX9po_tduFw_sETUg'

try {

const categories = await categoryService.retrieveAllCategoriesName()

if (categories.length) {
await googleSheetService.syncCategories(categories)
}

return res.json({ status: categories.length ? 'ok' : 'failed, no categories available!' })
} catch (error) {
return res.status(500).json({ status: 500, message: 'An error occurred!', error: error instanceof Error ? error.message : error })
}
})
}
30 changes: 30 additions & 0 deletions src/api/routes/admin/sheets/sync-locations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { authenticate } from "@medusajs/medusa";
import express, { Router } from "express";
import CategoryService from "../../../../services/category";
import GoogleSheetAPIService from "../../../../services/google-sheet-api";


export const SheetsSyncLocationsRouter = (router: Router) => {
router.use('/admin/sheets/sync-locations', express.json(), authenticate());

router.get('/admin/sheets/sync-locations', async (req, res) => {
const googleSheetService = req.scope.resolve('googleSheetApiService') as GoogleSheetAPIService;
const categoryService = req.scope.resolve('categoryService') as CategoryService;

googleSheetService.sheetId = (req.query.sheetId as string) || '1TaiFMTqYGirhLrjUkEfCGbV3hCrX9po_tduFw_sETUg'

try {

const lastLocations = await categoryService.retrieveAllLastLocationsName()


if (lastLocations.length) {
await googleSheetService.syncLocations(lastLocations)
}

return res.json({ status: lastLocations.length ? 'ok' : 'failed, no locations available!' })
} catch (error) {
return res.status(500).json({ status: 500, message: 'An error occurred!', error: error instanceof Error ? error.message : error })
}
})
}
26 changes: 26 additions & 0 deletions src/api/routes/admin/sheets/sync-products.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { authenticate } from "@medusajs/medusa"
import express, { Router } from "express";
import GoogleSheetAPIService from "../../../../services/google-sheet-api";
import ProductService from "../../../../services/product";


export const SheetsSyncProductsRouter = (router: Router) => {
router.use('/admin/sheets/sync-products', express.json(), authenticate());

router.get('/admin/sheets/sync-products', async (req, res) => {
const googleSheetService = req.scope.resolve('googleSheetApiService') as GoogleSheetAPIService;
const productService = req.scope.resolve('productService') as ProductService;

googleSheetService.sheetId = (req.query.sheetId as string) || '1TaiFMTqYGirhLrjUkEfCGbV3hCrX9po_tduFw_sETUg'

try {
const products = await productService.list({}, { relations: ['categories'] })

googleSheetService.syncProducts(products)

return res.json({ status: products.length ? 'ok' : 'failed, no categories available!' })
} catch (error) {
return res.status(500).json({ status: 500, message: 'An error occurred!', error: error instanceof Error ? error.message : error })
}
})
}
9 changes: 5 additions & 4 deletions src/api/routes/global/uploads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { authenticate, authenticateCustomer } from "@medusajs/medusa";
import express, { Router } from "express";
import { ulid } from "ulid";
import { getFileExtensionFromContentType } from "../../../utils/getFileExtensionFromContentType";

export const uploadFile = (router: Router, { s3Client }: { s3Client: S3Client }) => {
router.use('/uploads/getPreSignedUrl', express.json())
Expand All @@ -15,14 +16,14 @@ export const uploadFile = (router: Router, { s3Client }: { s3Client: S3Client })
return res.status(403).send()
}
}
const ext = req.query.ext as string
if (!ext) return res.status(422).send('Missing file extension!');
const contentType = req.query.type as string
if (!getFileExtensionFromContentType(contentType)) return res.status(422).send('Missing or invalid file extension!');


const id = req.user.customer_id || req.user.userId;

const key = `${id}/${ulid()}.${ext}`
const command = new PutObjectCommand({ Bucket: process.env.AWS_BUCKET, Key: key });
const key = `${id}/${ulid()}${getFileExtensionFromContentType(contentType)}`
const command = new PutObjectCommand({ Bucket: process.env.AWS_BUCKET, Key: key, ContentType: contentType });

const url = await getSignedUrl(s3Client, command, { expiresIn: 300 }); // 5 minutes

Expand Down
Loading
Loading