feat(medusa): Convert IdempotencyKeyService to TypeScript (#1995)

* feat(medusa): Migrate the idempotency key service to ts + fix

* feat(medusa): Finalise idempotency migration

* Create late-owls-pump.md

* feat(medusa): Polish

* feat(medusa): Add case to the error handler

* feat(medusa): Add case to the error handler

Co-authored-by: olivermrbl <oliver@mrbltech.com>
This commit is contained in:
Adrien de Peretti
2022-08-09 08:08:07 +02:00
committed by GitHub
parent 4b663cca3a
commit aaebb38eae
11 changed files with 226 additions and 186 deletions

View File

@@ -0,0 +1,6 @@
---
"@medusajs/medusa": patch
---
Convert IdempotencyKeyService to TypeScript
Add await to retrieve in lock method

View File

@@ -11,6 +11,7 @@ export const MedusaErrorTypes = {
NOT_FOUND: "not_found",
NOT_ALLOWED: "not_allowed",
UNEXPECTED_STATE: "unexpected_state",
CONFLICT: "conflict",
}
export const MedusaErrorCodes = {

View File

@@ -33,6 +33,7 @@ export default () => {
case QUERY_RUNNER_RELEASED:
case TRANSACTION_STARTED:
case TRANSACTION_NOT_STARTED:
case MedusaError.Types.CONFLICT:
statusCode = 409
errObj.code = INVALID_STATE_ERROR
errObj.message =

View File

@@ -148,7 +148,7 @@ export default async (req, res) => {
res.setHeader("Idempotency-Key", idempotencyKey.idempotency_key)
let inProgress = true
let err = false
let err: unknown = false
while (inProgress) {
switch (idempotencyKey.recovery_point) {

View File

@@ -34,7 +34,8 @@ export default async (req, res) => {
const headerKey = req.get("Idempotency-Key") || ""
let idempotencyKey!: IdempotencyKey
let idempotencyKey
try {
await manager.transaction(async (transactionManager) => {
idempotencyKey = await idempotencyKeyService
@@ -58,7 +59,7 @@ export default async (req, res) => {
const cartService: CartService = req.scope.resolve("cartService")
let inProgress = true
let err = false
let err: unknown = false
while (inProgress) {
switch (idempotencyKey.recovery_point) {

View File

@@ -108,7 +108,7 @@ export default async (req, res) => {
const eventBus: EventBusService = req.scope.resolve("eventBusService")
let inProgress = true
let err = false
let err: unknown = false
while (inProgress) {
switch (idempotencyKey.recovery_point) {

View File

@@ -114,7 +114,7 @@ export default async (req, res) => {
const returnService: ReturnService = req.scope.resolve("returnService")
let inProgress = true
let err = false
let err: unknown = false
while (inProgress) {
switch (idempotencyKey.recovery_point) {

View File

@@ -1,173 +0,0 @@
import { MedusaError } from "medusa-core-utils"
import { v4 } from "uuid"
import { TransactionBaseService } from "../interfaces"
const KEY_LOCKED_TIMEOUT = 1000
class IdempotencyKeyService extends TransactionBaseService {
constructor({ manager, idempotencyKeyRepository }) {
super({ manager, idempotencyKeyRepository })
/** @private @constant {EntityManager} */
this.manager_ = manager
/** @private @constant {IdempotencyKeyRepository} */
this.idempotencyKeyRepository_ = idempotencyKeyRepository
}
/**
* Execute the initial steps in a idempotent request.
* @param {string} headerKey - potential idempotency key from header
* @param {string} reqMethod - method of request
* @param {string} reqParams - params of request
* @param {string} reqPath - path of request
* @return {Promise<IdempotencyKeyModel>} the existing or created idempotency key
*/
async initializeRequest(headerKey, reqMethod, reqParams, reqPath) {
return this.atomicPhase_(async (_) => {
// If idempotency key exists, return it
let key = await this.retrieve(headerKey)
if (key) {
return key
}
key = await this.create({
request_method: reqMethod,
request_params: reqParams,
request_path: reqPath,
})
return key
}, "SERIALIZABLE")
}
/**
* Creates an idempotency key for a request.
* If no idempotency key is provided in request, we will create a unique
* identifier.
* @param {object} payload - payload of request to create idempotency key for
* @return {Promise<IdempotencyKeyModel>} the created idempotency key
*/
async create(payload) {
return this.atomicPhase_(async (manager) => {
const idempotencyKeyRepo = manager.getCustomRepository(
this.idempotencyKeyRepository_
)
if (!payload.idempotency_key) {
payload.idempotency_key = v4()
}
const created = await idempotencyKeyRepo.create(payload)
const result = await idempotencyKeyRepo.save(created)
return result
})
}
/**
* Retrieves an idempotency key
* @param {string} idempotencyKey - key to retrieve
* @return {Promise<IdempotencyKeyModel>} idempotency key
*/
async retrieve(idempotencyKey) {
const idempotencyKeyRepo = this.manager_.getCustomRepository(
this.idempotencyKeyRepository_
)
const key = await idempotencyKeyRepo.findOne({
where: { idempotency_key: idempotencyKey },
})
return key
}
/**
* Locks an idempotency.
* @param {string} idempotencyKey - key to lock
* @return {Promise} result of the update operation
*/
async lock(idempotencyKey) {
return this.atomicPhase_(async (manager) => {
const idempotencyKeyRepo = manager.getCustomRepository(
this.idempotencyKeyRepository_
)
const key = this.retrieve(idempotencyKey)
if (key.locked_at && key.locked_at > Date.now() - KEY_LOCKED_TIMEOUT) {
throw new MedusaError("conflict", "Key already locked")
}
const updated = await idempotencyKeyRepo.save({
...key,
locked_at: Date.now(),
})
return updated
})
}
/**
* Locks an idempotency.
* @param {string} idempotencyKey - key to update
* @param {object} update - update object
* @return {Promise} result of the update operation
*/
async update(idempotencyKey, update) {
return this.atomicPhase_(async (manager) => {
const idempotencyKeyRepo = manager.getCustomRepository(
this.idempotencyKeyRepository_
)
const iKey = await this.retrieve(idempotencyKey)
for (const [key, value] of Object.entries(update)) {
iKey[key] = value
}
const updated = await idempotencyKeyRepo.save(iKey)
return updated
})
}
/**
* Performs an atomic work stage.
* An atomic work stage contains some related functionality, that needs to be
* transactionally executed in isolation. An idempotent request will
* always consist of 2 or more of these phases. The required phases are
* "started" and "finished".
* @param {string} idempotencyKey - current idempotency key
* @param {Function} func - functionality to execute within the phase
* @return {IdempotencyKeyModel} new updated idempotency key
*/
async workStage(idempotencyKey, func) {
try {
return await this.atomicPhase_(async (manager) => {
let key
const { recovery_point, response_code, response_body } = await func(
manager
)
if (recovery_point) {
key = await this.update(idempotencyKey, {
recovery_point,
})
} else {
key = await this.update(idempotencyKey, {
recovery_point: "finished",
response_body,
response_code,
})
}
return { key }
}, "SERIALIZABLE")
} catch (err) {
return { error: err }
}
}
}
export default IdempotencyKeyService

View File

@@ -0,0 +1,198 @@
import { MedusaError } from "medusa-core-utils"
import { v4 } from "uuid"
import { TransactionBaseService } from "../interfaces"
import { DeepPartial, EntityManager } from "typeorm"
import { IdempotencyKeyRepository } from "../repositories/idempotency-key"
import { IdempotencyKey } from "../models"
import { CreateIdempotencyKeyInput } from "../types/idempotency-key"
const KEY_LOCKED_TIMEOUT = 1000
type InjectedDependencies = {
manager: EntityManager
idempotencyKeyRepository: typeof IdempotencyKeyRepository
}
class IdempotencyKeyService extends TransactionBaseService<IdempotencyKeyService> {
protected manager_: EntityManager
protected transactionManager_: EntityManager | undefined
protected readonly idempotencyKeyRepository_: typeof IdempotencyKeyRepository
constructor({ manager, idempotencyKeyRepository }: InjectedDependencies) {
super({ manager, idempotencyKeyRepository })
this.manager_ = manager
this.idempotencyKeyRepository_ = idempotencyKeyRepository
}
/**
* Execute the initial steps in a idempotent request.
* @param headerKey - potential idempotency key from header
* @param reqMethod - method of request
* @param reqParams - params of request
* @param reqPath - path of request
* @return the existing or created idempotency key
*/
async initializeRequest(
headerKey: string,
reqMethod: string,
reqParams: Record<string, unknown>,
reqPath: string
): Promise<IdempotencyKey> {
return await this.atomicPhase_(async () => {
const key = await this.retrieve(headerKey).catch(() => void 0)
if (key) {
return key
}
return await this.create({
request_method: reqMethod,
request_params: reqParams,
request_path: reqPath,
})
}, "SERIALIZABLE")
}
/**
* Creates an idempotency key for a request.
* If no idempotency key is provided in request, we will create a unique
* identifier.
* @param payload - payload of request to create idempotency key for
* @return the created idempotency key
*/
async create(payload: CreateIdempotencyKeyInput): Promise<IdempotencyKey> {
return await this.atomicPhase_(async (manager) => {
const idempotencyKeyRepo = manager.getCustomRepository(
this.idempotencyKeyRepository_
)
payload.idempotency_key = payload.idempotency_key ?? v4()
const created = idempotencyKeyRepo.create(payload)
return await idempotencyKeyRepo.save(created)
})
}
/**
* Retrieves an idempotency key
* @param idempotencyKey - key to retrieve
* @return idempotency key
*/
async retrieve(idempotencyKey: string): Promise<IdempotencyKey | never> {
const idempotencyKeyRepo = this.manager_.getCustomRepository(
this.idempotencyKeyRepository_
)
const iKey = await idempotencyKeyRepo.findOne({
where: { idempotency_key: idempotencyKey },
})
if (!iKey) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Idempotency key ${idempotencyKey} was not found`
)
}
return iKey
}
/**
* Locks an idempotency.
* @param idempotencyKey - key to lock
* @return result of the update operation
*/
async lock(idempotencyKey: string): Promise<IdempotencyKey | never> {
return await this.atomicPhase_(async (manager) => {
const idempotencyKeyRepo = manager.getCustomRepository(
this.idempotencyKeyRepository_
)
const key = await this.retrieve(idempotencyKey)
const isLocked =
key.locked_at &&
new Date(key.locked_at).getTime() > Date.now() - KEY_LOCKED_TIMEOUT
if (isLocked) {
throw new MedusaError(MedusaError.Types.CONFLICT, "Key already locked")
}
return await idempotencyKeyRepo.save({
...key,
locked_at: Date.now(),
})
})
}
/**
* Locks an idempotency.
* @param {string} idempotencyKey - key to update
* @param {object} update - update object
* @return {Promise} result of the update operation
*/
async update(
idempotencyKey: string,
update: DeepPartial<IdempotencyKey>
): Promise<IdempotencyKey> {
return await this.atomicPhase_(async (manager) => {
const idempotencyKeyRepo = manager.getCustomRepository(
this.idempotencyKeyRepository_
)
const iKey = await this.retrieve(idempotencyKey)
for (const [key, value] of Object.entries(update)) {
iKey[key] = value
}
return await idempotencyKeyRepo.save(iKey)
})
}
/**
* Performs an atomic work stage.
* An atomic work stage contains some related functionality, that needs to be
* transactionally executed in isolation. An idempotent request will
* always consist of 2 or more of these phases. The required phases are
* "started" and "finished".
* @param idempotencyKey - current idempotency key
* @param callback - functionality to execute within the phase
* @return new updated idempotency key
*/
async workStage(
idempotencyKey: string,
callback: (transactionManager: EntityManager) => Promise<
| {
recovery_point?: string
response_code?: number
response_body?: Record<string, unknown>
}
| never
>
): Promise<{ key?: IdempotencyKey; error?: unknown }> {
try {
return await this.atomicPhase_(async (manager) => {
const { recovery_point, response_code, response_body } = await callback(
manager
)
const data: DeepPartial<IdempotencyKey> = {
recovery_point: recovery_point ?? "finished",
}
if (!recovery_point) {
data.response_body = response_body
data.response_code = response_code
}
const key = await this.update(idempotencyKey, data)
return { key }
}, "SERIALIZABLE")
} catch (err) {
return { error: err }
}
}
}
export default IdempotencyKeyService

View File

@@ -1,16 +1,16 @@
import { EntityManager } from "typeorm"
import { MedusaError } from "medusa-core-utils"
import { EntityManager } from "typeorm"
import { IdempotencyKey, Order } from "../models"
import CartService from "../services/cart"
import { RequestContext } from "../types/request"
import OrderService from "../services/order"
import IdempotencyKeyService from "../services/idempotency-key"
import OrderService from "../services/order"
import SwapService from "../services/swap"
import { RequestContext } from "../types/request"
import {
CartCompletionResponse,
AbstractCartCompletionStrategy,
CartCompletionResponse,
} from "../interfaces"
type InjectedDependencies = {
@@ -58,7 +58,7 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
const swapService = this.swapService_
let inProgress = true
let err = false
let err: unknown = false
while (inProgress) {
switch (idempotencyKey.recovery_point) {
@@ -96,7 +96,7 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
inProgress = false
err = error
} else {
idempotencyKey = key
idempotencyKey = key as IdempotencyKey
}
})
break
@@ -141,7 +141,7 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
inProgress = false
err = error
} else {
idempotencyKey = key
idempotencyKey = key as IdempotencyKey
}
})
break
@@ -291,7 +291,7 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
inProgress = false
err = error
} else {
idempotencyKey = key
idempotencyKey = key as IdempotencyKey
}
})
break

View File

@@ -0,0 +1,6 @@
export type CreateIdempotencyKeyInput = {
request_method: string
request_params: Record<string, unknown>
request_path: string
idempotency_key?: string
}