From 897ccf475aa417ec61bdd534b0d6d40b4ee2ea33 Mon Sep 17 00:00:00 2001 From: Sebastian Mateos Nicolajsen <80953876+sebastiannicolajsen@users.noreply.github.com> Date: Wed, 22 Sep 2021 15:19:35 +0200 Subject: [PATCH] Feat/note on order (#399) * added NoteService and related endpoints && tests * removed snapshots * corrected error in service * removed snapshot * added the ability to note down author using a string * updated model for note * refactored to access logged in user * added other user id option * removed snapshot * updated according to feedback * removed snapshots * reintroduced snapshots * updated to snake case * removed try catch from use-db --- docs-util/helpers/test-server.js | 32 +-- docs-util/helpers/use-db.js | 44 +-- integration-tests/api/__tests__/admin/note.js | 268 ++++++++++++++++++ integration-tests/api/helpers/admin-seeder.js | 14 +- packages/medusa/src/api/routes/admin/index.js | 2 + .../src/api/routes/admin/notes/create-note.js | 63 ++++ .../src/api/routes/admin/notes/delete-note.js | 35 +++ .../src/api/routes/admin/notes/get-note.js | 31 ++ .../src/api/routes/admin/notes/index.js | 20 ++ .../src/api/routes/admin/notes/list-notes.js | 42 +++ .../src/api/routes/admin/notes/update-note.js | 51 ++++ packages/medusa/src/index.js | 1 + .../src/migrations/1632220294687-add_notes.ts | 23 ++ packages/medusa/src/models/note.ts | 96 +++++++ packages/medusa/src/repositories/note.ts | 5 + .../medusa/src/services/__tests__/note.js | 202 +++++++++++++ packages/medusa/src/services/note.js | 169 +++++++++++ packages/medusa/src/services/notification.js | 2 +- 18 files changed, 1054 insertions(+), 46 deletions(-) create mode 100644 integration-tests/api/__tests__/admin/note.js create mode 100644 packages/medusa/src/api/routes/admin/notes/create-note.js create mode 100644 packages/medusa/src/api/routes/admin/notes/delete-note.js create mode 100644 packages/medusa/src/api/routes/admin/notes/get-note.js create mode 100644 packages/medusa/src/api/routes/admin/notes/index.js create mode 100644 packages/medusa/src/api/routes/admin/notes/list-notes.js create mode 100644 packages/medusa/src/api/routes/admin/notes/update-note.js create mode 100644 packages/medusa/src/migrations/1632220294687-add_notes.ts create mode 100644 packages/medusa/src/models/note.ts create mode 100644 packages/medusa/src/repositories/note.ts create mode 100644 packages/medusa/src/services/__tests__/note.js create mode 100644 packages/medusa/src/services/note.js diff --git a/docs-util/helpers/test-server.js b/docs-util/helpers/test-server.js index 785c39fd7d..fcda9df353 100644 --- a/docs-util/helpers/test-server.js +++ b/docs-util/helpers/test-server.js @@ -1,34 +1,34 @@ -const path = require("path"); -const express = require("express"); -const getPort = require("get-port"); -const importFrom = require("import-from"); +const path = require("path") +const express = require("express") +const getPort = require("get-port") +const importFrom = require("import-from") const initialize = async () => { - const app = express(); + const app = express() - const cwd = process.cwd(); - const loaders = importFrom(cwd, "@medusajs/medusa/dist/loaders").default; + const cwd = process.cwd() + const loaders = importFrom(cwd, "@medusajs/medusa/dist/loaders").default const { dbConnection } = await loaders({ directory: path.resolve(process.cwd()), expressApp: app, - }); + }) - const PORT = await getPort(); + const PORT = await getPort() return { db: dbConnection, app, port: PORT, - }; -}; + } +} const setup = async () => { - const { app, port } = await initialize(); + const { app, port } = await initialize() app.listen(port, (err) => { - process.send(port); - }); -}; + process.send(port) + }) +} -setup(); +setup() diff --git a/docs-util/helpers/use-db.js b/docs-util/helpers/use-db.js index d533ae5ba2..c451fe0cb0 100644 --- a/docs-util/helpers/use-db.js +++ b/docs-util/helpers/use-db.js @@ -1,26 +1,26 @@ -const { dropDatabase, createDatabase } = require("pg-god"); -const { createConnection } = require("typeorm"); +const { dropDatabase, createDatabase } = require("pg-god") +const { createConnection } = require("typeorm") -const path = require("path"); +const path = require("path") const DbTestUtil = { db_: null, setDb: function (connection) { - this.db_ = connection; + this.db_ = connection }, clear: function () { - return this.db_.synchronize(true); + return this.db_.synchronize(true) }, shutdown: async function () { - await this.db_.close(); - return dropDatabase({ databaseName }); + await this.db_.close() + return dropDatabase({ databaseName }) }, -}; +} -const instance = DbTestUtil; +const instance = DbTestUtil module.exports = { initDb: async function ({ cwd }) { @@ -33,19 +33,19 @@ module.exports = { `dist`, `migrations` ) - ); + ) - const databaseName = "medusa-fixtures"; - await createDatabase({ databaseName }); + const databaseName = "medusa-fixtures" + await createDatabase({ databaseName }) const connection = await createConnection({ type: "postgres", url: "postgres://localhost/medusa-fixtures", migrations: [`${migrationDir}/*.js`], - }); + }) - await connection.runMigrations(); - await connection.close(); + await connection.runMigrations() + await connection.close() const modelsLoader = require(path.join( cwd, @@ -55,19 +55,19 @@ module.exports = { `dist`, `loaders`, `models` - )).default; + )).default - const entities = modelsLoader({}, { register: false }); + const entities = modelsLoader({}, { register: false }) const dbConnection = await createConnection({ type: "postgres", url: "postgres://localhost/medusa-fixtures", entities, - }); + }) - instance.setDb(dbConnection); - return dbConnection; + instance.setDb(dbConnection) + return dbConnection }, useDb: function () { - return instance; + return instance }, -}; +} diff --git a/integration-tests/api/__tests__/admin/note.js b/integration-tests/api/__tests__/admin/note.js new file mode 100644 index 0000000000..6e3bd05df0 --- /dev/null +++ b/integration-tests/api/__tests__/admin/note.js @@ -0,0 +1,268 @@ +const path = require("path") +const { Note } = require("@medusajs/medusa") + +const setupServer = require("../../../helpers/setup-server") +const { useApi } = require("../../../helpers/use-api") +const { initDb, useDb } = require("../../../helpers/use-db") + +const adminSeeder = require("../../helpers/admin-seeder") + +jest.setTimeout(30000) + +const note = { + id: "note1", + value: "note text", + resource_id: "resource1", + resource_type: "type", + author: { id: "admin_user" }, +} + +describe("/admin/notes", () => { + let medusaProcess + let dbConnection + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..")) + dbConnection = await initDb({ cwd }) + medusaProcess = await setupServer({ cwd }) + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + medusaProcess.kill() + }) + + describe("GET /admin/notes/:id", () => { + beforeEach(async () => { + const manager = dbConnection.manager + try { + await adminSeeder(dbConnection) + + await manager.insert(Note, note) + } catch (err) { + console.log(err) + } + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("properly retrieves note", async () => { + const api = useApi() + + const response = await api.get("/admin/notes/note1", { + headers: { + authorization: "Bearer test_token", + }, + }) + + expect(response.data).toMatchObject({ + note: { + id: "note1", + resource_id: "resource1", + resource_type: "type", + value: "note text", + author: { id: "admin_user" }, + }, + }) + }) + }) + + describe("POST /admin/notes", () => { + beforeEach(async () => { + try { + await adminSeeder(dbConnection) + } catch (err) { + console.log(err) + } + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("creates a note", async () => { + const api = useApi() + + const response = await api + .post( + "/admin/notes", + { + resource_id: "resource-id", + resource_type: "resource-type", + value: "my note", + }, + { + headers: { + authorization: "Bearer test_token", + }, + } + ) + .catch((err) => { + console.log(err) + }) + + expect(response.data).toMatchObject({ + note: { + id: expect.stringMatching(/^note_*/), + resource_id: "resource-id", + resource_type: "resource-type", + value: "my note", + author_id: "admin_user", + }, + }) + }) + }) + + describe("GET /admin/notes", () => { + beforeEach(async () => { + const manager = dbConnection.manager + try { + await adminSeeder(dbConnection) + + await manager.insert(Note, { ...note, id: "note1" }) + await manager.insert(Note, { ...note, id: "note2" }) + await manager.insert(Note, { + ...note, + id: "note3", + resource_id: "resource2", + }) + } catch (err) { + console.log(err) + } + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("lists notes only related to wanted resource", async () => { + const api = useApi() + const response = await api + .get("/admin/notes?resource_id=resource1", { + headers: { + authorization: "Bearer test_token", + }, + }) + .catch((err) => { + console.log(err) + }) + + expect(response.data.notes.length).toEqual(2) + expect(response.data).toMatchObject({ + notes: [ + { + id: "note1", + resource_id: "resource1", + resource_type: "type", + value: "note text", + author: { id: "admin_user" }, + }, + { + id: "note2", + resource_id: "resource1", + resource_type: "type", + value: "note text", + author: { id: "admin_user" }, + }, + ], + }) + }) + }) + + describe("POST /admin/notes/:id", () => { + beforeEach(async () => { + const manager = dbConnection.manager + try { + await adminSeeder(dbConnection) + + await manager.insert(Note, note) + } catch (err) { + console.log(err) + } + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("updates the content of the note", async () => { + const api = useApi() + + await api + .post( + "/admin/notes/note1", + { value: "new text" }, + { + headers: { + authorization: "Bearer test_token", + }, + } + ) + .catch((err) => { + console.log(err) + }) + + const response = await api + .get("/admin/notes/note1", { + headers: { + authorization: "Bearer test_token", + }, + }) + .catch((err) => { + console.log(err) + }) + + expect(response.data.note.value).toEqual("new text") + }) + }) + + describe("DELETE /admin/notes/:id", () => { + beforeEach(async () => { + const manager = dbConnection.manager + try { + await adminSeeder(dbConnection) + + await manager.insert(Note, note) + } catch (err) { + console.log(err) + } + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("deletes the wanted note", async () => { + const api = useApi() + + await api + .delete("/admin/notes/note1", { + headers: { + authorization: "Bearer test_token", + }, + }) + .catch((err) => { + console.log(err) + }) + + let error + await api + .get("/admin/notes/note1", { + headers: { + authorization: "Bearer test_token", + }, + }) + .catch((err) => (error = err)) + + expect(error.response.status).toEqual(404) + }) + }) +}) diff --git a/integration-tests/api/helpers/admin-seeder.js b/integration-tests/api/helpers/admin-seeder.js index 9a21256360..4a6fcfce82 100644 --- a/integration-tests/api/helpers/admin-seeder.js +++ b/integration-tests/api/helpers/admin-seeder.js @@ -1,11 +1,11 @@ -const Scrypt = require("scrypt-kdf"); -const { User } = require("@medusajs/medusa"); +const Scrypt = require("scrypt-kdf") +const { User } = require("@medusajs/medusa") module.exports = async (connection, data = {}) => { - const manager = connection.manager; + const manager = connection.manager - const buf = await Scrypt.kdf("secret_password", { logN: 1, r: 1, p: 1 }); - const password_hash = buf.toString("base64"); + const buf = await Scrypt.kdf("secret_password", { logN: 1, r: 1, p: 1 }) + const password_hash = buf.toString("base64") await manager.insert(User, { id: "admin_user", @@ -13,5 +13,5 @@ module.exports = async (connection, data = {}) => { api_token: "test_token", password_hash, ...data, - }); -}; + }) +} diff --git a/packages/medusa/src/api/routes/admin/index.js b/packages/medusa/src/api/routes/admin/index.js index c5b5d4b557..1ce749b289 100644 --- a/packages/medusa/src/api/routes/admin/index.js +++ b/packages/medusa/src/api/routes/admin/index.js @@ -22,6 +22,7 @@ import variantRoutes from "./variants" import draftOrderRoutes from "./draft-orders" import collectionRoutes from "./collections" import notificationRoutes from "./notifications" +import noteRoutes from "./notes" const route = Router() @@ -68,6 +69,7 @@ export default (app, container, config) => { collectionRoutes(route) notificationRoutes(route) returnReasonRoutes(route) + noteRoutes(route) return app } diff --git a/packages/medusa/src/api/routes/admin/notes/create-note.js b/packages/medusa/src/api/routes/admin/notes/create-note.js new file mode 100644 index 0000000000..518375876f --- /dev/null +++ b/packages/medusa/src/api/routes/admin/notes/create-note.js @@ -0,0 +1,63 @@ +import { MedusaError, Validator } from "medusa-core-utils" + +/** + * @oas [post] /notes + * operationId: "PostNotes" + * summary: "Creates a Note" + * description: "Creates a Note which can be associated with any resource as required." + * requestBody: + * content: + * application/json: + * schema: + * properties: + * resource_id: + * type: string + * description: The id of the resource which the Note relates to. + * resource_type: + * type: string + * description: The type of resource which the Note relates to. + * value: + * type: string + * description: The content of the Note to create. + * tags: + * - Note + * responses: + * 200: + * description: OK + * content: + * application/json: + * schema: + * properties: + * note: + * $ref: "#/components/schemas/note" + * + */ +export default async (req, res) => { + const schema = Validator.object().keys({ + resource_id: Validator.string(), + resource_type: Validator.string(), + value: Validator.string(), + }) + + const userId = req.user.id || req.user.userId + + const { value, error } = schema.validate(req.body) + if (error) { + throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details) + } + + try { + const noteService = req.scope.resolve("noteService") + + const result = await noteService.create({ + resource_id: value.resource_id, + resource_type: value.resource_type, + value: value.value, + author_id: userId, + }) + + res.status(200).json({ note: result }) + } catch (err) { + throw err + } +} diff --git a/packages/medusa/src/api/routes/admin/notes/delete-note.js b/packages/medusa/src/api/routes/admin/notes/delete-note.js new file mode 100644 index 0000000000..a9d079d5e6 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/notes/delete-note.js @@ -0,0 +1,35 @@ +/** + * @oas [delete] /notes/{id} + * operationId: "DeleteNotesNote" + * summary: "Deletes a Note" + * description: "Deletes a Note." + * parameters: + * - (path) id=* {string} The id of the Note to delete. + * tags: + * - Note + * responses: + * 200: + * description: OK + * content: + * application/json: + * schema: + * properties: + * id: + * type: string + * description: The id of the deleted Note. + * deleted: + * type: boolean + * description: Whether or not the Note was deleted. + */ +export default async (req, res) => { + const { id } = req.params + + try { + const noteService = req.scope.resolve("noteService") + await noteService.delete(id) + + res.status(200).json({ id, deleted: true }) + } catch (err) { + throw err + } +} diff --git a/packages/medusa/src/api/routes/admin/notes/get-note.js b/packages/medusa/src/api/routes/admin/notes/get-note.js new file mode 100644 index 0000000000..831b4f6b9e --- /dev/null +++ b/packages/medusa/src/api/routes/admin/notes/get-note.js @@ -0,0 +1,31 @@ +/** + * @oas [get] /notes/{id} + * operationId: "GetNoteNote" + * summary: "Get Note" + * description: "Retrieves a single note using its id" + * parameters: + * - (path) id=* {string} The id of the note to retrieve. + * tags: + * - Note + * responses: + * 200: + * description: OK + * content: + * application/json: + * schema: + * properties: + * note: + * $ref: "#/components/schemas/note" + */ +export default async (req, res) => { + const { id } = req.params + + try { + const noteService = req.scope.resolve("noteService") + const note = await noteService.retrieve(id, { relations: ["author"] }) + + res.status(200).json({ note }) + } catch (err) { + throw err + } +} diff --git a/packages/medusa/src/api/routes/admin/notes/index.js b/packages/medusa/src/api/routes/admin/notes/index.js new file mode 100644 index 0000000000..3bbf13f1e6 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/notes/index.js @@ -0,0 +1,20 @@ +import { Router } from "express" +import middlewares from "../../../middlewares" + +const route = Router() + +export default app => { + app.use("/notes", route) + + route.get("/:id", middlewares.wrap(require("./get-note").default)) + + route.get("/", middlewares.wrap(require("./list-notes").default)) + + route.post("/", middlewares.wrap(require("./create-note").default)) + + route.post("/:id", middlewares.wrap(require("./update-note").default)) + + route.delete("/:id", middlewares.wrap(require("./delete-note").default)) + + return app +} diff --git a/packages/medusa/src/api/routes/admin/notes/list-notes.js b/packages/medusa/src/api/routes/admin/notes/list-notes.js new file mode 100644 index 0000000000..ba2369866e --- /dev/null +++ b/packages/medusa/src/api/routes/admin/notes/list-notes.js @@ -0,0 +1,42 @@ +/** + * @oas [get] /notes + * operationId: "GetNotes" + * summary: "List Notes" + * description: "Retrieves a list of notes" + * tags: + * - Note + * responses: + * 200: + * description: OK + * content: + * application/json: + * schema: + * properties: + * notes: + * type: array + * items: + * $ref: "#/components/schemas/note" + */ +export default async (req, res) => { + try { + const limit = parseInt(req.query.limit) || 50 + const offset = parseInt(req.query.offset) || 0 + + const selector = {} + + if ("resource_id" in req.query) { + selector.resource_id = req.query.resource_id + } + + const noteService = req.scope.resolve("noteService") + const notes = await noteService.list(selector, { + take: limit, + skip: offset, + relations: ["author"], + }) + + res.status(200).json({ notes }) + } catch (err) { + throw err + } +} diff --git a/packages/medusa/src/api/routes/admin/notes/update-note.js b/packages/medusa/src/api/routes/admin/notes/update-note.js new file mode 100644 index 0000000000..eca3822e05 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/notes/update-note.js @@ -0,0 +1,51 @@ +import { MedusaError, Validator } from "medusa-core-utils" + +/** + * @oas [post] /notes/{id} + * operationId: "PostNotesNote" + * summary: "Updates a Note" + * description: "Updates a Note associated with some resource" + * parameters: + * - (path) id=* {string} The id of the Note to update + * requestBody: + * content: + * application/json: + * schema: + * properties: + * value: + * type: string + * description: The updated description of the Note. + * tags: + * - Note + * responses: + * 200: + * description: OK + * content: + * application/json: + * schema: + * properties: + * note: + * $ref: "#/components/schemas/note" + * + */ +export default async (req, res) => { + const { id } = req.params + + const schema = Validator.object().keys({ + value: Validator.string(), + }) + + const { value, error } = schema.validate(req.body) + if (error) { + throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details) + } + + try { + const noteService = req.scope.resolve("noteService") + const result = await noteService.update(id, value.value) + + res.status(200).json({ note: result }) + } catch (err) { + throw err + } +} diff --git a/packages/medusa/src/index.js b/packages/medusa/src/index.js index effb629ac7..fca74c93bd 100644 --- a/packages/medusa/src/index.js +++ b/packages/medusa/src/index.js @@ -45,3 +45,4 @@ export { Swap } from "./models/swap" export { User } from "./models/user" export { DraftOrder } from "./models/draft-order" export { ReturnReason } from "./models/return-reason" +export { Note } from "./models/note" diff --git a/packages/medusa/src/migrations/1632220294687-add_notes.ts b/packages/medusa/src/migrations/1632220294687-add_notes.ts new file mode 100644 index 0000000000..9550e11507 --- /dev/null +++ b/packages/medusa/src/migrations/1632220294687-add_notes.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class addNotes1632220294687 implements MigrationInterface { + name = "addNotes1632220294687" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "note" ("id" character varying NOT NULL, "value" character varying NOT NULL, "resource_type" character varying NOT NULL, "resource_id" character varying NOT NULL, "author_id" character varying, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP WITH TIME ZONE, "metadata" jsonb, CONSTRAINT "PK_96d0c172a4fba276b1bbed43058" PRIMARY KEY ("id"))` + ) + await queryRunner.query( + `CREATE INDEX "IDX_f74980b411cf94af523a72af7d" ON "note" ("resource_type") ` + ) + await queryRunner.query( + `CREATE INDEX "IDX_3287f98befad26c3a7dab088cf" ON "note" ("resource_id") ` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_3287f98befad26c3a7dab088cf"`) + await queryRunner.query(`DROP INDEX "IDX_f74980b411cf94af523a72af7d"`) + await queryRunner.query(`DROP TABLE "note"`) + } +} diff --git a/packages/medusa/src/models/note.ts b/packages/medusa/src/models/note.ts new file mode 100644 index 0000000000..77c29f89df --- /dev/null +++ b/packages/medusa/src/models/note.ts @@ -0,0 +1,96 @@ +import { + Entity, + BeforeInsert, + Column, + DeleteDateColumn, + CreateDateColumn, + UpdateDateColumn, + Index, + JoinColumn, + PrimaryColumn, + ManyToOne, +} from "typeorm" +import { ulid } from "ulid" +import { User } from "./user" +import { resolveDbType, DbAwareColumn } from "../utils/db-aware-column" + +@Entity() +export class Note { + @PrimaryColumn() + id: string + + @Column() + value: string + + @Index() + @Column() + resource_type: string + + @Index() + @Column() + resource_id: string + + @Column({ nullable: true }) + author_id: string + + @ManyToOne(() => User) + @JoinColumn({ name: "author_id" }) + author: User + + @CreateDateColumn({ type: resolveDbType("timestamptz") }) + created_at: Date + + @UpdateDateColumn({ type: resolveDbType("timestamptz") }) + updated_at: Date + + @DeleteDateColumn({ type: resolveDbType("timestamptz") }) + deleted_at: Date + + @DbAwareColumn({ type: "jsonb", nullable: true }) + metadata: any + + @BeforeInsert() + private beforeInsert() { + if (this.id) return + const id = ulid() + this.id = `note_${id}` + } +} + +/** + * @schema note + * title: "Note" + * description: "Notes are elements which we can use in association with different resources to allow users to describe additional information in relation to these." + * x-resourceId: note + * properties: + * id: + * description: "The id of the Note. This value will be prefixed by `note_`." + * type: string + * resource_type: + * description: "The type of resource that the Note refers to." + * type: string + * resource_id: + * description: "The id of the resource that the Note refers to." + * type: string + * value: + * description: "The contents of the note." + * type: string + * author: + * description: "The author of the note." + * type: User + * created_at: + * description: "The date with timezone at which the resource was created." + * type: string + * format: date-time + * updated_at: + * description: "The date with timezone at which the resource was last updated." + * type: string + * format: date-time + * deleted_at: + * description: "The date with timezone at which the resource was deleted." + * type: string + * format: date-time + * metadata: + * description: "An optional key-value map with additional information." + * type: object + */ diff --git a/packages/medusa/src/repositories/note.ts b/packages/medusa/src/repositories/note.ts new file mode 100644 index 0000000000..1e3065c417 --- /dev/null +++ b/packages/medusa/src/repositories/note.ts @@ -0,0 +1,5 @@ +import { EntityRepository, Repository } from "typeorm" +import { Note } from "../models/note" + +@EntityRepository(Note) +export class NoteRepository extends Repository {} diff --git a/packages/medusa/src/services/__tests__/note.js b/packages/medusa/src/services/__tests__/note.js new file mode 100644 index 0000000000..e2b9af4697 --- /dev/null +++ b/packages/medusa/src/services/__tests__/note.js @@ -0,0 +1,202 @@ +import NoteService from "../note" +import { MockManager, MockRepository, IdMap } from "medusa-test-utils" +import { EventBusServiceMock } from "../__mocks__/event-bus" + +describe("NoteService", () => { + describe("list", () => { + const noteRepo = MockRepository({ + find: q => { + return Promise.resolve([ + { id: IdMap.getId("note"), value: "some note" }, + ]) + }, + }) + + const noteService = new NoteService({ + manager: MockManager, + noteRepository: noteRepo, + }) + + beforeAll(async () => { + jest.clearAllMocks() + }) + + it("calls note model functions", async () => { + await noteService.list( + { resource_id: IdMap.getId("note") }, + { + relations: ["author"], + } + ) + expect(noteRepo.find).toHaveBeenCalledTimes(1) + expect(noteRepo.find).toHaveBeenCalledWith({ + where: { + resource_id: IdMap.getId("note"), + }, + relations: ["author"], + }) + }) + }) + + describe("retrieve", () => { + const noteRepo = MockRepository({ + findOne: q => { + switch (q.where.id) { + case IdMap.getId("note"): + return Promise.resolve({ + id: IdMap.getId("note"), + value: "some note", + }) + default: + return Promise.resolve() + } + }, + }) + + const noteService = new NoteService({ + manager: MockManager, + noteRepository: noteRepo, + }) + + beforeAll(async () => { + jest.clearAllMocks() + }) + + it("calls note model functions", async () => { + await noteService.retrieve(IdMap.getId("note"), { relations: ["author"] }) + + expect(noteRepo.findOne).toHaveBeenCalledTimes(1) + expect(noteRepo.findOne).toHaveBeenCalledWith({ + where: { id: IdMap.getId("note") }, + relations: ["author"], + }) + }) + + it("fails when note is not found", async () => { + await expect( + noteService.retrieve(IdMap.getId("not-existing")) + ).rejects.toThrow( + `Note with id: ${IdMap.getId("not-existing")} was not found.` + ) + }) + }) + + describe("create", () => { + const note = { + id: IdMap.getId("note"), + author_id: IdMap.getId("user"), + } + + const noteRepo = MockRepository({ + create: f => Promise.resolve(note), + save: f => Promise.resolve(note), + }) + + const noteService = new NoteService({ + manager: MockManager, + noteRepository: noteRepo, + eventBusService: EventBusServiceMock, + }) + + beforeAll(async () => { + jest.clearAllMocks() + }) + + it("calls note model functions", async () => { + await noteService.create({ + resource_id: IdMap.getId("resource-id"), + resource_type: "type", + value: "my note", + author_id: IdMap.getId("user"), + }) + + expect(noteRepo.create).toHaveBeenCalledTimes(1) + expect(noteRepo.create).toHaveBeenCalledWith({ + resource_id: IdMap.getId("resource-id"), + resource_type: "type", + value: "my note", + author_id: IdMap.getId("user"), + metadata: {}, + }) + + expect(noteRepo.save).toHaveBeenCalledTimes(1) + expect(noteRepo.save).toHaveBeenCalledWith({ + id: IdMap.getId("note"), + author_id: IdMap.getId("user"), + }) + + expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(1) + expect(EventBusServiceMock.emit).toHaveBeenCalledWith( + NoteService.Events.CREATED, + { id: IdMap.getId("note") } + ) + }) + }) + + describe("update", () => { + const note = { id: IdMap.getId("note") } + + const noteRepo = MockRepository({ + findOne: f => Promise.resolve(note), + save: f => Promise.resolve(note), + }) + + const noteService = new NoteService({ + manager: MockManager, + noteRepository: noteRepo, + eventBusService: EventBusServiceMock, + }) + + beforeAll(async () => { + jest.clearAllMocks() + }) + + it("calls note model functions", async () => { + await noteService.update(IdMap.getId("note"), "new note") + + expect(noteRepo.save).toHaveBeenCalledTimes(1) + expect(noteRepo.save).toHaveBeenCalledWith({ + ...note, + value: "new note", + }) + + expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(1) + expect(EventBusServiceMock.emit).toHaveBeenCalledWith( + NoteService.Events.UPDATED, + { id: IdMap.getId("note") } + ) + }) + }) + + describe("delete", () => { + const note = { id: IdMap.getId("note") } + + const noteRepo = MockRepository({ + softRemove: f => Promise.resolve(), + findOne: f => Promise.resolve(note), + }) + + const noteService = new NoteService({ + manager: MockManager, + noteRepository: noteRepo, + eventBusService: EventBusServiceMock, + }) + + beforeAll(async () => { + jest.clearAllMocks() + }) + + it("calls note model functions", async () => { + await noteService.delete(IdMap.getId("note")) + + expect(noteRepo.softRemove).toHaveBeenCalledTimes(1) + expect(noteRepo.softRemove).toHaveBeenCalledWith(note) + + expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(1) + expect(EventBusServiceMock.emit).toHaveBeenCalledWith( + NoteService.Events.DELETED, + { id: IdMap.getId("note") } + ) + }) + }) +}) diff --git a/packages/medusa/src/services/note.js b/packages/medusa/src/services/note.js new file mode 100644 index 0000000000..25d032803f --- /dev/null +++ b/packages/medusa/src/services/note.js @@ -0,0 +1,169 @@ +import { MedusaError } from "medusa-core-utils" +import { BaseService } from "medusa-interfaces" +import _ from "lodash" +import { TransactionManager } from "typeorm" + +class NoteService extends BaseService { + static Events = { + CREATED: "note.created", + UPDATED: "note.updated", + DELETED: "note.deleted", + } + + constructor({ manager, noteRepository, eventBusService, userService }) { + super() + + /** @private @const {EntityManager} */ + this.manager_ = manager + + /** @private @const {NoteRepository} */ + this.noteRepository_ = noteRepository + + /** @private @const {EventBus} */ + this.eventBus_ = eventBusService + } + + /** + * Sets the service's manager to a given transaction manager + * @param {EntityManager} transactionManager - the manager to use + * @return {NoteService} a cloned note service + */ + withTransaction(transactionManager) { + if (!TransactionManager) { + return this + } + + const cloned = new NoteService({ + manager: transactionManager, + noteRepository: this.noteRepository_, + eventBus: this.eventBus_, + }) + + cloned.transactionManager_ = transactionManager + return cloned + } + + /** + * Retrieves a specific note. + * @param {*} id - the id of the note to retrieve. + * @param {*} config - any options needed to query for the result. + * @returns {Promise} which resolves to the requested note. + */ + async retrieve(id, config = {}) { + const noteRepo = this.manager_.getCustomRepository(this.noteRepository_) + + const validatedId = this.validateId_(id) + const query = this.buildQuery_({ id: validatedId }, config) + + const note = await noteRepo.findOne(query) + + if (!note) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Note with id: ${id} was not found.` + ) + } + + return note + } + + /** Fetches all notes related to the given selector + * @param {Object} selector - the query object for find + * @param {Object} config - the configuration used to find the objects. contains relations, skip, and take. + * @return {Promise} notes related to the given search. + */ + async list( + selector, + config = { + skip: 0, + take: 50, + relations: [], + } + ) { + const noteRepo = this.manager_.getCustomRepository(this.noteRepository_) + + const query = this.buildQuery_(selector, config) + + return noteRepo.find(query) + } + + /** + * Creates a note associated with a given author + * @param {object} data - the note to create + * @param {*} config - any configurations if needed, including meta data + * @returns {Promise} resolves to the creation result + */ + async create(data, config = { metadata: {} }) { + const { metadata } = config + + const { resource_id, resource_type, value, author_id } = data + + return this.atomicPhase_(async manager => { + const noteRepo = manager.getCustomRepository(this.noteRepository_) + + const toCreate = { + resource_id, + resource_type, + value, + author_id, + metadata, + } + + const note = await noteRepo.create(toCreate) + const result = await noteRepo.save(note) + + await this.eventBus_ + .withTransaction(manager) + .emit(NoteService.Events.CREATED, { id: result.id }) + + return result + }) + } + + /** + * Updates a given note with a new value + * @param {*} noteId - the id of the note to update + * @param {*} value - the new value + * @returns {Promise} resolves to the updated element + */ + async update(noteId, value) { + return this.atomicPhase_(async manager => { + const noteRepo = manager.getCustomRepository(this.noteRepository_) + + const note = await this.retrieve(noteId, { relations: ["author"] }) + + note.value = value + + const result = await noteRepo.save(note) + + await this.eventBus_ + .withTransaction(manager) + .emit(NoteService.Events.UPDATED, { id: result.id }) + + return result + }) + } + + /** + * Deletes a given note + * @param {*} noteId - id of the note to delete + * @returns {Promise} + */ + async delete(noteId) { + return this.atomicPhase_(async manager => { + const noteRepo = manager.getCustomRepository(this.noteRepository_) + + const note = await this.retrieve(noteId) + + await noteRepo.softRemove(note) + + await this.eventBus_ + .withTransaction(manager) + .emit(NoteService.Events.DELETED, { id: noteId }) + + return Promise.resolve() + }) + } +} + +export default NoteService diff --git a/packages/medusa/src/services/notification.js b/packages/medusa/src/services/notification.js index 8dcf7962a0..615efad575 100644 --- a/packages/medusa/src/services/notification.js +++ b/packages/medusa/src/services/notification.js @@ -41,7 +41,7 @@ class NotificationService extends BaseService { /** * Sets the service's manager to a given transaction manager. - * @parma {EntityManager} transactionManager - the manager to use + * @param {EntityManager} transactionManager - the manager to use * return {NotificationService} a cloned notification service */ withTransaction(transactionManager) {