refactor(medusa-interfaces): Migrate baseService to TS + improve the service it self (#1314)
* refactor(medusa-interfaces): Migrate baseService to TS + improve the service it self * feat(medusa,medusa-interfaces): Cleanup and move base-service to medusa core * medusa(medusa, medusa-interfaces): Improve typings * medusa(medusa, medusa-interfaces): buildQuery make properties optionals * feat(medusa-interfaces): Revert temporarly the medusa-interfaces while moving the base-service to the medusa core * feat(medusa): Improve base-service typings * feat(medusa): Remove template that is not necessary
This commit is contained in:
committed by
GitHub
parent
edc6d9d29c
commit
3f19a3c4d5
@@ -24,7 +24,7 @@
|
||||
"cross-env": "^5.2.1",
|
||||
"eslint": "^6.8.0",
|
||||
"jest": "^25.5.2",
|
||||
"medusa-test-utils": "^0.3.0"
|
||||
"medusa-test-utils": "^1.1.37"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "babel src -d .",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"cross-env": "^5.2.1",
|
||||
"eslint": "^6.8.0",
|
||||
"jest": "^25.5.2",
|
||||
"medusa-test-utils": "^0.3.0"
|
||||
"medusa-test-utils": "^1.1.37"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "babel src -d .",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"directory": "packages/medusa-interfaces"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "babel src --out-dir dist/ --ignore **/__tests__",
|
||||
"build": "tsc --build",
|
||||
"prepare": "cross-env NODE_ENV=production npm run build",
|
||||
"watch": "babel -w src --out-dir dist/ --ignore **/__tests__",
|
||||
"test": "jest"
|
||||
@@ -24,18 +24,23 @@
|
||||
"@babel/plugin-transform-instanceof": "^7.8.3",
|
||||
"@babel/plugin-transform-runtime": "^7.7.6",
|
||||
"@babel/preset-env": "^7.7.5",
|
||||
"@babel/preset-typescript": "^7.13.0",
|
||||
"@babel/runtime": "^7.9.6",
|
||||
"cross-env": "^5.2.1",
|
||||
"eslint": "^6.8.0",
|
||||
"jest": "^25.5.2",
|
||||
"prettier": "^1.19.1",
|
||||
"typeorm": "^0.2.29"
|
||||
"typeorm": "^0.2.29",
|
||||
"typescript": "^4.4.4",
|
||||
"medusa-core-utils": "^1.1.31",
|
||||
"medusa-test-utils": "^1.1.37"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typeorm": "0.x"
|
||||
"typeorm": "0.x",
|
||||
"medusa-core-utils": "^1.1.31"
|
||||
},
|
||||
"dependencies": {
|
||||
"medusa-core-utils": "^1.1.31"
|
||||
|
||||
},
|
||||
"gitHead": "cd1f5afa5aa8c0b15ea957008ee19f1d695cbd2e"
|
||||
}
|
||||
|
||||
28
packages/medusa-interfaces/tsconfig.json
Normal file
28
packages/medusa-interfaces/tsconfig.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["es5", "es6"],
|
||||
"target": "es5",
|
||||
"outDir": "./dist",
|
||||
"esModuleInterop": true,
|
||||
"declaration": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"sourceMap": true,
|
||||
"noImplicitReturns": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"noImplicitThis": true,
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"downlevelIteration": true // to use ES5 specific tooling
|
||||
},
|
||||
"include": ["./src/**/*", "index.d.ts"],
|
||||
"exclude": [
|
||||
"./dist/**/*",
|
||||
"./src/**/__tests__",
|
||||
"./src/**/__mocks__",
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import BaseService from "../base-service"
|
||||
import { In, Not } from "typeorm"
|
||||
import { MockManager } from "medusa-test-utils"
|
||||
|
||||
describe("BaseService", () => {
|
||||
it("should cloned the child class withTransaction", () => {
|
||||
class Child extends BaseService<Child> {
|
||||
constructor(protected readonly container) {
|
||||
super(container, {});
|
||||
this.container = container
|
||||
}
|
||||
|
||||
message() {
|
||||
return `child class message method called with title ${this.container.title}`
|
||||
}
|
||||
|
||||
getTransactionManager() {
|
||||
return this.transactionManager_
|
||||
}
|
||||
}
|
||||
|
||||
const child = new Child({ title: 'title' })
|
||||
|
||||
expect(child.message()).toBe(`child class message method called with title title`)
|
||||
expect(child.getTransactionManager()).toBeFalsy()
|
||||
|
||||
const fakeManager = MockManager
|
||||
fakeManager.testProp = 'testProp'
|
||||
const child2 = child.withTransaction(fakeManager)
|
||||
|
||||
expect(child2.message()).toBe(`child class message method called with title title`)
|
||||
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"],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
342
packages/medusa/src/interfaces/base-service.ts
Normal file
342
packages/medusa/src/interfaces/base-service.ts
Normal file
@@ -0,0 +1,342 @@
|
||||
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"
|
||||
|
||||
type Selector<TEntity> = { [key in keyof TEntity]?: unknown }
|
||||
|
||||
/**
|
||||
* Common functionality for Services
|
||||
* @interface
|
||||
*/
|
||||
class BaseService<
|
||||
TChild extends BaseService<TChild, TContainer>,
|
||||
TContainer = unknown
|
||||
> {
|
||||
protected transactionManager_: EntityManager | undefined
|
||||
protected manager_: EntityManager
|
||||
private readonly container_: TContainer
|
||||
|
||||
constructor(
|
||||
container: TContainer,
|
||||
protected readonly configModule: Record<string, unknown>
|
||||
) {
|
||||
this.container_ = container
|
||||
}
|
||||
|
||||
withTransaction(): this
|
||||
withTransaction(transactionManager: EntityManager): TChild
|
||||
withTransaction(transactionManager?: EntityManager): this | TChild {
|
||||
if (!transactionManager) {
|
||||
return this
|
||||
}
|
||||
|
||||
const cloned = new (<typeof BaseService>this.constructor)<
|
||||
TChild,
|
||||
TContainer
|
||||
>(
|
||||
{
|
||||
...this.container_,
|
||||
manager: transactionManager,
|
||||
},
|
||||
this.configModule
|
||||
)
|
||||
|
||||
cloned.transactionManager_ = transactionManager
|
||||
|
||||
return cloned as TChild
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to build TypeORM queries.
|
||||
* @param selector The selector
|
||||
* @param config The config
|
||||
* @return The QueryBuilderConfig
|
||||
*/
|
||||
buildQuery_<TEntity = unknown>(
|
||||
selector: Selector<TEntity>,
|
||||
config: FindConfig<TEntity> = {}
|
||||
): FindConfig<TEntity> & {
|
||||
where: { [key in keyof TEntity]?: unknown }
|
||||
withDeleted?: boolean
|
||||
} {
|
||||
const build = (
|
||||
obj: Record<string, unknown>
|
||||
): { [key in keyof TEntity]?: unknown } => {
|
||||
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 as Record<string, unknown>).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 { [key in keyof TEntity]?: unknown })
|
||||
}
|
||||
|
||||
const query: FindConfig<TEntity> & {
|
||||
where: { [key in keyof TEntity]?: unknown }
|
||||
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
|
||||
}
|
||||
|
||||
shouldRetryTransaction(
|
||||
err: { code: string } | Record<string, unknown>
|
||||
): 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<unknown>,
|
||||
isolationOrErrorHandler?:
|
||||
| IsolationLevel
|
||||
| ((error: unknown) => Promise<unknown>),
|
||||
maybeErrorHandlerOrDontFail?: (error: unknown) => Promise<unknown>
|
||||
): Promise<unknown | never> {
|
||||
let errorHandler = maybeErrorHandlerOrDontFail
|
||||
let isolation:
|
||||
| IsolationLevel
|
||||
| ((error: unknown) => Promise<unknown>)
|
||||
| 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<unknown | never> => {
|
||||
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 doWork(this.transactionManager_)
|
||||
} else {
|
||||
const temp = this.manager_
|
||||
const doWork = async (m: EntityManager): Promise<unknown | never> => {
|
||||
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) =>
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<string, unknown> },
|
||||
metadata: Record<string, unknown>
|
||||
): Record<string, unknown> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
export default BaseService
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./tax-calculation-strategy"
|
||||
export * from "./cart-completion-strategy"
|
||||
export * from "./tax-service"
|
||||
export * from "./base-service"
|
||||
|
||||
Reference in New Issue
Block a user