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:
committed by
GitHub
parent
4b663cca3a
commit
aaebb38eae
6
.changeset/late-owls-pump.md
Normal file
6
.changeset/late-owls-pump.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@medusajs/medusa": patch
|
||||
---
|
||||
|
||||
Convert IdempotencyKeyService to TypeScript
|
||||
Add await to retrieve in lock method
|
||||
@@ -11,6 +11,7 @@ export const MedusaErrorTypes = {
|
||||
NOT_FOUND: "not_found",
|
||||
NOT_ALLOWED: "not_allowed",
|
||||
UNEXPECTED_STATE: "unexpected_state",
|
||||
CONFLICT: "conflict",
|
||||
}
|
||||
|
||||
export const MedusaErrorCodes = {
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
198
packages/medusa/src/services/idempotency-key.ts
Normal file
198
packages/medusa/src/services/idempotency-key.ts
Normal 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
|
||||
@@ -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
|
||||
|
||||
6
packages/medusa/src/types/idempotency-key.ts
Normal file
6
packages/medusa/src/types/idempotency-key.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export type CreateIdempotencyKeyInput = {
|
||||
request_method: string
|
||||
request_params: Record<string, unknown>
|
||||
request_path: string
|
||||
idempotency_key?: string
|
||||
}
|
||||
Reference in New Issue
Block a user