Skip to content

Commit

Permalink
Merge pull request #44 from curiosta/feat/add-product-sync
Browse files Browse the repository at this point in the history
Feat/add product sync
  • Loading branch information
ShivamJoker authored Oct 30, 2023
2 parents faf78c3 + 17fcbf0 commit 2935e8e
Show file tree
Hide file tree
Showing 18 changed files with 740 additions and 36 deletions.
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

0 comments on commit 2935e8e

Please sign in to comment.