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:
Adrien de Peretti
2022-04-13 17:42:09 +02:00
committed by GitHub
parent edc6d9d29c
commit 3f19a3c4d5
8 changed files with 23658 additions and 20393 deletions

View File

@@ -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 .",

View File

@@ -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 .",

View File

@@ -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"
}

View 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"
]
}

View File

@@ -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"],
})
})
})
})

View 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

View File

@@ -1,3 +1,4 @@
export * from "./tax-calculation-strategy"
export * from "./cart-completion-strategy"
export * from "./tax-service"
export * from "./base-service"

43602
yarn.lock

File diff suppressed because it is too large Load Diff