From bfb81b8b32fbba538010221f1a30fcc31aaa691e Mon Sep 17 00:00:00 2001 From: adrien2p Date: Thu, 14 Apr 2022 18:33:04 +0200 Subject: [PATCH 01/16] feat(medusa): Improve base-service --- .../interfaces/__tests__/base-service.spec.ts | 6 +- .../medusa/src/interfaces/base-service.ts | 89 +++++++++---------- 2 files changed, 45 insertions(+), 50 deletions(-) diff --git a/packages/medusa/src/interfaces/__tests__/base-service.spec.ts b/packages/medusa/src/interfaces/__tests__/base-service.spec.ts index 9166759fac..9fa804e4f1 100644 --- a/packages/medusa/src/interfaces/__tests__/base-service.spec.ts +++ b/packages/medusa/src/interfaces/__tests__/base-service.spec.ts @@ -1,4 +1,4 @@ -import BaseService from "../base-service" +import { BaseService } from "../base-service" import { In, Not } from "typeorm" import { MockManager } from "medusa-test-utils" @@ -6,7 +6,7 @@ describe("BaseService", () => { it("should cloned the child class withTransaction", () => { class Child extends BaseService { constructor(protected readonly container) { - super(container, {}); + super(container); this.container = container } @@ -58,4 +58,4 @@ describe("BaseService", () => { }) }) }) -}) +}) \ No newline at end of file diff --git a/packages/medusa/src/interfaces/base-service.ts b/packages/medusa/src/interfaces/base-service.ts index 18560465f2..d606da89e7 100644 --- a/packages/medusa/src/interfaces/base-service.ts +++ b/packages/medusa/src/interfaces/base-service.ts @@ -1,7 +1,7 @@ import { MedusaError } from "medusa-core-utils" import { EntityManager, FindOperator, In, Raw } from "typeorm" import { IsolationLevel } from "typeorm/driver/types/IsolationLevel" -import { FindConfig } from "../types/common" +import { FindConfig, Writable } from "../types/common" type Selector = { [key in keyof TEntity]?: unknown } @@ -9,7 +9,7 @@ type Selector = { [key in keyof TEntity]?: unknown } * Common functionality for Services * @interface */ -class BaseService< +export class BaseService< TChild extends BaseService, TContainer = unknown > { @@ -19,22 +19,17 @@ class BaseService< constructor( container: TContainer, - protected readonly configModule: Record + protected readonly configModule?: Record ) { this.container_ = container } - withTransaction(): this - withTransaction(transactionManager: EntityManager): TChild withTransaction(transactionManager?: EntityManager): this | TChild { if (!transactionManager) { return this } - const cloned = new (this.constructor)< - TChild, - TContainer - >( + const cloned = new (this.constructor)( { ...this.container_, manager: transactionManager, @@ -57,12 +52,12 @@ class BaseService< selector: Selector, config: FindConfig = {} ): FindConfig & { - where: { [key in keyof TEntity]?: unknown } + where: Partial> withDeleted?: boolean } { const build = ( obj: Record - ): { [key in keyof TEntity]?: unknown } => { + ): Partial> => { return Object.entries(obj).reduce((acc, [key, value]: any) => { // Undefined values indicate that they have no significance to the query. // If the query is looking for rows where a column is not set it should use null instead of undefined @@ -83,27 +78,25 @@ class BaseService< acc[key] = In([...(value as unknown[])]) break case value !== null && typeof value === "object": - Object.entries(value as Record).map( - ([modifier, val]) => { - switch (modifier) { - case "lt": - subquery.push({ operator: "<", value: val }) - break - case "gt": - subquery.push({ operator: ">", value: val }) - break - case "lte": - subquery.push({ operator: "<=", value: val }) - break - case "gte": - subquery.push({ operator: ">=", value: val }) - break - default: - acc[key] = value - break - } + Object.entries(value).map(([modifier, val]) => { + switch (modifier) { + case "lt": + subquery.push({ operator: "<", value: val }) + break + case "gt": + subquery.push({ operator: ">", value: val }) + break + case "lte": + subquery.push({ operator: "<=", value: val }) + break + case "gte": + subquery.push({ operator: ">=", value: val }) + break + default: + acc[key] = value + break } - ) + }) if (subquery.length) { acc[key] = Raw( @@ -121,11 +114,11 @@ class BaseService< } return acc - }, {} as { [key in keyof TEntity]?: unknown }) + }, {} as Partial>) } const query: FindConfig & { - where: { [key in keyof TEntity]?: unknown } + where: Partial> withDeleted?: boolean } = { where: build(selector), @@ -217,17 +210,19 @@ class BaseService< * @param maybeErrorHandlerOrDontFail Potential error handler * @return the result of the transactional work */ - async atomicPhase_( - work: (transactionManager: EntityManager) => Promise, + async atomicPhase_( + work: (transactionManager: EntityManager) => Promise, isolationOrErrorHandler?: | IsolationLevel - | ((error: unknown) => Promise), - maybeErrorHandlerOrDontFail?: (error: unknown) => Promise - ): Promise { + | ((error: TError) => Promise), + maybeErrorHandlerOrDontFail?: ( + error: TError + ) => Promise + ): Promise { let errorHandler = maybeErrorHandlerOrDontFail let isolation: | IsolationLevel - | ((error: unknown) => Promise) + | ((error: TError) => Promise) | undefined | null = isolationOrErrorHandler let dontFail = false @@ -238,7 +233,7 @@ class BaseService< } if (this.transactionManager_) { - const doWork = async (m: EntityManager): Promise => { + const doWork = async (m: EntityManager): Promise => { this.manager_ = m this.transactionManager_ = m try { @@ -256,10 +251,10 @@ class BaseService< } } - return doWork(this.transactionManager_) + return await doWork(this.transactionManager_) } else { const temp = this.manager_ - const doWork = async (m: EntityManager): Promise => { + const doWork = async (m: EntityManager): Promise => { this.manager_ = m this.transactionManager_ = m try { @@ -284,8 +279,9 @@ class BaseService< return result } catch (error) { if (this.shouldRetryTransaction(error)) { - return this.manager_.transaction(isolation as IsolationLevel, (m) => - doWork(m) + return this.manager_.transaction( + isolation as IsolationLevel, + (m): Promise => doWork(m) ) } else { if (errorHandler) { @@ -302,7 +298,7 @@ class BaseService< if (errorHandler) { const result = await errorHandler(error) if (dontFail) { - return result + return result as TResult } } @@ -338,5 +334,4 @@ class BaseService< ...newData, } } -} -export default BaseService +} \ No newline at end of file From 1499bc52e36616ca9d67fd55bf5965478dff390f Mon Sep 17 00:00:00 2001 From: adrien2p Date: Thu, 14 Apr 2022 18:55:27 +0200 Subject: [PATCH 02/16] feat(medusa): Add writable type --- packages/medusa/src/types/common.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/medusa/src/types/common.ts b/packages/medusa/src/types/common.ts index 94f06c01e1..9837a0de1e 100644 --- a/packages/medusa/src/types/common.ts +++ b/packages/medusa/src/types/common.ts @@ -7,6 +7,8 @@ export type PartialPick = { [P in K]?: T[P] } +export type Writable = { -readonly [key in keyof T]: T[key] } + export type TotalField = | "shipping_total" | "discount_total" From 99146b74037b89c6893c97e77e43a1aaf1c2a3d4 Mon Sep 17 00:00:00 2001 From: adrien2p Date: Sun, 17 Apr 2022 20:52:48 +0200 Subject: [PATCH 03/16] feat(medusa): Export transaction related methods to the transactionBaseService --- .../medusa/src/interfaces/base-service.ts | 137 +--------------- .../interfaces/transaction-base-service.ts | 149 ++++++++++++++++++ 2 files changed, 150 insertions(+), 136 deletions(-) create mode 100644 packages/medusa/src/interfaces/transaction-base-service.ts diff --git a/packages/medusa/src/interfaces/base-service.ts b/packages/medusa/src/interfaces/base-service.ts index d606da89e7..b87c15dd32 100644 --- a/packages/medusa/src/interfaces/base-service.ts +++ b/packages/medusa/src/interfaces/base-service.ts @@ -1,6 +1,5 @@ import { MedusaError } from "medusa-core-utils" import { EntityManager, FindOperator, In, Raw } from "typeorm" -import { IsolationLevel } from "typeorm/driver/types/IsolationLevel" import { FindConfig, Writable } from "../types/common" type Selector = { [key in keyof TEntity]?: unknown } @@ -24,24 +23,6 @@ export class BaseService< this.container_ = container } - withTransaction(transactionManager?: EntityManager): this | TChild { - if (!transactionManager) { - return this - } - - const cloned = new (this.constructor)( - { - ...this.container_, - manager: transactionManager, - }, - this.configModule - ) - - cloned.transactionManager_ = transactionManager - - return cloned as TChild - } - /** * Used to build TypeORM queries. * @param selector The selector @@ -191,122 +172,6 @@ export class BaseService< return rawId } - shouldRetryTransaction( - err: { code: string } | Record - ): boolean { - if (!(err as { code: string })?.code) { - return false - } - const code = (err as { code: string })?.code - return code === "40001" || code === "40P01" - } - - /** - * Wraps some work within a transactional block. If the service already has - * a transaction manager attached this will be reused, otherwise a new - * transaction manager is created. - * @param work - the transactional work to be done - * @param isolationOrErrorHandler - the isolation level to be used for the work. - * @param maybeErrorHandlerOrDontFail Potential error handler - * @return the result of the transactional work - */ - async atomicPhase_( - work: (transactionManager: EntityManager) => Promise, - isolationOrErrorHandler?: - | IsolationLevel - | ((error: TError) => Promise), - maybeErrorHandlerOrDontFail?: ( - error: TError - ) => Promise - ): Promise { - let errorHandler = maybeErrorHandlerOrDontFail - let isolation: - | IsolationLevel - | ((error: TError) => Promise) - | undefined - | null = isolationOrErrorHandler - let dontFail = false - if (typeof isolationOrErrorHandler === "function") { - isolation = null - errorHandler = isolationOrErrorHandler - dontFail = !!maybeErrorHandlerOrDontFail - } - - if (this.transactionManager_) { - const doWork = async (m: EntityManager): Promise => { - this.manager_ = m - this.transactionManager_ = m - try { - return await work(m) - } catch (error) { - if (errorHandler) { - const queryRunner = this.transactionManager_.queryRunner - if (queryRunner && queryRunner.isTransactionActive) { - await queryRunner.rollbackTransaction() - } - - await errorHandler(error) - } - throw error - } - } - - return await doWork(this.transactionManager_) - } else { - const temp = this.manager_ - const doWork = async (m: EntityManager): Promise => { - this.manager_ = m - this.transactionManager_ = m - try { - const result = await work(m) - this.manager_ = temp - this.transactionManager_ = undefined - return result - } catch (error) { - this.manager_ = temp - this.transactionManager_ = undefined - throw error - } - } - - if (isolation) { - let result - try { - result = await this.manager_.transaction( - isolation as IsolationLevel, - (m) => doWork(m) - ) - return result - } catch (error) { - if (this.shouldRetryTransaction(error)) { - return this.manager_.transaction( - isolation as IsolationLevel, - (m): Promise => doWork(m) - ) - } else { - if (errorHandler) { - await errorHandler(error) - } - throw error - } - } - } - - try { - return await this.manager_.transaction((m) => doWork(m)) - } catch (error) { - if (errorHandler) { - const result = await errorHandler(error) - if (dontFail) { - return result as TResult - } - } - - throw error - } - } - } - /** * Dedicated method to set metadata. * @param obj - the entity to apply metadata to. @@ -334,4 +199,4 @@ export class BaseService< ...newData, } } -} \ No newline at end of file +} diff --git a/packages/medusa/src/interfaces/transaction-base-service.ts b/packages/medusa/src/interfaces/transaction-base-service.ts new file mode 100644 index 0000000000..cbddd4b6f5 --- /dev/null +++ b/packages/medusa/src/interfaces/transaction-base-service.ts @@ -0,0 +1,149 @@ +import { EntityManager } from "typeorm" +import { IsolationLevel } from "typeorm/driver/types/IsolationLevel" + +export abstract class TransactionBaseService< + TChild extends TransactionBaseService, + TContainer = unknown +> { + protected abstract manager_: EntityManager + protected abstract transactionManager_: EntityManager | undefined + + protected constructor( + protected readonly container: TContainer, + protected readonly configModule?: Record + ) {} + + withTransaction(transactionManager?: EntityManager): this | TChild { + if (!transactionManager) { + return this + } + + const cloned = new (this.constructor)( + { + ...this.container, + manager: transactionManager, + }, + this.configModule + ) + + cloned.transactionManager_ = transactionManager + + return cloned as TChild + } + + shouldRetryTransaction( + err: { code: string } | Record + ): boolean { + if (!(err as { code: string })?.code) { + return false + } + const code = (err as { code: string })?.code + return code === "40001" || code === "40P01" + } + + /** + * Wraps some work within a transactional block. If the service already has + * a transaction manager attached this will be reused, otherwise a new + * transaction manager is created. + * @param work - the transactional work to be done + * @param isolationOrErrorHandler - the isolation level to be used for the work. + * @param maybeErrorHandlerOrDontFail Potential error handler + * @return the result of the transactional work + */ + async atomicPhase_( + work: (transactionManager: EntityManager) => Promise, + isolationOrErrorHandler?: + | IsolationLevel + | ((error: TError) => Promise), + maybeErrorHandlerOrDontFail?: ( + error: TError + ) => Promise + ): Promise { + let errorHandler = maybeErrorHandlerOrDontFail + let isolation: + | IsolationLevel + | ((error: TError) => Promise) + | undefined + | null = isolationOrErrorHandler + let dontFail = false + if (typeof isolationOrErrorHandler === "function") { + isolation = null + errorHandler = isolationOrErrorHandler + dontFail = !!maybeErrorHandlerOrDontFail + } + + if (this.transactionManager_) { + const doWork = async (m: EntityManager): Promise => { + this.manager_ = m + this.transactionManager_ = m + try { + return await work(m) + } catch (error) { + if (errorHandler) { + const queryRunner = this.transactionManager_.queryRunner + if (queryRunner && queryRunner.isTransactionActive) { + await queryRunner.rollbackTransaction() + } + + await errorHandler(error) + } + throw error + } + } + + return await doWork(this.transactionManager_) + } else { + const temp = this.manager_ + const doWork = async (m: EntityManager): Promise => { + this.manager_ = m + this.transactionManager_ = m + try { + const result = await work(m) + this.manager_ = temp + this.transactionManager_ = undefined + return result + } catch (error) { + this.manager_ = temp + this.transactionManager_ = undefined + throw error + } + } + + if (isolation && this.manager_) { + let result + try { + result = await this.manager_.transaction( + isolation as IsolationLevel, + (m) => doWork(m) + ) + return result + } catch (error) { + if (this.shouldRetryTransaction(error)) { + return this.manager_.transaction( + isolation as IsolationLevel, + (m): Promise => doWork(m) + ) + } else { + if (errorHandler) { + await errorHandler(error) + } + throw error + } + } + } + + try { + return await this.manager_.transaction((m) => doWork(m)) + } catch (error) { + if (errorHandler) { + const result = await errorHandler(error) + if (dontFail) { + return result as TResult + } + } + + throw error + } + } + } +} From e7e715ac177bd0b883ffa0d72a208580012bfbab Mon Sep 17 00:00:00 2001 From: adrien2p Date: Mon, 18 Apr 2022 15:45:33 +0200 Subject: [PATCH 04/16] feat(medusa): Split base service to its related TransactionBaseService and utilities methods when required --- .../interfaces/__tests__/base-service.spec.ts | 37 +--- .../medusa/src/interfaces/base-service.ts | 202 ------------------ packages/medusa/src/interfaces/index.ts | 2 +- .../src/utils/__tests__/build-query.spec.ts | 28 +++ packages/medusa/src/utils/build-query.ts | 112 ++++++++++ packages/medusa/src/utils/set-metadata.ts | 29 +++ packages/medusa/src/utils/validate-id.ts | 41 ++++ 7 files changed, 218 insertions(+), 233 deletions(-) delete mode 100644 packages/medusa/src/interfaces/base-service.ts create mode 100644 packages/medusa/src/utils/__tests__/build-query.spec.ts create mode 100644 packages/medusa/src/utils/build-query.ts create mode 100644 packages/medusa/src/utils/set-metadata.ts create mode 100644 packages/medusa/src/utils/validate-id.ts diff --git a/packages/medusa/src/interfaces/__tests__/base-service.spec.ts b/packages/medusa/src/interfaces/__tests__/base-service.spec.ts index 9fa804e4f1..65c5b62608 100644 --- a/packages/medusa/src/interfaces/__tests__/base-service.spec.ts +++ b/packages/medusa/src/interfaces/__tests__/base-service.spec.ts @@ -1,10 +1,13 @@ -import { BaseService } from "../base-service" -import { In, Not } from "typeorm" +import { EntityManager } from "typeorm" import { MockManager } from "medusa-test-utils" +import { TransactionBaseService } from "../transaction-base-service" -describe("BaseService", () => { +describe("TransactionBaseService", () => { it("should cloned the child class withTransaction", () => { - class Child extends BaseService { + class Child extends TransactionBaseService { + protected manager_!: EntityManager + protected transactionManager_!: EntityManager + constructor(protected readonly container) { super(container); this.container = container @@ -32,30 +35,4 @@ describe("BaseService", () => { expect(child2.getTransactionManager()).toBeTruthy() expect((child2.getTransactionManager() as any)?.testProp).toBe('testProp') }) - - describe("buildQuery_", () => { - const baseService = new BaseService({}, {}) - - it("successfully creates query", () => { - const q = baseService.buildQuery_( - { - id: "1234", - test1: ["123", "12", "1"], - test2: Not("this"), - }, - { - relations: ["1234"], - } - ) - - expect(q).toEqual({ - where: { - id: "1234", - test1: In(["123", "12", "1"]), - test2: Not("this"), - }, - relations: ["1234"], - }) - }) - }) }) \ No newline at end of file diff --git a/packages/medusa/src/interfaces/base-service.ts b/packages/medusa/src/interfaces/base-service.ts deleted file mode 100644 index b87c15dd32..0000000000 --- a/packages/medusa/src/interfaces/base-service.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { MedusaError } from "medusa-core-utils" -import { EntityManager, FindOperator, In, Raw } from "typeorm" -import { FindConfig, Writable } from "../types/common" - -type Selector = { [key in keyof TEntity]?: unknown } - -/** - * Common functionality for Services - * @interface - */ -export class BaseService< - TChild extends BaseService, - TContainer = unknown -> { - protected transactionManager_: EntityManager | undefined - protected manager_: EntityManager - private readonly container_: TContainer - - constructor( - container: TContainer, - protected readonly configModule?: Record - ) { - this.container_ = container - } - - /** - * Used to build TypeORM queries. - * @param selector The selector - * @param config The config - * @return The QueryBuilderConfig - */ - buildQuery_( - selector: Selector, - config: FindConfig = {} - ): FindConfig & { - where: Partial> - withDeleted?: boolean - } { - const build = ( - obj: Record - ): Partial> => { - return Object.entries(obj).reduce((acc, [key, value]: any) => { - // Undefined values indicate that they have no significance to the query. - // If the query is looking for rows where a column is not set it should use null instead of undefined - if (typeof value === "undefined") { - return acc - } - - const subquery: { - operator: "<" | ">" | "<=" | ">=" - value: unknown - }[] = [] - - switch (true) { - case value instanceof FindOperator: - acc[key] = value - break - case Array.isArray(value): - acc[key] = In([...(value as unknown[])]) - break - case value !== null && typeof value === "object": - Object.entries(value).map(([modifier, val]) => { - switch (modifier) { - case "lt": - subquery.push({ operator: "<", value: val }) - break - case "gt": - subquery.push({ operator: ">", value: val }) - break - case "lte": - subquery.push({ operator: "<=", value: val }) - break - case "gte": - subquery.push({ operator: ">=", value: val }) - break - default: - acc[key] = value - break - } - }) - - if (subquery.length) { - acc[key] = Raw( - (a) => - subquery - .map((s, index) => `${a} ${s.operator} :${index}`) - .join(" AND "), - subquery.map((s) => s.value) - ) - } - break - default: - acc[key] = value - break - } - - return acc - }, {} as Partial>) - } - - const query: FindConfig & { - where: Partial> - withDeleted?: boolean - } = { - where: build(selector), - } - - if ("deleted_at" in selector) { - query.withDeleted = true - } - - if ("skip" in config) { - query.skip = config.skip - } - - if ("take" in config) { - query.take = config.take - } - - if ("relations" in config) { - query.relations = config.relations - } - - if ("select" in config) { - query.select = config.select - } - - if ("order" in config) { - query.order = config.order - } - - return query - } - - /** - * Confirms whether a given raw id is valid. Fails if the provided - * id is null or undefined. The validate function takes an optional config - * param, to support checking id prefix and length. - * @param rawId - the id to validate. - * @param config - optional config - * @returns the rawId given that nothing failed - */ - validateId_( - rawId: string, - config: { prefix?: string; length?: number } = {} - ): string { - const { prefix, length } = config - if (!rawId) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Failed to validate id: ${rawId}` - ) - } - - if (prefix || length) { - const [pre, rand] = rawId.split("_") - if (prefix && pre !== prefix) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `The provided id: ${rawId} does not adhere to prefix constraint: ${prefix}` - ) - } - - if (length && length !== rand.length) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `The provided id: ${rawId} does not adhere to length constraint: ${length}` - ) - } - } - - return rawId - } - - /** - * Dedicated method to set metadata. - * @param obj - the entity to apply metadata to. - * @param metadata - the metadata to set - * @return resolves to the updated result. - */ - setMetadata_( - obj: { metadata: Record }, - metadata: Record - ): Record { - const existing = obj.metadata || {} - const newData = {} - for (const [key, value] of Object.entries(metadata)) { - if (typeof key !== "string") { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "Key type is invalid. Metadata keys must be strings" - ) - } - newData[key] = value - } - - return { - ...existing, - ...newData, - } - } -} diff --git a/packages/medusa/src/interfaces/index.ts b/packages/medusa/src/interfaces/index.ts index 7552d3c149..9472f26f30 100644 --- a/packages/medusa/src/interfaces/index.ts +++ b/packages/medusa/src/interfaces/index.ts @@ -1,4 +1,4 @@ export * from "./tax-calculation-strategy" export * from "./cart-completion-strategy" export * from "./tax-service" -export * from "./base-service" +export * from "./transaction-base-service" diff --git a/packages/medusa/src/utils/__tests__/build-query.spec.ts b/packages/medusa/src/utils/__tests__/build-query.spec.ts new file mode 100644 index 0000000000..f9a4819390 --- /dev/null +++ b/packages/medusa/src/utils/__tests__/build-query.spec.ts @@ -0,0 +1,28 @@ +import { In, Not } from "typeorm" +import { buildQuery } from "../build-query" + +describe('buildQuery', () => { + describe("buildQuery_", () => { + it("successfully creates query", () => { + const q = buildQuery( + { + id: "1234", + test1: ["123", "12", "1"], + test2: Not("this"), + }, + { + relations: ["1234"], + } + ) + + expect(q).toEqual({ + where: { + id: "1234", + test1: In(["123", "12", "1"]), + test2: Not("this"), + }, + relations: ["1234"], + }) + }) + }) +}) \ No newline at end of file diff --git a/packages/medusa/src/utils/build-query.ts b/packages/medusa/src/utils/build-query.ts new file mode 100644 index 0000000000..103af54379 --- /dev/null +++ b/packages/medusa/src/utils/build-query.ts @@ -0,0 +1,112 @@ +import { FindConfig, Writable } from "../types/common" +import { FindOperator, In, Raw } from "typeorm" + +type Selector = { [key in keyof TEntity]?: unknown } +/** +* Used to build TypeORM queries. +* @param selector The selector +* @param config The config +* @return The QueryBuilderConfig +*/ +export function buildQuery( + selector: Selector, + config: FindConfig = {} +): FindConfig & { + where: Partial> + withDeleted?: boolean +} { + const build = ( + obj: Record + ): Partial> => { + return Object.entries(obj).reduce((acc, [key, value]: any) => { + // Undefined values indicate that they have no significance to the query. + // If the query is looking for rows where a column is not set it should use null instead of undefined + if (typeof value === "undefined") { + return acc + } + + const subquery: { + operator: "<" | ">" | "<=" | ">=" + value: unknown + }[] = [] + + switch (true) { + case value instanceof FindOperator: + acc[key] = value + break + case Array.isArray(value): + acc[key] = In([...(value as unknown[])]) + break + case value !== null && typeof value === "object": + Object.entries(value).map(([modifier, val]) => { + switch (modifier) { + case "lt": + subquery.push({ operator: "<", value: val }) + break + case "gt": + subquery.push({ operator: ">", value: val }) + break + case "lte": + subquery.push({ operator: "<=", value: val }) + break + case "gte": + subquery.push({ operator: ">=", value: val }) + break + default: + acc[key] = value + break + } + }) + + if (subquery.length) { + acc[key] = Raw( + (a) => + subquery + .map((s, index) => `${a} ${s.operator} :${index}`) + .join(" AND "), + subquery.map((s) => s.value) + ) + } + break + default: + acc[key] = value + break + } + + return acc + }, {} as Partial>) + } + + const query: FindConfig & { + where: Partial> + withDeleted?: boolean + } = { + where: build(selector), + } + + if ("deleted_at" in selector) { + query.withDeleted = true + } + + if ("skip" in config) { + query.skip = config.skip + } + + if ("take" in config) { + query.take = config.take + } + + if ("relations" in config) { + query.relations = config.relations + } + + if ("select" in config) { + query.select = config.select + } + + if ("order" in config) { + query.order = config.order + } + + return query +} \ No newline at end of file diff --git a/packages/medusa/src/utils/set-metadata.ts b/packages/medusa/src/utils/set-metadata.ts new file mode 100644 index 0000000000..38399e1dbb --- /dev/null +++ b/packages/medusa/src/utils/set-metadata.ts @@ -0,0 +1,29 @@ +import { MedusaError } from "medusa-core-utils/dist" + +/** +* Dedicated method to set metadata. +* @param obj - the entity to apply metadata to. +* @param metadata - the metadata to set +* @return resolves to the updated result. +*/ +export function setMetadata_( + obj: { metadata: Record }, + metadata: Record +): Record { + const existing = obj.metadata || {} + const newData = {} + for (const [key, value] of Object.entries(metadata)) { + if (typeof key !== "string") { + throw new MedusaError( + MedusaError.Types.INVALID_ARGUMENT, + "Key type is invalid. Metadata keys must be strings" + ) + } + newData[key] = value + } + + return { + ...existing, + ...newData, + } +} \ No newline at end of file diff --git a/packages/medusa/src/utils/validate-id.ts b/packages/medusa/src/utils/validate-id.ts new file mode 100644 index 0000000000..2c616fad48 --- /dev/null +++ b/packages/medusa/src/utils/validate-id.ts @@ -0,0 +1,41 @@ +/** +* Confirms whether a given raw id is valid. Fails if the provided +* id is null or undefined. The validate function takes an optional config +* param, to support checking id prefix and length. +* @param rawId - the id to validate. +* @param config - optional config +* @returns the rawId given that nothing failed +*/ +import { MedusaError } from "medusa-core-utils/dist" + +export function validateId_( + rawId: string, + config: { prefix?: string; length?: number } = {} +): string { + const { prefix, length } = config + if (!rawId) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Failed to validate id: ${rawId}` + ) + } + + if (prefix || length) { + const [pre, rand] = rawId.split("_") + if (prefix && pre !== prefix) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `The provided id: ${rawId} does not adhere to prefix constraint: ${prefix}` + ) + } + + if (length && length !== rand.length) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `The provided id: ${rawId} does not adhere to length constraint: ${length}` + ) + } + } + + return rawId +} \ No newline at end of file From b90291b18d438e3bb7f5239f4470eec7b3c5ff03 Mon Sep 17 00:00:00 2001 From: adrien2p Date: Mon, 18 Apr 2022 16:23:11 +0200 Subject: [PATCH 05/16] feat(medusa): Improve buildQuery as well as refactor the cart service as an example --- packages/medusa/src/services/cart.ts | 127 +++++++++++----------- packages/medusa/src/types/cart.ts | 2 +- packages/medusa/src/utils/build-query.ts | 19 +++- packages/medusa/src/utils/index.ts | 3 + packages/medusa/src/utils/set-metadata.ts | 2 +- packages/medusa/src/utils/validate-id.ts | 2 +- 6 files changed, 86 insertions(+), 69 deletions(-) create mode 100644 packages/medusa/src/utils/index.ts diff --git a/packages/medusa/src/services/cart.ts b/packages/medusa/src/services/cart.ts index 6e5d0c4284..c7baf16699 100644 --- a/packages/medusa/src/services/cart.ts +++ b/packages/medusa/src/services/cart.ts @@ -1,6 +1,5 @@ import _ from "lodash" import { MedusaError, Validator } from "medusa-core-utils" -import { BaseService } from "medusa-interfaces" import { DeepPartial, EntityManager, In } from "typeorm" import { IPriceSelectionStrategy } from "../interfaces/price-selection-strategy" import { Address } from "../models/address" @@ -37,6 +36,8 @@ import InventoryService from "./inventory" import CustomShippingOptionService from "./custom-shipping-option" import LineItemAdjustmentService from "./line-item-adjustment" import { LineItemRepository } from "../repositories/line-item" +import { TransactionBaseService } from "../interfaces" +import { buildQuery, setMetadata, validateId } from "../utils" type InjectedDependencies = { manager: EntityManager @@ -70,14 +71,16 @@ type TotalsConfig = { /* Provides layer to manipulate carts. * @implements BaseService */ -class CartService extends BaseService { - static Events = { +class CartService extends TransactionBaseService { + static readonly Events = { CUSTOMER_UPDATED: "cart.customer_updated", CREATED: "cart.created", UPDATED: "cart.updated", } - protected readonly manager_: EntityManager + protected manager_: EntityManager + protected transactionManager_: EntityManager | undefined + protected readonly shippingMethodRepository_: typeof ShippingMethodRepository protected readonly cartRepository_: typeof CartRepository protected readonly addressRepository_: typeof AddressRepository @@ -124,7 +127,30 @@ class CartService extends BaseService { lineItemAdjustmentService, priceSelectionStrategy, }: InjectedDependencies) { - super() + super({ + manager, + cartRepository, + shippingMethodRepository, + lineItemRepository, + eventBusService, + paymentProviderService, + productService, + productVariantService, + taxProviderService, + regionService, + lineItemService, + shippingOptionService, + customerService, + discountService, + giftCardService, + totalsService, + addressRepository, + paymentSessionRepository, + inventoryService, + customShippingOptionService, + lineItemAdjustmentService, + priceSelectionStrategy, + }) this.manager_ = manager this.shippingMethodRepository_ = shippingMethodRepository @@ -150,41 +176,6 @@ class CartService extends BaseService { this.priceSelectionStrategy_ = priceSelectionStrategy } - withTransaction(transactionManager: EntityManager): CartService { - if (!transactionManager) { - return this - } - - const cloned = new CartService({ - manager: transactionManager, - taxProviderService: this.taxProviderService_, - cartRepository: this.cartRepository_, - lineItemRepository: this.lineItemRepository_, - eventBusService: this.eventBus_, - paymentProviderService: this.paymentProviderService_, - paymentSessionRepository: this.paymentSessionRepository_, - shippingMethodRepository: this.shippingMethodRepository_, - productService: this.productService_, - productVariantService: this.productVariantService_, - regionService: this.regionService_, - lineItemService: this.lineItemService_, - shippingOptionService: this.shippingOptionService_, - customerService: this.customerService_, - discountService: this.discountService_, - totalsService: this.totalsService_, - addressRepository: this.addressRepository_, - giftCardService: this.giftCardService_, - inventoryService: this.inventoryService_, - customShippingOptionService: this.customShippingOptionService_, - lineItemAdjustmentService: this.lineItemAdjustmentService_, - priceSelectionStrategy: this.priceSelectionStrategy_, - }) - - cloned.transactionManager_ = transactionManager - - return cloned - } - protected transformQueryForTotals_( config: FindConfig ): FindConfig & { totalsToSelect: TotalField[] } { @@ -292,7 +283,7 @@ class CartService extends BaseService { this.cartRepository_ ) - const query = this.buildQuery_(selector, config) + const query = buildQuery(selector, config) return await cartRepo.find(query) } ) @@ -315,12 +306,12 @@ class CartService extends BaseService { const cartRepo = transactionManager.getCustomRepository( this.cartRepository_ ) - const validatedId = this.validateId_(cartId) + const validatedId = validateId(cartId) const { select, relations, totalsToSelect } = this.transformQueryForTotals_(options) - const query = this.buildQuery_( + const query = buildQuery( { id: validatedId }, { ...options, select, relations } ) @@ -714,16 +705,20 @@ class CartService extends BaseService { * @param shouldAdd - flag to indicate, if we should add or remove * @return void */ - async adjustFreeShipping_(cart: Cart, shouldAdd: boolean): Promise { + protected async adjustFreeShipping_( + cart: Cart, + shouldAdd: boolean + ): Promise { + const transactionManager = this.transactionManager_ ?? this.manager_ + if (cart.shipping_methods?.length) { - const shippingMethodRepository = - this.transactionManager_.getCustomRepository( - this.shippingMethodRepository_ - ) + const shippingMethodRepository = transactionManager.getCustomRepository( + this.shippingMethodRepository_ + ) // if any free shipping discounts, we ensure to update shipping method amount if (shouldAdd) { - return shippingMethodRepository.update( + await shippingMethodRepository.update( { id: In( cart.shipping_methods.map((shippingMethod) => shippingMethod.id) @@ -855,8 +850,8 @@ class CartService extends BaseService { ) } - if ("metadata" in data) { - cart.metadata = this.setMetadata_(cart, data.metadata) + if (data?.metadata) { + cart.metadata = setMetadata(cart, data.metadata) } if ("context" in data) { @@ -1352,7 +1347,7 @@ class CartService extends BaseService { * @param cartOrCartId - the id of the cart to set payment session for * @return the result of the update operation. */ - async setPaymentSessions(cartOrCartId: Cart | string): Promise { + async setPaymentSessions(cartOrCartId: Cart | string): Promise { return await this.atomicPhase_( async (transactionManager: EntityManager) => { const psRepo = transactionManager.getCustomRepository( @@ -1679,6 +1674,8 @@ class CartService extends BaseService { regionId?: string, customer_id?: string ): Promise { + const transactionManager = this.transactionManager_ ?? this.manager_ + // If the cart contains items, we update the price of the items // to match the updated region or customer id (keeping the old // value if it exists) @@ -1693,7 +1690,7 @@ class CartService extends BaseService { await Promise.all( cart.items.map(async (item) => { const availablePrice = await this.priceSelectionStrategy_ - .withTransaction(this.transactionManager_) + .withTransaction(transactionManager) .calculateVariantPrice(item.variant_id, { region_id: region.id, currency_code: region.currency_code, @@ -1708,14 +1705,14 @@ class CartService extends BaseService { availablePrice.calculatedPrice !== null ) { return this.lineItemService_ - .withTransaction(this.transactionManager_) + .withTransaction(transactionManager) .update(item.id, { has_shipping: false, unit_price: availablePrice.calculatedPrice, }) } else { await this.lineItemService_ - .withTransaction(this.transactionManager_) + .withTransaction(transactionManager) .delete(item.id) return } @@ -1737,6 +1734,8 @@ class CartService extends BaseService { regionId: string, countryCode: string | null ): Promise { + const transactionManager = this.transactionManager_ ?? this.manager_ + if (cart.completed_at || cart.payment_authorized_at) { throw new MedusaError( MedusaError.Types.NOT_ALLOWED, @@ -1750,7 +1749,7 @@ class CartService extends BaseService { .retrieve(regionId, { relations: ["countries"], }) - const addrRepo = this.transactionManager_.getCustomRepository( + const addrRepo = transactionManager.getCustomRepository( this.addressRepository_ ) cart.region = region @@ -1828,7 +1827,7 @@ class CartService extends BaseService { // new shipping method if (cart.shipping_methods && cart.shipping_methods.length) { await this.shippingOptionService_ - .withTransaction(this.transactionManager_) + .withTransaction(transactionManager) .deleteShippingMethods(cart.shipping_methods) } @@ -1841,7 +1840,7 @@ class CartService extends BaseService { cart.gift_cards = [] if (cart.payment_sessions && cart.payment_sessions.length) { - const paymentSessionRepo = this.transactionManager_.getCustomRepository( + const paymentSessionRepo = transactionManager.getCustomRepository( this.paymentSessionRepository_ ) await paymentSessionRepo.delete({ @@ -1859,7 +1858,7 @@ class CartService extends BaseService { * @param cartId - the id of the cart to delete * @return the deleted cart or undefined if the cart was not found. */ - async delete(cartId: string): Promise { + async delete(cartId: string): Promise { return await this.atomicPhase_( async (transactionManager: EntityManager) => { const cart = await this.retrieve(cartId, { @@ -1913,7 +1912,7 @@ class CartService extends BaseService { this.cartRepository_ ) - const validatedId = this.validateId_(cartId) + const validatedId = validateId(cartId) if (typeof key !== "string") { throw new MedusaError( MedusaError.Types.INVALID_ARGUMENT, @@ -1972,20 +1971,22 @@ class CartService extends BaseService { } protected async refreshAdjustments_(cart: Cart): Promise { + const transactionManager = this.transactionManager_ ?? this.manager_ + const nonReturnLineIDs = cart.items .filter((item) => !item.is_return) .map((i) => i.id) // delete all old non return line item adjustments await this.lineItemAdjustmentService_ - .withTransaction(this.transactionManager_) + .withTransaction(transactionManager) .delete({ item_id: nonReturnLineIDs, }) // potentially create/update line item adjustments await this.lineItemAdjustmentService_ - .withTransaction(this.transactionManager_) + .withTransaction(transactionManager) .createAdjustments(cart) } @@ -2001,7 +2002,7 @@ class CartService extends BaseService { const cartRepo = transactionManager.getCustomRepository( this.cartRepository_ ) - const validatedId = this.validateId_(cartId) + const validatedId = validateId(cartId) if (typeof key !== "string") { throw new MedusaError( diff --git a/packages/medusa/src/types/cart.ts b/packages/medusa/src/types/cart.ts index 249c0c9af0..dfba20b82f 100644 --- a/packages/medusa/src/types/cart.ts +++ b/packages/medusa/src/types/cart.ts @@ -71,5 +71,5 @@ export type CartUpdateProps = { discounts?: Discount[] customer_id?: string context?: object - metadata?: object + metadata?: Record } diff --git a/packages/medusa/src/utils/build-query.ts b/packages/medusa/src/utils/build-query.ts index 103af54379..f116404729 100644 --- a/packages/medusa/src/utils/build-query.ts +++ b/packages/medusa/src/utils/build-query.ts @@ -1,7 +1,20 @@ -import { FindConfig, Writable } from "../types/common" +import { + DateComparisonOperator, + FindConfig, + NumericalComparisonOperator, + StringComparisonOperator, + Writable, +} from "../types/common" import { FindOperator, In, Raw } from "typeorm" -type Selector = { [key in keyof TEntity]?: unknown } +type Selector = { + [key in keyof TEntity]?: TEntity[key] + | TEntity[key][] + | DateComparisonOperator + | StringComparisonOperator + | NumericalComparisonOperator +} + /** * Used to build TypeORM queries. * @param selector The selector @@ -16,7 +29,7 @@ export function buildQuery( withDeleted?: boolean } { const build = ( - obj: Record + obj: Selector ): Partial> => { return Object.entries(obj).reduce((acc, [key, value]: any) => { // Undefined values indicate that they have no significance to the query. diff --git a/packages/medusa/src/utils/index.ts b/packages/medusa/src/utils/index.ts new file mode 100644 index 0000000000..5d7ec9889f --- /dev/null +++ b/packages/medusa/src/utils/index.ts @@ -0,0 +1,3 @@ +export * from './build-query' +export * from './set-metadata' +export * from './validate-id' \ No newline at end of file diff --git a/packages/medusa/src/utils/set-metadata.ts b/packages/medusa/src/utils/set-metadata.ts index 38399e1dbb..d27cddc5ed 100644 --- a/packages/medusa/src/utils/set-metadata.ts +++ b/packages/medusa/src/utils/set-metadata.ts @@ -6,7 +6,7 @@ import { MedusaError } from "medusa-core-utils/dist" * @param metadata - the metadata to set * @return resolves to the updated result. */ -export function setMetadata_( +export function setMetadata( obj: { metadata: Record }, metadata: Record ): Record { diff --git a/packages/medusa/src/utils/validate-id.ts b/packages/medusa/src/utils/validate-id.ts index 2c616fad48..da6102e69a 100644 --- a/packages/medusa/src/utils/validate-id.ts +++ b/packages/medusa/src/utils/validate-id.ts @@ -8,7 +8,7 @@ */ import { MedusaError } from "medusa-core-utils/dist" -export function validateId_( +export function validateId( rawId: string, config: { prefix?: string; length?: number } = {} ): string { From ff9ff214873a14b3b0abc419f53838b8d728eea9 Mon Sep 17 00:00:00 2001 From: adrien2p Date: Mon, 2 May 2022 13:38:15 +0200 Subject: [PATCH 06/16] feat(medusa): Update TransactionBaseService methods visibility --- packages/medusa/src/interfaces/transaction-base-service.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/medusa/src/interfaces/transaction-base-service.ts b/packages/medusa/src/interfaces/transaction-base-service.ts index cbddd4b6f5..96d472bb23 100644 --- a/packages/medusa/src/interfaces/transaction-base-service.ts +++ b/packages/medusa/src/interfaces/transaction-base-service.ts @@ -31,7 +31,7 @@ export abstract class TransactionBaseService< return cloned as TChild } - shouldRetryTransaction( + protected shouldRetryTransaction_( err: { code: string } | Record ): boolean { if (!(err as { code: string })?.code) { @@ -50,7 +50,7 @@ export abstract class TransactionBaseService< * @param maybeErrorHandlerOrDontFail Potential error handler * @return the result of the transactional work */ - async atomicPhase_( + protected async atomicPhase_( work: (transactionManager: EntityManager) => Promise, isolationOrErrorHandler?: | IsolationLevel @@ -118,7 +118,7 @@ export abstract class TransactionBaseService< ) return result } catch (error) { - if (this.shouldRetryTransaction(error)) { + if (this.shouldRetryTransaction_(error)) { return this.manager_.transaction( isolation as IsolationLevel, (m): Promise => doWork(m) From 978ee98dc3f406cbacde1a1e91d26782c61b7f86 Mon Sep 17 00:00:00 2001 From: adrien2p Date: Mon, 2 May 2022 18:06:09 +0200 Subject: [PATCH 07/16] refactor(medusa): Improve proposal in cartService use arguments to pass to the super --- packages/medusa/src/services/cart.ts | 26 ++------------------------ 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/packages/medusa/src/services/cart.ts b/packages/medusa/src/services/cart.ts index c7baf16699..69973d4371 100644 --- a/packages/medusa/src/services/cart.ts +++ b/packages/medusa/src/services/cart.ts @@ -127,30 +127,8 @@ class CartService extends TransactionBaseService { lineItemAdjustmentService, priceSelectionStrategy, }: InjectedDependencies) { - super({ - manager, - cartRepository, - shippingMethodRepository, - lineItemRepository, - eventBusService, - paymentProviderService, - productService, - productVariantService, - taxProviderService, - regionService, - lineItemService, - shippingOptionService, - customerService, - discountService, - giftCardService, - totalsService, - addressRepository, - paymentSessionRepository, - inventoryService, - customShippingOptionService, - lineItemAdjustmentService, - priceSelectionStrategy, - }) + // eslint-disable-next-line prefer-rest-params + super(arguments[0]) this.manager_ = manager this.shippingMethodRepository_ = shippingMethodRepository From f7ef3aac36ed3053c8880522cc95bb046c03ebda Mon Sep 17 00:00:00 2001 From: adrien2p Date: Tue, 3 May 2022 09:22:00 +0200 Subject: [PATCH 08/16] feat(medusa): Rename base-service.spec to transaction-base-service.spec --- .../{base-service.spec.ts => transaction-base-service.spec.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/medusa/src/interfaces/__tests__/{base-service.spec.ts => transaction-base-service.spec.ts} (100%) diff --git a/packages/medusa/src/interfaces/__tests__/base-service.spec.ts b/packages/medusa/src/interfaces/__tests__/transaction-base-service.spec.ts similarity index 100% rename from packages/medusa/src/interfaces/__tests__/base-service.spec.ts rename to packages/medusa/src/interfaces/__tests__/transaction-base-service.spec.ts From 3ad91741b2e729638902d7c62ac99fdfe9a40b91 Mon Sep 17 00:00:00 2001 From: adrien2p Date: Wed, 4 May 2022 16:01:04 +0200 Subject: [PATCH 09/16] feat(medusa): Move some typings into the common types --- packages/medusa/src/types/common.ts | 14 ++++++++++++++ packages/medusa/src/utils/build-query.ts | 18 +++--------------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/packages/medusa/src/types/common.ts b/packages/medusa/src/types/common.ts index 9837a0de1e..f094dc73b0 100644 --- a/packages/medusa/src/types/common.ts +++ b/packages/medusa/src/types/common.ts @@ -9,6 +9,20 @@ export type PartialPick = { export type Writable = { -readonly [key in keyof T]: T[key] } +export type ExtendedFindConfig = FindConfig & { + where: Partial> + withDeleted?: boolean +} + +export type Selector = { + [key in keyof TEntity]?: + | TEntity[key] + | TEntity[key][] + | DateComparisonOperator + | StringComparisonOperator + | NumericalComparisonOperator +} + export type TotalField = | "shipping_total" | "discount_total" diff --git a/packages/medusa/src/utils/build-query.ts b/packages/medusa/src/utils/build-query.ts index f116404729..f61123741d 100644 --- a/packages/medusa/src/utils/build-query.ts +++ b/packages/medusa/src/utils/build-query.ts @@ -1,20 +1,11 @@ import { - DateComparisonOperator, + ExtendedFindConfig, FindConfig, - NumericalComparisonOperator, - StringComparisonOperator, + Selector, Writable, } from "../types/common" import { FindOperator, In, Raw } from "typeorm" -type Selector = { - [key in keyof TEntity]?: TEntity[key] - | TEntity[key][] - | DateComparisonOperator - | StringComparisonOperator - | NumericalComparisonOperator -} - /** * Used to build TypeORM queries. * @param selector The selector @@ -24,10 +15,7 @@ type Selector = { export function buildQuery( selector: Selector, config: FindConfig = {} -): FindConfig & { - where: Partial> - withDeleted?: boolean -} { +): ExtendedFindConfig { const build = ( obj: Selector ): Partial> => { From 3c75a657924938f2aeda8cca7ad84a1971629ee0 Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Thu, 5 May 2022 07:56:34 +0200 Subject: [PATCH 10/16] fix(medusa): MoneyAmountRepository#findManyForVariantInRegion sql statement for constraint related to price_list (#1462) --- .../medusa/src/repositories/money-amount.ts | 26 +++++++------------ 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/packages/medusa/src/repositories/money-amount.ts b/packages/medusa/src/repositories/money-amount.ts index 63713651b7..496562a962 100644 --- a/packages/medusa/src/repositories/money-amount.ts +++ b/packages/medusa/src/repositories/money-amount.ts @@ -126,21 +126,17 @@ export class MoneyAmountRepository extends Repository { const date = new Date() const qb = this.createQueryBuilder("ma") - .leftJoinAndSelect( - "ma.price_list", - "price_list", - "ma.price_list_id = price_list.id " - ) + .leftJoinAndSelect("ma.price_list", "price_list") .where({ variant_id: variant_id }) .andWhere("(ma.price_list_id is null or price_list.status = 'active')") .andWhere( - "(price_list is null or price_list.ends_at is null OR price_list.ends_at > :date) ", + "(price_list.ends_at is null OR price_list.ends_at > :date)", { date: date.toUTCString(), } ) .andWhere( - "(price_list is null or price_list.starts_at is null OR price_list.starts_at < :date)", + "(price_list.starts_at is null OR price_list.starts_at < :date)", { date: date.toUTCString(), } @@ -155,23 +151,19 @@ export class MoneyAmountRepository extends Repository { ) ) } else if (!customer_id && !include_discount_prices) { - qb.andWhere("price_list IS null") + qb.andWhere("price_list.id IS null") } if (customer_id) { qb.leftJoin("price_list.customer_groups", "cgroup") - .leftJoin( - "customer_group_customers", - "cgc", - "cgc.customer_group_id = cgroup.id" - ) - .andWhere("(cgc is null OR cgc.customer_id = :customer_id)", { + .leftJoin("customer_group_customers", "cgc", "cgc.customer_group_id = cgroup.id") + .andWhere("(cgc.customer_group_id is null OR cgc.customer_id = :customer_id)", { customer_id, }) } else { - qb.leftJoin("price_list.customer_groups", "cgroup").andWhere( - "cgroup.id is null" - ) + qb + .leftJoin("price_list.customer_groups", "cgroup") + .andWhere("cgroup.id is null") } return await qb.getManyAndCount() } From e2d08316dd03946453c5bf39c78a141d9fd57d3c Mon Sep 17 00:00:00 2001 From: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Date: Sun, 8 May 2022 18:03:29 +0700 Subject: [PATCH 11/16] fix: Use correct product price when fetching product for pricelist (#1416) --- .../api/__tests__/admin/price-list.js | 60 +++++++++++-- .../factories/simple-price-list-factory.ts | 24 ++++++ .../price-lists/list-price-list-products.ts | 85 +++++++++++++++---- .../medusa/src/repositories/money-amount.ts | 18 ++++ packages/medusa/src/services/price-list.ts | 51 +++++++++++ 5 files changed, 216 insertions(+), 22 deletions(-) diff --git a/integration-tests/api/__tests__/admin/price-list.js b/integration-tests/api/__tests__/admin/price-list.js index 0c03141bf2..e7aae7f444 100644 --- a/integration-tests/api/__tests__/admin/price-list.js +++ b/integration-tests/api/__tests__/admin/price-list.js @@ -794,9 +794,17 @@ describe("/admin/price-lists", () => { await simplePriceListFactory(dbConnection, { id: "test-list", + customer_groups: ["test-group"], prices: [ - { variant_id: "test-variant-1", currency_code: "usd", amount: 100 }, - { variant_id: "test-variant-4", currency_code: "usd", amount: 100 }, + { variant_id: "test-variant-1", currency_code: "usd", amount: 150 }, + { variant_id: "test-variant-4", currency_code: "usd", amount: 150 }, + ], + }) + await simplePriceListFactory(dbConnection, { + id: "test-list-2", + prices: [ + { variant_id: "test-variant-1", currency_code: "usd", amount: 200 }, + { variant_id: "test-variant-4", currency_code: "usd", amount: 200 }, ], }) } catch (err) { @@ -810,7 +818,7 @@ describe("/admin/price-lists", () => { await db.teardown() }) - it("lists only product 1, 2", async () => { + it("lists only product 1, 2 with price list prices", async () => { const api = useApi() const response = await api @@ -826,8 +834,50 @@ describe("/admin/price-lists", () => { expect(response.status).toEqual(200) expect(response.data.count).toEqual(2) expect(response.data.products).toEqual([ - expect.objectContaining({ id: "test-prod-1" }), - expect.objectContaining({ id: "test-prod-2" }), + expect.objectContaining({ + id: "test-prod-1", + variants: [ + expect.objectContaining({ + id: "test-variant-1", + prices: [ + expect.objectContaining({ currency_code: "usd", amount: 100 }), + expect.objectContaining({ + currency_code: "usd", + amount: 150, + price_list_id: "test-list", + }), + ], + }), + expect.objectContaining({ + id: "test-variant-2", + prices: [ + expect.objectContaining({ currency_code: "usd", amount: 100 }), + ], + }), + ], + }), + expect.objectContaining({ + id: "test-prod-2", + variants: [ + expect.objectContaining({ + id: "test-variant-3", + prices: [ + expect.objectContaining({ currency_code: "usd", amount: 100 }), + ], + }), + expect.objectContaining({ + id: "test-variant-4", + prices: [ + expect.objectContaining({ currency_code: "usd", amount: 100 }), + expect.objectContaining({ + currency_code: "usd", + amount: 150, + price_list_id: "test-list", + }), + ], + }), + ], + }), ]) }) diff --git a/integration-tests/api/factories/simple-price-list-factory.ts b/integration-tests/api/factories/simple-price-list-factory.ts index 439b35deec..a20380d3b0 100644 --- a/integration-tests/api/factories/simple-price-list-factory.ts +++ b/integration-tests/api/factories/simple-price-list-factory.ts @@ -3,6 +3,7 @@ import { MoneyAmount, PriceListType, PriceListStatus, + CustomerGroup, } from "@medusajs/medusa" import faker from "faker" import { Connection } from "typeorm" @@ -38,6 +39,28 @@ export const simplePriceListFactory = async ( const manager = connection.manager const listId = data.id || `simple-price-list-${Math.random() * 1000}` + + let customerGroups = [] + if (typeof data.customer_groups !== "undefined") { + await manager + .createQueryBuilder() + .insert() + .into(CustomerGroup) + .values( + data.customer_groups.map((group) => ({ + id: group, + name: faker.company.companyName(), + })) + ) + .orIgnore() + .execute() + + customerGroups = await manager.findByIds( + CustomerGroup, + data.customer_groups + ) + } + const toCreate = { id: listId, name: data.name || faker.commerce.productName(), @@ -46,6 +69,7 @@ export const simplePriceListFactory = async ( type: data.type || PriceListType.OVERRIDE, starts_at: data.starts_at || null, ends_at: data.ends_at || null, + customer_groups: customerGroups, } const toSave = manager.create(PriceList, toCreate) diff --git a/packages/medusa/src/api/routes/admin/price-lists/list-price-list-products.ts b/packages/medusa/src/api/routes/admin/price-lists/list-price-list-products.ts index 5cbe0abba6..21a3f73b66 100644 --- a/packages/medusa/src/api/routes/admin/price-lists/list-price-list-products.ts +++ b/packages/medusa/src/api/routes/admin/price-lists/list-price-list-products.ts @@ -1,5 +1,5 @@ import { Type } from "class-transformer" -import { omit } from "lodash" +import { omit, pickBy } from "lodash" import { IsArray, IsBoolean, @@ -19,6 +19,9 @@ import { defaultAdminProductRelations, } from "../products" import listAndCount from "../../../../controllers/products/admin-list-products" +import { MedusaError } from "medusa-core-utils" +import { getListConfig } from "../../../../utils/get-query-config" +import PriceListService from "../../../../services/price-list" /** * @oas [get] /price-lists/:id/products @@ -78,7 +81,7 @@ export default async (req, res) => { req.query.price_list_id = [id] - const filterableFields: FilterableProductProps = omit(req.query, [ + const query: FilterableProductProps = omit(req.query, [ "limit", "offset", "expand", @@ -86,23 +89,71 @@ export default async (req, res) => { "order", ]) - const result = await listAndCount( - req.scope, - filterableFields, - {}, - { - limit: validatedParams.limit ?? 50, - offset: validatedParams.offset ?? 0, - expand: validatedParams.expand, - fields: validatedParams.fields, - order: validatedParams.order, - allowedFields: allowedAdminProductFields, - defaultFields: defaultAdminProductFields as (keyof Product)[], - defaultRelations: defaultAdminProductRelations, - } + const limit = validatedParams.limit ?? 50 + const offset = validatedParams.offset ?? 0 + const expand = validatedParams.expand + const fields = validatedParams.fields + const order = validatedParams.order + const allowedFields = allowedAdminProductFields + const defaultFields = defaultAdminProductFields as (keyof Product)[] + const defaultRelations = defaultAdminProductRelations.filter( + (r) => r !== "variants.prices" ) - res.json(result) + const priceListService: PriceListService = + req.scope.resolve("priceListService") + + let includeFields: (keyof Product)[] | undefined + if (fields) { + includeFields = fields.split(",") as (keyof Product)[] + } + + let expandFields: string[] | undefined + if (expand) { + expandFields = expand.split(",") + } + + let orderBy: { [k: symbol]: "DESC" | "ASC" } | undefined + if (typeof order !== "undefined") { + let orderField = order + if (order.startsWith("-")) { + const [, field] = order.split("-") + orderField = field + orderBy = { [field]: "DESC" } + } else { + orderBy = { [order]: "ASC" } + } + + if (!(allowedFields || []).includes(orderField)) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Order field must be a valid product field" + ) + } + } + + const listConfig = getListConfig( + defaultFields ?? [], + defaultRelations ?? [], + includeFields, + expandFields, + limit, + offset, + orderBy + ) + + const [products, count] = await priceListService.listProducts( + id, + pickBy(query, (val) => typeof val !== "undefined"), + listConfig + ) + + res.json({ + products, + count, + offset, + limit, + }) } enum ProductStatus { diff --git a/packages/medusa/src/repositories/money-amount.ts b/packages/medusa/src/repositories/money-amount.ts index 496562a962..716db232cf 100644 --- a/packages/medusa/src/repositories/money-amount.ts +++ b/packages/medusa/src/repositories/money-amount.ts @@ -116,6 +116,24 @@ export class MoneyAmountRepository extends Repository { .execute() } + public async findManyForVariantInPriceList( + variant_id: string, + price_list_id: string + ): Promise<[MoneyAmount[], number]> { + const qb = this.createQueryBuilder("ma") + .leftJoinAndSelect("ma.price_list", "price_list") + .where("ma.variant_id = :variant_id", { variant_id }) + .andWhere( + new Brackets((qb) => { + qb.where("ma.price_list_id = :price_list_id", { + price_list_id, + }).orWhere("ma.price_list_id IS NULL") + }) + ) + + return await qb.getManyAndCount() + } + public async findManyForVariantInRegion( variant_id: string, region_id?: string, diff --git a/packages/medusa/src/services/price-list.ts b/packages/medusa/src/services/price-list.ts index 0312b6e81e..6c96e8508e 100644 --- a/packages/medusa/src/services/price-list.ts +++ b/packages/medusa/src/services/price-list.ts @@ -2,6 +2,7 @@ import { MedusaError } from "medusa-core-utils" import { BaseService } from "medusa-interfaces" import { EntityManager } from "typeorm" import { CustomerGroupService } from "." +import { Product } from "../models" import { CustomerGroup } from "../models/customer-group" import { PriceList } from "../models/price-list" import { MoneyAmountRepository } from "../repositories/money-amount" @@ -14,10 +15,12 @@ import { UpdatePriceListInput, } from "../types/price-list" import { formatException } from "../utils/exception-formatter" +import ProductService from "./product" type PriceListConstructorProps = { manager: EntityManager customerGroupService: CustomerGroupService + productService: ProductService priceListRepository: typeof PriceListRepository moneyAmountRepository: typeof MoneyAmountRepository } @@ -29,18 +32,21 @@ type PriceListConstructorProps = { class PriceListService extends BaseService { private manager_: EntityManager private customerGroupService_: CustomerGroupService + private productService_: ProductService private priceListRepo_: typeof PriceListRepository private moneyAmountRepo_: typeof MoneyAmountRepository constructor({ manager, customerGroupService, + productService, priceListRepository, moneyAmountRepository, }: PriceListConstructorProps) { super() this.manager_ = manager this.customerGroupService_ = customerGroupService + this.productService_ = productService this.priceListRepo_ = priceListRepository this.moneyAmountRepo_ = moneyAmountRepository } @@ -53,6 +59,7 @@ class PriceListService extends BaseService { const cloned = new PriceListService({ manager: transactionManager, customerGroupService: this.customerGroupService_, + productService: this.productService_, priceListRepository: this.priceListRepo_, moneyAmountRepository: this.moneyAmountRepo_, }) @@ -276,6 +283,50 @@ class PriceListService extends BaseService { await priceListRepo.save(priceList) } + + async listProducts( + priceListId: string, + selector = {}, + config: FindConfig = { + relations: [], + skip: 0, + take: 20, + } + ): Promise<[Product[], number]> { + return await this.atomicPhase_(async (manager: EntityManager) => { + const [products, count] = await this.productService_.listAndCount( + selector, + config + ) + + const moneyAmountRepo = manager.getCustomRepository(this.moneyAmountRepo_) + + const productsWithPrices = await Promise.all( + products.map(async (p) => { + if (p.variants?.length) { + p.variants = await Promise.all( + p.variants.map(async (v) => { + const [prices] = + await moneyAmountRepo.findManyForVariantInPriceList( + v.id, + priceListId + ) + + return { + ...v, + prices, + } + }) + ) + } + + return p + }) + ) + + return [productsWithPrices, count] + }) + } } export default PriceListService From e7cb76ab6e13fe756e090fd1a0a3ff645c30c69a Mon Sep 17 00:00:00 2001 From: Kasper Fabricius Kristensen <45367945+kasperkristensen@users.noreply.github.com> Date: Sun, 8 May 2022 16:12:31 +0200 Subject: [PATCH 12/16] fix: Cascade remove prices + option values on variant and product delete (#1465) --- .../api/__tests__/admin/product.js | 173 +++++++++++++++++- .../medusa/src/services/product-variant.ts | 2 +- packages/medusa/src/services/product.js | 2 +- 3 files changed, 174 insertions(+), 3 deletions(-) diff --git a/integration-tests/api/__tests__/admin/product.js b/integration-tests/api/__tests__/admin/product.js index df71a7530f..831a12296d 100644 --- a/integration-tests/api/__tests__/admin/product.js +++ b/integration-tests/api/__tests__/admin/product.js @@ -6,7 +6,7 @@ const { initDb, useDb } = require("../../../helpers/use-db") const adminSeeder = require("../../helpers/admin-seeder") const productSeeder = require("../../helpers/product-seeder") -const { ProductVariant } = require("@medusajs/medusa") +const { ProductVariant, ProductOptionValue, MoneyAmount } = require("@medusajs/medusa") const priceListSeeder = require("../../helpers/price-list-seeder") jest.setTimeout(50000) @@ -1737,6 +1737,177 @@ describe("/admin/products", () => { expect(variant).toEqual(undefined) }) + it("successfully deletes a product variant and its associated option values", async () => { + const api = useApi() + + // Validate that the option value exists + const optValPre = await dbConnection.manager.findOne(ProductOptionValue, { + variant_id: "test-variant_2", + }) + + expect(optValPre).not.toEqual(undefined) + + // Soft delete the variant + const response = await api.delete( + "/admin/products/test-product/variants/test-variant_2", + { + headers: { + Authorization: "Bearer test_token", + }, + } + ) + + expect(response.status).toEqual(200) + + // Validate that the option value was deleted + const optValPost = await dbConnection.manager.findOne( + ProductOptionValue, + { + variant_id: "test-variant_2", + } + ) + + expect(optValPost).toEqual(undefined) + + // Validate that the option still exists in the DB with deleted_at + const optValDeleted = await dbConnection.manager.findOne(ProductOptionValue, { + variant_id: "test-variant_2", + }, { + withDeleted: true, + }) + + expect(optValDeleted).toEqual(expect.objectContaining({ + deleted_at: expect.any(Date), + variant_id: "test-variant_2", + })) + }) + + it("successfully deletes a product and any option value associated with one of its variants", async () => { + const api = useApi() + + // Validate that the option value exists + const optValPre = await dbConnection.manager.findOne(ProductOptionValue, { + variant_id: "test-variant_2", + }) + + expect(optValPre).not.toEqual(undefined) + + // Soft delete the product + const response = await api.delete("/admin/products/test-product", { + headers: { + Authorization: "Bearer test_token", + }, + }) + + expect(response.status).toEqual(200) + + // Validate that the option value has been deleted + const optValPost = await dbConnection.manager.findOne( + ProductOptionValue, + { + variant_id: "test-variant_2", + } + ) + + expect(optValPost).toEqual(undefined) + + // Validate that the option still exists in the DB with deleted_at + const optValDeleted = await dbConnection.manager.findOne(ProductOptionValue, { + variant_id: "test-variant_2", + }, { + withDeleted: true, + }) + + expect(optValDeleted).toEqual(expect.objectContaining({ + deleted_at: expect.any(Date), + variant_id: "test-variant_2", + })) + }) + + it("successfully deletes a product variant and its associated prices", async () => { + const api = useApi() + + // Validate that the price exists + const pricePre = await dbConnection.manager.findOne(MoneyAmount, { + id: "test-price", + }) + + expect(pricePre).not.toEqual(undefined) + + // Soft delete the variant + const response = await api.delete( + "/admin/products/test-product/variants/test-variant", + { + headers: { + Authorization: "Bearer test_token", + }, + } + ) + + expect(response.status).toEqual(200) + + // Validate that the price was deleted + const pricePost = await dbConnection.manager.findOne( + MoneyAmount, + { + id: "test-price", + } + ) + + expect(pricePost).toEqual(undefined) + + // Validate that the price still exists in the DB with deleted_at + const optValDeleted = await dbConnection.manager.findOne(MoneyAmount, { + id: "test-price", + }, { + withDeleted: true, + }) + + expect(optValDeleted).toEqual(expect.objectContaining({ + deleted_at: expect.any(Date), + id: "test-price", + })) + }) + + it("successfully deletes a product and any prices associated with one of its variants", async () => { + const api = useApi() + + // Validate that the price exists + const pricePre = await dbConnection.manager.findOne(MoneyAmount, { + id: "test-price", + }) + + expect(pricePre).not.toEqual(undefined) + + // Soft delete the product + const response = await api.delete("/admin/products/test-product", { + headers: { + Authorization: "Bearer test_token", + }, + }) + + expect(response.status).toEqual(200) + + // Validate that the price has been deleted + const pricePost = await dbConnection.manager.findOne(MoneyAmount, { + id: "test-price", + }) + + expect(pricePost).toEqual(undefined) + + // Validate that the price still exists in the DB with deleted_at + const optValDeleted = await dbConnection.manager.findOne(MoneyAmount, { + id: "test-price", + }, { + withDeleted: true, + }) + + expect(optValDeleted).toEqual(expect.objectContaining({ + deleted_at: expect.any(Date), + id: "test-price", + })) + }) + it("successfully creates product with soft-deleted product handle and deletes it again", async () => { const api = useApi() diff --git a/packages/medusa/src/services/product-variant.ts b/packages/medusa/src/services/product-variant.ts index 62fe890150..2d8f6cf4af 100644 --- a/packages/medusa/src/services/product-variant.ts +++ b/packages/medusa/src/services/product-variant.ts @@ -751,7 +751,7 @@ class ProductVariantService extends BaseService { const variant = await variantRepo.findOne({ where: { id: variantId }, - relations: ["prices"], + relations: ["prices", "options"], }) if (!variant) { diff --git a/packages/medusa/src/services/product.js b/packages/medusa/src/services/product.js index 5e1da6caa8..b746e2e049 100644 --- a/packages/medusa/src/services/product.js +++ b/packages/medusa/src/services/product.js @@ -668,7 +668,7 @@ class ProductService extends BaseService { // Should not fail, if product does not exist, since delete is idempotent const product = await productRepo.findOne( { id: productId }, - { relations: ["variants"] } + { relations: ["variants", "variants.prices", "variants.options"] } ) if (!product) { From f71b9b3a8733fdcfe4298fcf49fd06ae89850fc2 Mon Sep 17 00:00:00 2001 From: Zakaria El Asri <33696020+zakariaelas@users.noreply.github.com> Date: Sun, 8 May 2022 17:45:18 +0100 Subject: [PATCH 13/16] fix(medusa): support searching for price lists (#1407) --- .../api/__tests__/admin/price-list.js | 111 ++++++++++++++++++ integration-tests/api/package.json | 6 +- integration-tests/api/yarn.lock | 71 ++++++----- .../medusa/src/repositories/price-list.ts | 94 ++++++++++++++- packages/medusa/src/services/price-list.ts | 13 +- packages/medusa/src/types/common.ts | 17 +++ 6 files changed, 268 insertions(+), 44 deletions(-) diff --git a/integration-tests/api/__tests__/admin/price-list.js b/integration-tests/api/__tests__/admin/price-list.js index e7aae7f444..b1c39b9819 100644 --- a/integration-tests/api/__tests__/admin/price-list.js +++ b/integration-tests/api/__tests__/admin/price-list.js @@ -211,6 +211,117 @@ describe("/admin/price-lists", () => { ]) ) }) + + it("given a search query, returns matching results by name", async () => { + const api = useApi() + + const response = await api + .get("/admin/price-lists?q=winter", { + headers: { + Authorization: "Bearer test_token", + }, + }) + .catch((err) => { + console.warn(err.response.data) + }) + + expect(response.status).toEqual(200) + expect(response.data.price_lists).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: "VIP winter sale", + }), + ]) + ) + expect(response.data.count).toEqual(1) + }) + + it("given a search query, returns matching results by description", async () => { + const api = useApi() + + const response = await api + .get("/admin/price-lists?q=25%", { + headers: { + Authorization: "Bearer test_token", + }, + }) + .catch((err) => { + console.warn(err.response.data) + }) + + expect(response.status).toEqual(200) + expect(response.data.price_lists).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: "VIP winter sale", + description: + "Winter sale for VIP customers. 25% off selected items.", + }), + ]) + ) + expect(response.data.count).toEqual(1) + }) + + it("given a search query, returns empty list when does not exist", async () => { + const api = useApi() + + const response = await api + .get("/admin/price-lists?q=blablabla", { + headers: { + Authorization: "Bearer test_token", + }, + }) + .catch((err) => { + console.warn(err.response.data) + }) + + expect(response.status).toEqual(200) + expect(response.data.price_lists).toEqual([]) + expect(response.data.count).toEqual(0) + }) + + it("given a search query and a status filter not matching any price list, returns an empty set", async () => { + const api = useApi() + + const response = await api + .get("/admin/price-lists?q=vip&status[]=draft", { + headers: { + Authorization: "Bearer test_token", + }, + }) + .catch((err) => { + console.warn(err.response.data) + }) + + expect(response.status).toEqual(200) + expect(response.data.price_lists).toEqual([]) + expect(response.data.count).toEqual(0) + }) + + it("given a search query and a status filter matching a price list, returns a price list", async () => { + const api = useApi() + + const response = await api + .get("/admin/price-lists?q=vip&status[]=active", { + headers: { + Authorization: "Bearer test_token", + }, + }) + .catch((err) => { + console.warn(err.response.data) + }) + + expect(response.status).toEqual(200) + expect(response.data.price_lists).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: "VIP winter sale", + status: "active", + }), + ]) + ) + expect(response.data.count).toEqual(1) + }) }) describe("POST /admin/price-lists/:id", () => { diff --git a/integration-tests/api/package.json b/integration-tests/api/package.json index 42e6e05c96..40318b328f 100644 --- a/integration-tests/api/package.json +++ b/integration-tests/api/package.json @@ -8,16 +8,16 @@ "build": "babel src -d dist --extensions \".ts,.js\"" }, "dependencies": { - "@medusajs/medusa": "1.2.1-dev-1649181615374", + "@medusajs/medusa": "1.2.1-dev-1650573289860", "faker": "^5.5.3", - "medusa-interfaces": "1.2.1-dev-1649181615374", + "medusa-interfaces": "1.2.1-dev-1650573289860", "typeorm": "^0.2.31" }, "devDependencies": { "@babel/cli": "^7.12.10", "@babel/core": "^7.12.10", "@babel/node": "^7.12.10", - "babel-preset-medusa-package": "1.1.19-dev-1649181615374", + "babel-preset-medusa-package": "1.1.19-dev-1650573289860", "jest": "^26.6.3" } } diff --git a/integration-tests/api/yarn.lock b/integration-tests/api/yarn.lock index b81b1094d6..b83de4e288 100644 --- a/integration-tests/api/yarn.lock +++ b/integration-tests/api/yarn.lock @@ -1301,10 +1301,10 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" -"@medusajs/medusa-cli@1.2.1-dev-1649181615374": - version "1.2.1-dev-1649181615374" - resolved "http://localhost:4873/@medusajs%2fmedusa-cli/-/medusa-cli-1.2.1-dev-1649181615374.tgz#1ea9014e3ec9813a52457b0d6e2fc6bb64d3bfd6" - integrity sha512-8m6Z1ZZqstZKaAaKoFS3v3IzI7BFhcBgpF+iCSRuJoXltQgzVQOAxXuPjkRoi+m1ZZ+Yi/YYEzKmNQ99vmXisQ== +"@medusajs/medusa-cli@1.2.1-dev-1650573289860": + version "1.2.1-dev-1650573289860" + resolved "http://localhost:4873/@medusajs%2fmedusa-cli/-/medusa-cli-1.2.1-dev-1650573289860.tgz#7685e4add2985e95fd945f6e7154f6ecb175d565" + integrity sha512-RpLR/uM/HfEEFtlZmeImT295ohpSCOKTrWcKVXj8UT6L4jj+FJ0SpcW3Fnr2Q5kOulQVL3qXx5t79Nz02O4qvw== dependencies: "@babel/polyfill" "^7.8.7" "@babel/runtime" "^7.9.6" @@ -1322,8 +1322,8 @@ is-valid-path "^0.1.1" joi-objectid "^3.0.1" meant "^1.0.1" - medusa-core-utils "1.1.31-dev-1649181615374" - medusa-telemetry "0.0.11-dev-1649181615374" + medusa-core-utils "1.1.31-dev-1650573289860" + medusa-telemetry "0.0.11-dev-1650573289860" netrc-parser "^3.1.6" open "^8.0.6" ora "^5.4.1" @@ -1337,13 +1337,13 @@ winston "^3.3.3" yargs "^15.3.1" -"@medusajs/medusa@1.2.1-dev-1649181615374": - version "1.2.1-dev-1649181615374" - resolved "http://localhost:4873/@medusajs%2fmedusa/-/medusa-1.2.1-dev-1649181615374.tgz#6a62f8628b84b47a8717e9e0c276f3a9c2e376ce" - integrity sha512-eiCGE6JqYuP7GCzTBGg5LI9U0uQ0wlsR+NuMZVEwldj+xc7qwMjBJwUA7gc58gBv6JesfMYj3VZmJComN4+7Bg== +"@medusajs/medusa@1.2.1-dev-1650573289860": + version "1.2.1-dev-1650573289860" + resolved "http://localhost:4873/@medusajs%2fmedusa/-/medusa-1.2.1-dev-1650573289860.tgz#740d20bf349be9ad9e0f151305a75bd40284b641" + integrity sha512-kU3l95SjU/B4SQLhof2obdXbWwkMcRKDnSXE2CB4G1a9jchwnzfRDVDy2MjO/4/0l6AgTLHwxoBR8F9zYUsiDQ== dependencies: "@hapi/joi" "^16.1.8" - "@medusajs/medusa-cli" "1.2.1-dev-1649181615374" + "@medusajs/medusa-cli" "1.2.1-dev-1650573289860" "@types/lodash" "^4.14.168" awilix "^4.2.3" body-parser "^1.19.0" @@ -1356,7 +1356,6 @@ core-js "^3.6.5" cors "^2.8.5" cross-spawn "^7.0.3" - dotenv "^8.2.0" express "^4.17.1" express-session "^1.17.1" fs-exists-cached "^1.0.0" @@ -1367,8 +1366,8 @@ joi "^17.3.0" joi-objectid "^3.0.1" jsonwebtoken "^8.5.1" - medusa-core-utils "1.1.31-dev-1649181615374" - medusa-test-utils "1.1.37-dev-1649181615374" + medusa-core-utils "1.1.31-dev-1650573289860" + medusa-test-utils "1.1.37-dev-1650573289860" morgan "^1.9.1" multer "^1.4.2" passport "^0.4.0" @@ -2010,10 +2009,10 @@ babel-preset-jest@^26.6.2: babel-plugin-jest-hoist "^26.6.2" babel-preset-current-node-syntax "^1.0.0" -babel-preset-medusa-package@1.1.19-dev-1649181615374: - version "1.1.19-dev-1649181615374" - resolved "http://localhost:4873/babel-preset-medusa-package/-/babel-preset-medusa-package-1.1.19-dev-1649181615374.tgz#2f13d52fedd336ad4b4c0602b3bf4696d2d08db7" - integrity sha512-N4XL7rTmNM2W+iRR92xvU4bKadP25lY5QR3vndxTxsLNSgcR5tLjKLO/4j7AqiFvcthbE8cF1TcdECH5aJfSuA== +babel-preset-medusa-package@1.1.19-dev-1650573289860: + version "1.1.19-dev-1650573289860" + resolved "http://localhost:4873/babel-preset-medusa-package/-/babel-preset-medusa-package-1.1.19-dev-1650573289860.tgz#493490de8ca1ce75b30545f19fdb9544b6324af4" + integrity sha512-++eqULlSbdH4bnwi/edLa097io4sxZvJSaXIwSC0it7GDB3IXITk5xyuxgKCJue3psSexwuUV58i/or8sZ1idg== dependencies: "@babel/plugin-proposal-class-properties" "^7.12.1" "@babel/plugin-proposal-decorators" "^7.12.1" @@ -5156,25 +5155,23 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= -medusa-core-utils@1.1.31-dev-1649181615374: - version "1.1.31-dev-1649181615374" - resolved "http://localhost:4873/medusa-core-utils/-/medusa-core-utils-1.1.31-dev-1649181615374.tgz#60416bba53eaba607d77ca36789aa23756b8db0f" - integrity sha512-w5nusocZweIrAFJ6sl4hD/mN+UtNjz39IIfXukekyJByg3wpv4P+vsW3XdrFTX5OLvKVxuzjl3B2zeZUmdgSKg== +medusa-core-utils@1.1.31-dev-1650573289860: + version "1.1.31-dev-1650573289860" + resolved "http://localhost:4873/medusa-core-utils/-/medusa-core-utils-1.1.31-dev-1650573289860.tgz#4b6ce1ab888a1b56dc08657e1c9ec0686678d7c1" + integrity sha512-y4Xy9Z+LQAXK4CzGzrC+sn0ngTfZgzIbVTUFVi2YhjRrjCXP36Caisf7e5gEkvmC8TxFsl061mneTcZBX+ni9g== dependencies: joi "^17.3.0" joi-objectid "^3.0.1" -medusa-interfaces@1.2.1-dev-1649181615374: - version "1.2.1-dev-1649181615374" - resolved "http://localhost:4873/medusa-interfaces/-/medusa-interfaces-1.2.1-dev-1649181615374.tgz#0b664f4e3e8e61b67108a41c8f0f9dd58a947075" - integrity sha512-JRD773nZnxjn/2oNrgb/zXn+scBoNHpW97YQYW7+LFX1JBYafzyGQW3vWTFX4X+q08ehz4dc21CgoMeYco8yvQ== - dependencies: - medusa-core-utils "1.1.31-dev-1649181615374" +medusa-interfaces@1.2.1-dev-1650573289860: + version "1.2.1-dev-1650573289860" + resolved "http://localhost:4873/medusa-interfaces/-/medusa-interfaces-1.2.1-dev-1650573289860.tgz#088ef6571cf3ec4b77de716bb6a3897f5a7a3de7" + integrity sha512-/WFMXz6iZp8tau6V/eVYao4SoIyYDrIUKXx32dfFibsQdnf8ev2CL08iTncfmWgAdlHNlO3lMJKF4arEdv3QTQ== -medusa-telemetry@0.0.11-dev-1649181615374: - version "0.0.11-dev-1649181615374" - resolved "http://localhost:4873/medusa-telemetry/-/medusa-telemetry-0.0.11-dev-1649181615374.tgz#3f4c366ea8d0d0fdde9b289f7e771bb27adae56d" - integrity sha512-RMJR3/qlTb1nV05RnBnX1bNOvYyeuXf4owxLlfbWzKAZirWQ5LAC2GikEGGbHGbw7UgiLgQtu4Rnmg2Uye+VcA== +medusa-telemetry@0.0.11-dev-1650573289860: + version "0.0.11-dev-1650573289860" + resolved "http://localhost:4873/medusa-telemetry/-/medusa-telemetry-0.0.11-dev-1650573289860.tgz#d74c00da87adc4e0105047db519dcbbddc36475e" + integrity sha512-UFOGj3hKpfJLKIaQMZQqb2DlGs0gScPJdrgMa5GQFwOGxcYCN2D6gyWtIWuKEDZ8oG4X8MCE0zTa3r+Sh4+zPQ== dependencies: axios "^0.21.1" axios-retry "^3.1.9" @@ -5186,13 +5183,13 @@ medusa-telemetry@0.0.11-dev-1649181615374: remove-trailing-slash "^0.1.1" uuid "^8.3.2" -medusa-test-utils@1.1.37-dev-1649181615374: - version "1.1.37-dev-1649181615374" - resolved "http://localhost:4873/medusa-test-utils/-/medusa-test-utils-1.1.37-dev-1649181615374.tgz#079c16a791d47c52072c6f0837d0a827208bc9cc" - integrity sha512-hj3iNZsIA01l7qAZrOgt+kT8PDkXKoW4CEL3bhVfIUEwsdv9jID7FGdTIN/7G3diioTypvrVcqRrp0uiWdgp+Q== +medusa-test-utils@1.1.37-dev-1650573289860: + version "1.1.37-dev-1650573289860" + resolved "http://localhost:4873/medusa-test-utils/-/medusa-test-utils-1.1.37-dev-1650573289860.tgz#1c8705617b64c4a474891f99985044faa5f8fa2f" + integrity sha512-MnKhy7hbNcZdYjVm9B3Z9MAT7MTf4oTdqQR9/lmb4qd7dJqd2AbxhglsOn5ZRYQHynuzubS+j9EHkxgOgji8MQ== dependencies: "@babel/plugin-transform-classes" "^7.9.5" - medusa-core-utils "1.1.31-dev-1649181615374" + medusa-core-utils "1.1.31-dev-1650573289860" randomatic "^3.1.1" merge-descriptors@1.0.1: diff --git a/packages/medusa/src/repositories/price-list.ts b/packages/medusa/src/repositories/price-list.ts index bcf37926bf..0eac64a2d2 100644 --- a/packages/medusa/src/repositories/price-list.ts +++ b/packages/medusa/src/repositories/price-list.ts @@ -1,5 +1,95 @@ -import { EntityRepository, Repository } from "typeorm" +import { groupBy, map } from "lodash" +import { + Brackets, + EntityRepository, + FindManyOptions, Repository +} from "typeorm" import { PriceList } from "../models/price-list" +import { CustomFindOptions } from "../types/common" + +type PriceListFindOptions = CustomFindOptions @EntityRepository(PriceList) -export class PriceListRepository extends Repository {} +export class PriceListRepository extends Repository { + public async getFreeTextSearchResultsAndCount( + q: string, + options: PriceListFindOptions = { where: {} }, + relations: (keyof PriceList)[] = [] + ): Promise<[PriceList[], number]> { + options.where = options.where ?? {} + let qb = this.createQueryBuilder("price_list") + .leftJoinAndSelect("price_list.customer_groups", "customer_group") + .select(["price_list.id"]) + .where(options.where) + .andWhere( + new Brackets((qb) => { + qb.where(`price_list.description ILIKE :q`, { q: `%${q}%` }) + .orWhere(`price_list.name ILIKE :q`, { q: `%${q}%` }) + .orWhere(`customer_group.name ILIKE :q`, { q: `%${q}%` }) + }) + ) + .skip(options.skip) + .take(options.take) + + const [results, count] = await qb.getManyAndCount() + + const price_lists = await this.findWithRelations( + relations, + results.map((r) => r.id) + ) + + return [price_lists, count] + } + + public async findWithRelations( + relations: (keyof PriceList)[] = [], + idsOrOptionsWithoutRelations: + | Omit, "relations"> + | string[] = {} + ): Promise { + let entities + if (Array.isArray(idsOrOptionsWithoutRelations)) { + entities = await this.findByIds(idsOrOptionsWithoutRelations) + } else { + entities = await this.find(idsOrOptionsWithoutRelations) + } + + const groupedRelations: Record = {} + for (const relation of relations) { + const [topLevel] = relation.split(".") + if (groupedRelations[topLevel]) { + groupedRelations[topLevel].push(relation) + } else { + groupedRelations[topLevel] = [relation] + } + } + + const entitiesIds = entities.map(({ id }) => id) + const entitiesIdsWithRelations = await Promise.all( + Object.values(groupedRelations).map((relations: string[]) => { + return this.findByIds(entitiesIds, { + select: ["id"], + relations: relations as string[], + }) + }) + ).then(entitiesIdsWithRelations => entitiesIdsWithRelations.flat()) + const entitiesAndRelations = entitiesIdsWithRelations.concat(entities) + + const entitiesAndRelationsById = groupBy(entitiesAndRelations, "id") + return map(entitiesAndRelationsById, (entityAndRelations) => + this.merge(this.create(), ...entityAndRelations) + ) + } + + public async findOneWithRelations( + relations: (keyof PriceList)[] = [], + options: Omit, "relations"> = {} + ): Promise { + options.take = 1 + + return (await this.findWithRelations( + relations, + options + ))?.pop() + } +} diff --git a/packages/medusa/src/services/price-list.ts b/packages/medusa/src/services/price-list.ts index 6c96e8508e..38bae29080 100644 --- a/packages/medusa/src/services/price-list.ts +++ b/packages/medusa/src/services/price-list.ts @@ -260,9 +260,18 @@ class PriceListService extends BaseService { config: FindConfig = { skip: 0, take: 20 } ): Promise<[PriceList[], number]> { const priceListRepo = this.manager_.getCustomRepository(this.priceListRepo_) + const q = selector.q + const { relations, ...query } = this.buildQuery_(selector, config) - const query = this.buildQuery_(selector, config) - return await priceListRepo.findAndCount(query) + if (q) { + delete query.where.q + return await priceListRepo.getFreeTextSearchResultsAndCount( + q, + query, + relations + ) + } + return await priceListRepo.findAndCount({ ...query, relations }) } async upsertCustomerGroups_( diff --git a/packages/medusa/src/types/common.ts b/packages/medusa/src/types/common.ts index 94f06c01e1..98bd6fde7d 100644 --- a/packages/medusa/src/types/common.ts +++ b/packages/medusa/src/types/common.ts @@ -1,6 +1,12 @@ import { Transform, Type } from "class-transformer" import { IsDate, IsNumber, IsOptional, IsString } from "class-validator" import "reflect-metadata" +import { + BaseEntity, + FindManyOptions, + FindOperator, + OrderByCondition, +} from "typeorm" import { transformDate } from "../utils/validators/date-transform" export type PartialPick = { @@ -25,6 +31,17 @@ export interface FindConfig { order?: Record } +export interface CustomFindOptions { + select?: FindManyOptions["select"] + where?: FindManyOptions["where"] & + { + [P in InKeys]?: TModel[P][] + } + order?: OrderByCondition + skip?: number + take?: number +} + export type PaginatedResponse = { limit: number; offset: number; count: number } export type DeleteResponse = { From 525910f72aa76355c29dd153f28ea08221956f3e Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Mon, 9 May 2022 09:41:18 +0200 Subject: [PATCH 14/16] fix(medusa-file-spaces): Allow duplicate filenames (#1474) --- packages/medusa-file-spaces/src/services/digital-ocean.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/medusa-file-spaces/src/services/digital-ocean.js b/packages/medusa-file-spaces/src/services/digital-ocean.js index 82e6a66e77..d9ef58e2e7 100644 --- a/packages/medusa-file-spaces/src/services/digital-ocean.js +++ b/packages/medusa-file-spaces/src/services/digital-ocean.js @@ -1,5 +1,6 @@ import fs from "fs" import aws from "aws-sdk" +import { parse } from "path" import { FileService } from "medusa-interfaces" class DigitalOceanService extends FileService { @@ -23,12 +24,14 @@ class DigitalOceanService extends FileService { endpoint: this.endpoint_, }) + const parsedFilename = parse(file.originalname) + const fileKey = `${parsedFilename.name}-${Date.now()}${parsedFilename.ext}` const s3 = new aws.S3() var params = { ACL: "public-read", Bucket: this.bucket_, Body: fs.createReadStream(file.path), - Key: `${file.originalname}`, + Key: fileKey, } return new Promise((resolve, reject) => { From c67d6bee303ad8e43f320fe16f807b0d22890c5b Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Mon, 9 May 2022 09:42:47 +0200 Subject: [PATCH 15/16] fix(medusa): PluginLoaders when loading services should only look for js files (#1473) --- .../src/loaders/__tests__/plugins.spec.ts | 61 +++++++++++++++++++ packages/medusa/src/loaders/plugins.ts | 4 +- 2 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 packages/medusa/src/loaders/__tests__/plugins.spec.ts diff --git a/packages/medusa/src/loaders/__tests__/plugins.spec.ts b/packages/medusa/src/loaders/__tests__/plugins.spec.ts new file mode 100644 index 0000000000..e193a70d8c --- /dev/null +++ b/packages/medusa/src/loaders/__tests__/plugins.spec.ts @@ -0,0 +1,61 @@ +import { createContainer, asValue } from "awilix" +import { mkdirSync, rmSync, rmdirSync, writeFileSync } from "fs" +import { resolve } from "path" +import Logger from "../logger" +import { registerServices } from "../plugins" +import { MedusaContainer } from "../../types/global" + +const distTestTargetDirectorPath = resolve(__dirname, "__pluginsLoaderTest__") +const servicesTestTargetDirectoryPath = resolve(distTestTargetDirectorPath, "services") +const buildServiceTemplate = (name: string) => { + return ` + import { BaseService } from "medusa-interfaces" + export default class ${name}Service extends BaseService {} + ` +} + +describe('plugins loader', () => { + const container = createContainer() as MedusaContainer + const pluginsDetails = { + resolve: resolve(__dirname, "__pluginsLoaderTest__"), + name: `project-plugin`, + id: "fakeId", + options: {}, + version: '"fakeVersion', + } + + describe("registerServices", function() { + beforeAll(() => { + container.register("logger", asValue(Logger)) + mkdirSync(servicesTestTargetDirectoryPath, { mode: "777", recursive: true }) + writeFileSync(resolve(servicesTestTargetDirectoryPath, "test.js"), buildServiceTemplate("test")) + writeFileSync(resolve(servicesTestTargetDirectoryPath, "test2.js"), buildServiceTemplate("test2")) + writeFileSync(resolve(servicesTestTargetDirectoryPath, "test2.js.map"), "map:file") + writeFileSync(resolve(servicesTestTargetDirectoryPath, "test2.d.ts"), "export interface Test {}") + }) + + afterAll(() => { + rmSync(distTestTargetDirectorPath, { recursive: true, force: true }) + jest.clearAllMocks() + }) + + it('should load the services from the services directory but only js files', async () => { + let err; + try { + await registerServices(pluginsDetails, container) + } catch (e) { + err = e + } + + expect(err).toBeFalsy() + + const testService: (...args: unknown[]) => any = container.resolve("testService") + const test2Service: (...args: unknown[]) => any = container.resolve("test2Service") + + expect(testService).toBeTruthy() + expect(testService.constructor.name).toBe("testService") + expect(test2Service).toBeTruthy() + expect(test2Service.constructor.name).toBe("test2Service") + }) + }) +}) \ No newline at end of file diff --git a/packages/medusa/src/loaders/plugins.ts b/packages/medusa/src/loaders/plugins.ts index ae831ddbeb..7c24df9b17 100644 --- a/packages/medusa/src/loaders/plugins.ts +++ b/packages/medusa/src/loaders/plugins.ts @@ -245,8 +245,8 @@ function registerApi( * registered * @return {void} */ -async function registerServices(pluginDetails: PluginDetails, container: MedusaContainer): Promise { - const files = glob.sync(`${pluginDetails.resolve}/services/[!__]*`, {}) +export async function registerServices(pluginDetails: PluginDetails, container: MedusaContainer): Promise { + const files = glob.sync(`${pluginDetails.resolve}/services/[!__]*.js`, {}) await Promise.all( files.map(async (fn) => { const loaded = require(fn).default From 90870292c62e2e96ca8c9ffc51e32c73493e0c0b Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Mon, 9 May 2022 09:52:15 +0200 Subject: [PATCH 16/16] fix(medusa): Remove line-item.js file (#1414) --- .../src/services/__tests__/line-item.js | 15 +- packages/medusa/src/services/line-item.js | 282 ------------------ packages/medusa/src/services/line-item.ts | 4 +- 3 files changed, 11 insertions(+), 290 deletions(-) delete mode 100644 packages/medusa/src/services/line-item.js diff --git a/packages/medusa/src/services/__tests__/line-item.js b/packages/medusa/src/services/__tests__/line-item.js index 0642e9791d..d36bd8363a 100644 --- a/packages/medusa/src/services/__tests__/line-item.js +++ b/packages/medusa/src/services/__tests__/line-item.js @@ -3,7 +3,9 @@ import LineItemService from "../line-item" describe("LineItemService", () => { describe("create", () => { - const lineItemRepository = MockRepository({}) + const lineItemRepository = MockRepository({ + create: (data) => data + }) const cartRepository = MockRepository({ findOne: () => @@ -105,9 +107,10 @@ describe("LineItemService", () => { }) it("successfully create a line item giftcard", async () => { - const line = await await lineItemService.generate( + const line = await lineItemService.generate( IdMap.getId("test-giftcard"), - IdMap.getId("test-region") + IdMap.getId("test-region"), + 1 ) await lineItemService.create({ @@ -115,8 +118,8 @@ describe("LineItemService", () => { cart_id: IdMap.getId("test-cart"), }) - expect(lineItemRepository.create).toHaveBeenCalledTimes(1) - expect(lineItemRepository.create).toHaveBeenCalledWith({ + expect(lineItemRepository.create).toHaveBeenCalledTimes(2) + expect(lineItemRepository.create).toHaveBeenNthCalledWith(2, expect.objectContaining({ allow_discounts: false, variant_id: IdMap.getId("test-giftcard"), cart_id: IdMap.getId("test-cart"), @@ -128,7 +131,7 @@ describe("LineItemService", () => { is_giftcard: true, should_merge: true, metadata: {}, - }) + })) }) }) diff --git a/packages/medusa/src/services/line-item.js b/packages/medusa/src/services/line-item.js deleted file mode 100644 index e48f0892f4..0000000000 --- a/packages/medusa/src/services/line-item.js +++ /dev/null @@ -1,282 +0,0 @@ -import { MedusaError } from "medusa-core-utils" -import { BaseService } from "medusa-interfaces" - -/** - * Provides layer to manipulate line items. - * @extends BaseService - */ -class LineItemService extends BaseService { - constructor({ - manager, - lineItemRepository, - lineItemTaxLineRepository, - productVariantService, - productService, - regionService, - cartRepository, - lineItemAdjustmentService, - }) { - super() - - /** @private @const {EntityManager} */ - this.manager_ = manager - - /** @private @const {LineItemRepository} */ - this.lineItemRepository_ = lineItemRepository - - /** @private @const {typeof LineItemTaxLineRepository} */ - this.itemTaxLineRepo_ = lineItemTaxLineRepository - - /** @private @const {ProductVariantService} */ - this.productVariantService_ = productVariantService - - /** @private @const {ProductService} */ - this.productService_ = productService - - /** @private @const {RegionService} */ - this.regionService_ = regionService - - /** @private @const {CartRepository} */ - this.cartRepository_ = cartRepository - - this.lineItemAdjustmentService_ = lineItemAdjustmentService - } - - withTransaction(transactionManager) { - if (!transactionManager) { - return this - } - - const cloned = new LineItemService({ - manager: transactionManager, - lineItemRepository: this.lineItemRepository_, - lineItemTaxLineRepository: this.itemTaxLineRepo_, - productVariantService: this.productVariantService_, - productService: this.productService_, - regionService: this.regionService_, - cartRepository: this.cartRepository_, - lineItemAdjustmentService: this.lineItemAdjustmentService_, - }) - - cloned.transactionManager_ = transactionManager - - return cloned - } - - async list( - selector, - config = { skip: 0, take: 50, order: { created_at: "DESC" } } - ) { - const liRepo = this.manager_.getCustomRepository(this.lineItemRepository_) - const query = this.buildQuery_(selector, config) - return liRepo.find(query) - } - - /** - * Retrieves a line item by its id. - * @param {string} id - the id of the line item to retrieve - * @param {object} config - the config to be used at query building - * @return {LineItem} the line item - */ - async retrieve(id, config = {}) { - const lineItemRepository = this.manager_.getCustomRepository( - this.lineItemRepository_ - ) - - const validatedId = this.validateId_(id) - const query = this.buildQuery_({ id: validatedId }, config) - - const lineItem = await lineItemRepository.findOne(query) - - if (!lineItem) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `Line item with ${id} was not found` - ) - } - - return lineItem - } - - /** - * Creates return line items for a given cart based on the return items in a - * return. - * @param {string} returnId - the id to generate return items from. - * @param {string} cartId - the cart to assign the return line items to. - * @return {Promise} the created line items - */ - async createReturnLines(returnId, cartId) { - const lineItemRepo = this.manager_.getCustomRepository( - this.lineItemRepository_ - ) - - const itemTaxLineRepo = this.manager_.getCustomRepository( - this.itemTaxLineRepo_ - ) - - const items = await lineItemRepo.findByReturn(returnId) - - const toCreate = items.map((i) => - lineItemRepo.create({ - cart_id: cartId, - thumbnail: i.thumbnail, - is_return: true, - title: i.title, - variant_id: i.variant_id, - unit_price: -1 * i.unit_price, - quantity: i.return_item.quantity, - allow_discounts: i.allow_discounts, - tax_lines: i.tax_lines.map((tl) => { - return itemTaxLineRepo.create({ - name: tl.name, - code: tl.code, - rate: tl.rate, - metadata: tl.metadata, - }) - }), - metadata: i.metadata, - adjustments: i.adjustments.map((adjustment) => { - return { - amount: -1 * adjustment.amount, - description: adjustment.description, - discount_id: adjustment.discount_id, - metadata: adjustment.metadata, - } - }), - }) - ) - - return await lineItemRepo.save(toCreate) - } - - async generate(variantId, regionId, quantity, context = {}) { - return this.atomicPhase_(async (manager) => { - const variant = await this.productVariantService_ - .withTransaction(manager) - .retrieve(variantId, { - relations: ["product"], - include_discount_prices: true, - }) - - const region = await this.regionService_ - .withTransaction(manager) - .retrieve(regionId) - - let price - let shouldMerge = true - - if (context.unit_price !== undefined && context.unit_price !== null) { - // if custom unit_price, we ensure positive values - // and we choose to not merge the items - shouldMerge = false - if (context.unit_price < 0) { - price = 0 - } else { - price = context.unit_price - } - } else { - price = await this.productVariantService_ - .withTransaction(manager) - .getRegionPrice(variant.id, { - regionId: region.id, - quantity: quantity, - customer_id: context.customer_id, - include_discount_prices: true, - }) - } - - const toCreate = { - unit_price: price, - title: variant.product.title, - description: variant.title, - thumbnail: variant.product.thumbnail, - variant_id: variant.id, - quantity: quantity || 1, - allow_discounts: variant.product.discountable, - is_giftcard: variant.product.is_giftcard, - metadata: context?.metadata || {}, - should_merge: shouldMerge, - } - - if (context.cart) { - const adjustments = await this.lineItemAdjustmentService_ - .withTransaction(manager) - .generateAdjustments(context.cart, toCreate, { variant }) - toCreate.adjustments = adjustments - } - - return toCreate - }) - } - - /** - * Create a line item - * @param {LineItem} lineItem - the line item object to create - * @return {LineItem} the created line item - */ - async create(lineItem) { - return this.atomicPhase_(async (manager) => { - const lineItemRepository = manager.getCustomRepository( - this.lineItemRepository_ - ) - - const created = await lineItemRepository.create(lineItem) - const result = await lineItemRepository.save(created) - return result - }) - } - - /** - * Updates a line item - * @param {string} id - the id of the line item to update - * @param {object} update - the properties to update on line item - * @return {LineItem} the update line item - */ - async update(id, update) { - return this.atomicPhase_(async (manager) => { - const lineItemRepository = manager.getCustomRepository( - this.lineItemRepository_ - ) - - const lineItem = await this.retrieve(id) - - const { metadata, ...rest } = update - - if (metadata) { - lineItem.metadata = this.setMetadata_(lineItem, metadata) - } - - for (const [key, value] of Object.entries(rest)) { - lineItem[key] = value - } - - const result = await lineItemRepository.save(lineItem) - return result - }) - } - - /** - * Deletes a line item. - * @param {string} id - the id of the line item to delete - * @return {Promise} the result of the delete operation - */ - async delete(id) { - return this.atomicPhase_(async (manager) => { - const lineItemRepository = manager.getCustomRepository( - this.lineItemRepository_ - ) - - const lineItem = await lineItemRepository.findOne({ where: { id } }) - - if (!lineItem) { - return Promise.resolve() - } - - await lineItemRepository.remove(lineItem) - - return Promise.resolve() - }) - } -} - -export default LineItemService diff --git a/packages/medusa/src/services/line-item.ts b/packages/medusa/src/services/line-item.ts index 89fd8befcd..55e55a802c 100644 --- a/packages/medusa/src/services/line-item.ts +++ b/packages/medusa/src/services/line-item.ts @@ -236,10 +236,10 @@ class LineItemService extends BaseService { should_merge: shouldMerge, } - const lineLitemRepo = transactionManager.getCustomRepository( + const lineItemRepo = transactionManager.getCustomRepository( this.lineItemRepository_ ) - const lineItem = lineLitemRepo.create(rawLineItem) + const lineItem = lineItemRepo.create(rawLineItem) if (context.cart) { const adjustments = await this.lineItemAdjustmentService_