diff --git a/package.json b/package.json index 23f767e..2216a9d 100644 --- a/package.json +++ b/package.json @@ -18,15 +18,9 @@ ], "devDependencies": { "@medusajs/medusa": "^1.10.1", - "@medusajs/types": "^1.8.5", - "@medusajs/utils": "^1.8.4", - "body-parser": "^1.19.0", - "cors": "^2.8.5", "cross-env": "^5.2.1", - "express": "^4.17.2", - "medusa-interfaces": "^1.3.7", "typeorm": "^0.3.16", - "typescript": "^4.9.5" + "typescript": "^4.4.4" }, "scripts": { "prepare": "cross-env NODE_ENV=production yarn run build", diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 0000000..cd9b7f0 --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,140 @@ +import cors from "cors" +import configLoader from "@medusajs/medusa/dist/loaders/config" +import { Router } from "express" +import * as bodyParser from "body-parser" +//import { User, UserService, Customer, CustomerService } from "@medusajs/medusa" +import { authenticateCustomer } from "@medusajs/medusa" +import { MedusaError } from "@medusajs/utils" +import { z } from "zod" + +export default (rootDirectory: string): Router | Router[] => { + + const config = configLoader(rootDirectory) + const storeCorsOptions = { origin: config.projectConfig.store_cors.split(","), credentials: true, } + const adminCorsOptions = { origin: config.projectConfig.admin_cors.split(","), credentials: true, } + + const router = Router() + + // REVIEWS - GET ALL REVIEWS FOR A PRODUCT + //router.use("/store/products/:id/reviews", authenticateCustomer()) + router.use(authenticateCustomer()) + //router.get("/store/products/:id/reviews", cors(storeCorsOptions), authenticateCustomer(), async (req, res) => { + router.get("/store/products/:id/reviews", cors(storeCorsOptions), async (req, res) => { +// console.log('get reviews') +// console.log(req.user) + const productReviewService = req.scope.resolve("productReviewService") + productReviewService.getProductReviews(req.params.id).then((product_reviews) => { + return res.json({product_reviews}) + }) + }) + + // REVIEWS - GET ALL REVIEWS FOR A CUSTOMER + // router.use("/store/customers/:id/reviews", authenticate(), getCustomer) + router.get("/store/customers/:id/reviews", async (req, res) => { + const productReviewService = req.scope.resolve("productReviewService") + productReviewService.getCustomerProductReviews(req.params.id).then((product_reviews) => { + return res.json({product_reviews}) + }) + }) + + // REVIEWS - ADD A NEW REVIEW FOR A PRODUCT + //router.use("/store/products/:id/reviews", authenticate(), getCustomer, bodyParser.json()) + router.use("/store/products/:id/reviews", bodyParser.json()) + router.post("/store/products/:id/reviews", async (req, res) => { + const schema = z.object({ + customer_id: z.string().min(1), + display_name: z.string().min(1), + content: z.string().min(1), + rating: z.coerce.number().min(0).max(5), + }) + /* @ts-ignore */ + const { success, error, data } = schema.safeParse(req.body) + if (!success) { + throw new MedusaError(MedusaError.Types.INVALID_DATA, error) + } else { + const productReviewService = req.scope.resolve("productReviewService") + productReviewService.addProductReview(req.params.id, data.customer_id, data.display_name, data.content, data.rating) + .then((product_review) => { + return res.json({product_review}) + }) + } + }) + + // REVIEWS - GET A SINGLE REVIEW + router.get("/store/reviews/:id", async (req, res) => { + const productReviewService = req.scope.resolve("productReviewService") + productReviewService.getReview(req.params.id).then((product_review) => { + return res.json({product_review}) + }) + }) + + // REVIEWS - UPDATE A REVIEW FOR A PRODUCT + //router.use("/store/reviews/:id", authenticate(), getCustomer, bodyParser.json()) + router.use("/store/reviews/:id", bodyParser.json()) + router.post("/store/reviews/:id", async (req, res) => { + const schema = z.object({ + customer_id: z.string().min(1), + display_name: z.string().min(1), + content: z.string().min(1), + rating: z.coerce.number().min(0).max(5), + }) + /* @ts-ignore */ + const { success, error, data } = schema.safeParse(req.body) + if (!success) { + throw new MedusaError(MedusaError.Types.INVALID_DATA, error) + } else { + } + const productReviewService = req.scope.resolve("productReviewService") + const productReview = await productReviewService.getProductReviews(req.params.id) + if (productReview.customer_id !== req.body.userId) { + return res.status(401).json({ message: "Unauthorized" }) + } + productReviewService.updateProductReview(req.params.id, req.body.display_name, req.body.content, req.body.rating) + .then((product_review) => { + return res.json({product_review}) + }) + }) + + // REVIEWS - ADMIN GET ALL REVIEWS FOR A PRODUCT + router.get("/admin/products/:id/reviews", async (req, res) => { + const productReviewService = req.scope.resolve("productReviewService") + productReviewService.getProductReviews(req.params.id).then((product_reviews) => { + return res.json({product_reviews}) + }) + }) + + // REVIEWS - ADMIN EDIT A REVIEW FOR A PRODUCT + //router.use("/admin/reviews/:id", bodyParser.json(), authenticate(), getUser) + router.use("/admin/reviews/:id", bodyParser.json()) + router.post("/admin/reviews/:id", async (req, res) => { + const productReviewService = req.scope.resolve("productReviewService") + productReviewService.editProductReview(req.params.id, req.body.display_name, req.body.content, req.body.rating, req.body.approved) + .then((product_review) => { + return res.json({product_review}) + }) + }) + + return router +} + +// async function getCustomer(req, res, next) { +// console.log('getCustomer') +// let loggedInCustomer: Customer | null = null +// if (req.customer && req.customer.customerId) { +// console.log(req.customer.customerId) +// const customerService = req.scope.resolve("customerService") as CustomerService +// loggedInCustomer = await customerService.retrieve(req.customer.customerId) +// } +// req.scope.register({ loggedInCustomer: { resolve: () => loggedInCustomer }}) +// next() +// } + +// async function getUser(req, res, next) { +// let loggedInUser: User | null = null +// if (req.user && req.user.userId) { +// const userService = req.scope.resolve("userService") as UserService +// loggedInUser = await userService.retrieve(req.user.userId) +// } +// req.scope.register({ loggedInUser: { resolve: () => loggedInUser }}) +// next() +// } \ No newline at end of file diff --git a/src/migrations/1684173263726-Blog.ts b/src/migrations/1684173263726-Blog.ts new file mode 100644 index 0000000..e89a4aa --- /dev/null +++ b/src/migrations/1684173263726-Blog.ts @@ -0,0 +1,112 @@ +import { MigrationInterface, QueryRunner, TableForeignKey } from "typeorm" + +export class Blog1684173263726 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE IF NOT EXISTS "blog_category" ("id" character varying NOT NULL, + "handle" character varying, + "title" character varying NOT NULL, + "description" character varying, + "keywords" varchar[], + "metadata" jsonb)` + ) + await queryRunner.createPrimaryKey("blog_category", ["id"]) + + await queryRunner.query( + `CREATE TABLE IF NOT EXISTS "blog_post" ("id" character varying NOT NULL, + "handle" character varying, + "title" character varying NOT NULL, + "author" character varying, + "published" boolean NOT NULL DEFAULT false, + "content" character varying, + "description" character varying, + "keywords" varchar[], + "category_id" character varying, + "metadata" jsonb, + "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now())` + ) + await queryRunner.createPrimaryKey("blog_post", ["id"]) + await queryRunner.createForeignKey("blog_post", new TableForeignKey({ + columnNames: ["category_id"], + referencedColumnNames: ["id"], + referencedTableName: "blog_category", + onDelete: "CASCADE", + onUpdate: "CASCADE" + })) + + await queryRunner.query( + `CREATE TABLE IF NOT EXISTS "blog_tag" ("id" character varying NOT NULL, + "title" character varying NOT NULL)` + ) + await queryRunner.createPrimaryKey("blog_tag", ["id"]) + + await queryRunner.query( + `CREATE TABLE IF NOT EXISTS "blog_tags" ("post_id" character varying NOT NULL, + "tag_id" character varying NOT NULL)` + ) + await queryRunner.createForeignKey("blog_tags", new TableForeignKey({ + columnNames: ["post_id"], + referencedColumnNames: ["id"], + referencedTableName: "blog_post", + onDelete: "CASCADE", + onUpdate: "CASCADE" + })) + await queryRunner.createForeignKey("blog_tags", new TableForeignKey({ + columnNames: ["tag_id"], + referencedColumnNames: ["id"], + referencedTableName: "blog_tag", + onDelete: "CASCADE", + onUpdate: "CASCADE" + })) + + await queryRunner.query( + `CREATE TABLE IF NOT EXISTS "blog_product_tags" ("post_id" character varying NOT NULL, + "product_id" character varying NOT NULL)` + ) + await queryRunner.createForeignKey("blog_product_tags", new TableForeignKey({ + columnNames: ["post_id"], + referencedColumnNames: ["id"], + referencedTableName: "blog_post", + onDelete: "CASCADE", + onUpdate: "CASCADE" + })) + await queryRunner.createForeignKey("blog_product_tags", new TableForeignKey({ + columnNames: ["product_id"], + referencedColumnNames: ["id"], + referencedTableName: "product", + onDelete: "CASCADE", + onUpdate: "CASCADE" + })) + + await queryRunner.query( + `CREATE TABLE IF NOT EXISTS "blog_collection_tags" ("post_id" character varying NOT NULL, + "collection_id" character varying NOT NULL)` + ) + await queryRunner.createForeignKey("blog_collection_tags", new TableForeignKey({ + columnNames: ["post_id"], + referencedColumnNames: ["id"], + referencedTableName: "blog_post", + onDelete: "CASCADE", + onUpdate: "CASCADE" + })) + await queryRunner.createForeignKey("blog_collection_tags", new TableForeignKey({ + columnNames: ["collection_id"], + referencedColumnNames: ["id"], + referencedTableName: "collection", + onDelete: "CASCADE", + onUpdate: "CASCADE" + })) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable("blog_category", true) + await queryRunner.dropTable("blog_post", true) + await queryRunner.dropTable("blog_tag", true) + await queryRunner.dropTable("blog_tags", true) + await queryRunner.dropTable("blog_product_tags", true) + await queryRunner.dropTable("blog_collection_tags", true) + } + +} diff --git a/src/models/blog-post.ts b/src/models/blog-post.ts index 872d847..f5a0e6c 100644 --- a/src/models/blog-post.ts +++ b/src/models/blog-post.ts @@ -27,9 +27,9 @@ export class BlogPost extends BaseEntity { description: string | null @Column({ type: "text", nullable: true, array: true }) - keywords: string | null + keywords: string[] | null - @Column({ type: "varchar", nullable: false }) + @Column({ type: "varchar", nullable: true }) category_id: string @ManyToOne(() => BlogCategory) diff --git a/src/repositories/blog-category.ts b/src/repositories/blog-category.ts new file mode 100644 index 0000000..a224b48 --- /dev/null +++ b/src/repositories/blog-category.ts @@ -0,0 +1,4 @@ +import { BlogCategory } from "../models/blog-category" +import { dataSource } from '@medusajs/medusa/dist/loaders/database' + +export const BlogCategoryRepository = dataSource.getRepository(BlogCategory) \ No newline at end of file diff --git a/src/repositories/blog-post.ts b/src/repositories/blog-post.ts new file mode 100644 index 0000000..4da12cb --- /dev/null +++ b/src/repositories/blog-post.ts @@ -0,0 +1,4 @@ +import { BlogPost } from "../models/blog-post" +import { dataSource } from '@medusajs/medusa/dist/loaders/database' + +export const BlogPostRepository = dataSource.getRepository(BlogPost) \ No newline at end of file diff --git a/src/repositories/blog-tag.ts b/src/repositories/blog-tag.ts new file mode 100644 index 0000000..4cb88fb --- /dev/null +++ b/src/repositories/blog-tag.ts @@ -0,0 +1,4 @@ +import { BlogTag } from "../models/blog-tag" +import { dataSource } from '@medusajs/medusa/dist/loaders/database' + +export const BlogTagRepository = dataSource.getRepository(BlogTag) \ No newline at end of file diff --git a/src/services/blog.ts b/src/services/blog.ts new file mode 100644 index 0000000..e825946 --- /dev/null +++ b/src/services/blog.ts @@ -0,0 +1,177 @@ +import { TransactionBaseService } from '@medusajs/utils' +import { BlogCategoryRepository } from '../repositories/blog-category' +import { BlogPostRepository } from '../repositories/blog-post' +import { BlogTagRepository } from '../repositories/blog-tag' + +export default class BlogService extends TransactionBaseService { + protected readonly blogCategoryRepository_: typeof BlogCategoryRepository + protected readonly blogPostRepository_: typeof BlogPostRepository + protected readonly blogTagRepository_: typeof BlogTagRepository + + constructor({ blogCategoryRepository, blogPostRepository, blogTagRepository }) { + super(arguments[0]) + this.blogCategoryRepository_ = blogCategoryRepository + this.blogPostRepository_ = blogPostRepository + this.blogTagRepository_ = blogTagRepository + } + + async getBlogCategories() { + /* @ts-ignore */ + const blogCategoryRepository = this.activeManager_.withRepository(this.blogCategoryRepository_) + return await blogCategoryRepository.find() + } + + async getBlogCategory(id) { + /* @ts-ignore */ + const blogCategoryRepository = this.activeManager_.withRepository(this.blogCategoryRepository_) + return await blogCategoryRepository.findOne({ + where: { id } + }) + } + + async addBlogCategory (category) { + const { handle, title, description, keywords, metadata } = category + if (!title) throw new Error("Adding a blog category requires a title") + /* @ts-ignore */ + const blogCategoryRepository = this.activeManager_.withRepository(this.blogCategoryRepository_) + const createdCategory = blogCategoryRepository.create({ + handle, + title, + description, + keywords, + metadata + }) + const blogCategory = await blogCategoryRepository.save(createdCategory) + return blogCategory + } + + async updateBlogCategory (id, category) { + const { handle, title, description, keywords, metadata } = category + if (!id || !title) throw new Error("Updating a blog category requires an id and a title") + /* @ts-ignore */ + const blogCategoryRepository = this.activeManager_.withRepository(this.blogCategoryRepository_) + const blogCategory = blogCategoryRepository.update(id, { + handle, + title, + description, + keywords, + metadata + }) + return blogCategory + } + + async deleteBlogCategory(id) { + if (!id) throw new Error("Deleting a blog category requires an id") + /* @ts-ignore */ + const blogCategoryRepository = this.activeManager_.withRepository(this.blogCategoryRepository_) + return await blogCategoryRepository.delete(id) + } + + async getBlogPosts() { + /* @ts-ignore */ + const blogPostRepository = this.activeManager_.withRepository(this.blogPostRepository_) + return await blogPostRepository.find() + } + + async getBlogCategoryPosts(category_id) { + /* @ts-ignore */ + const blogPostRepository = this.activeManager_.withRepository(this.blogPostRepository_) + return await blogPostRepository.find({ + where: { category_id } + }) + } + + async getBlogPost(id) { + /* @ts-ignore */ + const blogPostRepository = this.activeManager_.withRepository(this.blogPostRepository_) + return await blogPostRepository.findOne({ + where: { id } + }) + } + + async addBlogPost(post) { + const { handle, title, author, published, content, description, keywords, category_id, tags, products, collections, metadata } = post + if (!title) throw new Error("Adding a blog post requires a title") + /* @ts-ignore */ + const blogPostRepository = this.activeManager_.withRepository(this.blogPostRepository_) + const createdPost = blogPostRepository.create({ + handle, + title, + author, + published, + content, + description, + keywords, + category_id, + tags, + products, + collections, + metadata + }) + const blogPost = await blogPostRepository.save(createdPost) + return blogPost + } + + async updateBlogPost(id, post) { + const { handle, title, author, published, content, description, keywords, category_id, tags, products, collections, metadata } = post + if (!id || !title) throw new Error("Updating a blog post requires an id and a title") + /* @ts-ignore */ + const blogPostRepository = this.activeManager_.withRepository(this.blogPostRepository_) + const blogPost = blogPostRepository.update(id, { + handle, + title, + author, + published, + content, + description, + keywords, + category_id, + tags, + products, + collections, + metadata + }) + return blogPost + } + + async deleteBlogPost(id) { + if (!id) throw new Error("Deleting a blog post requires an id") + /* @ts-ignore */ + const blogPostRepository = this.activeManager_.withRepository(this.blogPostRepository_) + return await blogPostRepository.delete(id) + } + + async getBlogTags() { + /* @ts-ignore */ + const blogTagRepository = this.activeManager_.withRepository(this.blogTagRepository_) + return await blogTagRepository.find() + } + + async addBlogTag(title) { + if (!title) throw new Error("Adding a blog tag requires a title") + /* @ts-ignore */ + const blogTagRepository = this.activeManager_.withRepository(this.blogTagRepository_) + const createdTag = blogTagRepository.create({ + title + }) + const blogTag = await blogTagRepository.save(createdTag) + return blogTag + } + + async updateBlogTag(id, title) { + if (!id || !title) throw new Error("Updating a blog tag requires an id and a title") + /* @ts-ignore */ + const blogTagRepository = this.activeManager_.withRepository(this.blogTagRepository_) + const blogTag = blogTagRepository.update(id, { + title + }) + return blogTag + } + + async deleteBlogTag(id) { + if (!id) throw new Error("Deleting a blog tag requires an id") + /* @ts-ignore */ + const blogTagRepository = this.activeManager_.withRepository(this.blogTagRepository_) + return await blogTagRepository.delete(id) + } +} \ No newline at end of file