feat(medusa): List batch jobs + Introduce composable handler pattern (#1541)
This commit is contained in:
committed by
GitHub
parent
f0ecef6b9a
commit
4489b75f5a
@@ -0,0 +1,44 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`/admin/batch GET /admin/batch lists batch jobs created by the user 1`] = `
|
||||
Object {
|
||||
"batch_jobs": Array [
|
||||
Object {
|
||||
"context": Object {},
|
||||
"created_at": Any<String>,
|
||||
"created_by": "admin_user",
|
||||
"deleted_at": null,
|
||||
"id": "job_3",
|
||||
"result": null,
|
||||
"status": "created",
|
||||
"type": "batch_2",
|
||||
"updated_at": Any<String>,
|
||||
},
|
||||
Object {
|
||||
"context": Object {},
|
||||
"created_at": Any<String>,
|
||||
"created_by": "admin_user",
|
||||
"deleted_at": null,
|
||||
"id": "job_2",
|
||||
"result": null,
|
||||
"status": "created",
|
||||
"type": "batch_2",
|
||||
"updated_at": Any<String>,
|
||||
},
|
||||
Object {
|
||||
"context": Object {},
|
||||
"created_at": Any<String>,
|
||||
"created_by": "admin_user",
|
||||
"deleted_at": null,
|
||||
"id": "job_1",
|
||||
"result": null,
|
||||
"status": "created",
|
||||
"type": "batch_1",
|
||||
"updated_at": Any<String>,
|
||||
},
|
||||
],
|
||||
"count": 3,
|
||||
"limit": 10,
|
||||
"offset": 0,
|
||||
}
|
||||
`;
|
||||
94
integration-tests/api/__tests__/batch-jobs/api.js
Normal file
94
integration-tests/api/__tests__/batch-jobs/api.js
Normal file
@@ -0,0 +1,94 @@
|
||||
const path = require("path")
|
||||
|
||||
const setupServer = require("../../../helpers/setup-server")
|
||||
const { useApi } = require("../../../helpers/use-api")
|
||||
const { initDb, useDb } = require("../../../helpers/use-db")
|
||||
|
||||
const adminSeeder = require("../../helpers/admin-seeder")
|
||||
const { simpleBatchJobFactory } = require("../../factories")
|
||||
|
||||
jest.setTimeout(50000)
|
||||
|
||||
const adminReqConfig = {
|
||||
headers: {
|
||||
Authorization: "Bearer test_token",
|
||||
},
|
||||
}
|
||||
|
||||
describe("/admin/batch", () => {
|
||||
let medusaProcess
|
||||
let dbConnection
|
||||
|
||||
beforeAll(async () => {
|
||||
const cwd = path.resolve(path.join(__dirname, "..", ".."))
|
||||
dbConnection = await initDb({ cwd })
|
||||
medusaProcess = await setupServer({ cwd, verbose: false })
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
const db = useDb()
|
||||
await db.shutdown()
|
||||
|
||||
medusaProcess.kill()
|
||||
})
|
||||
|
||||
describe("GET /admin/batch", () => {
|
||||
beforeEach(async () => {
|
||||
try {
|
||||
await simpleBatchJobFactory(dbConnection, {
|
||||
id: "job_1",
|
||||
type: "batch_1",
|
||||
created_by: "admin_user",
|
||||
})
|
||||
await simpleBatchJobFactory(dbConnection, {
|
||||
id: "job_2",
|
||||
type: "batch_2",
|
||||
created_by: "admin_user",
|
||||
})
|
||||
await simpleBatchJobFactory(dbConnection, {
|
||||
id: "job_3",
|
||||
type: "batch_2",
|
||||
created_by: "admin_user",
|
||||
})
|
||||
await simpleBatchJobFactory(dbConnection, {
|
||||
id: "job_4",
|
||||
type: "batch_1",
|
||||
created_by: "not_this_user",
|
||||
})
|
||||
await adminSeeder(dbConnection)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
throw err
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
const db = useDb()
|
||||
await db.teardown()
|
||||
})
|
||||
|
||||
it("lists batch jobs created by the user", async () => {
|
||||
const api = useApi()
|
||||
const response = await api.get("/admin/batch", adminReqConfig)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.batch_jobs.length).toEqual(3)
|
||||
expect(response.data).toMatchSnapshot({
|
||||
batch_jobs: [
|
||||
{
|
||||
created_at: expect.any(String),
|
||||
updated_at: expect.any(String),
|
||||
},
|
||||
{
|
||||
created_at: expect.any(String),
|
||||
updated_at: expect.any(String),
|
||||
},
|
||||
{
|
||||
created_at: expect.any(String),
|
||||
updated_at: expect.any(String),
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from "./simple-payment-factory"
|
||||
export * from "./simple-batch-job-factory"
|
||||
export * from "./simple-order-factory"
|
||||
export * from "./simple-cart-factory"
|
||||
export * from "./simple-region-factory"
|
||||
|
||||
27
integration-tests/api/factories/simple-batch-job-factory.ts
Normal file
27
integration-tests/api/factories/simple-batch-job-factory.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Connection } from "typeorm"
|
||||
import { BatchJob, BatchJobStatus } from "@medusajs/medusa"
|
||||
|
||||
export type BatchJobFactoryData = {
|
||||
id?: string
|
||||
type?: string
|
||||
status?: BatchJobStatus
|
||||
created_by?: string
|
||||
context?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export const simpleBatchJobFactory = async (
|
||||
connection: Connection,
|
||||
data: BatchJobFactoryData = {}
|
||||
): Promise<BatchJob> => {
|
||||
const manager = connection.manager
|
||||
|
||||
const job = manager.create(BatchJob, {
|
||||
id: data.id,
|
||||
status: data.status ?? BatchJobStatus.CREATED,
|
||||
type: data.type ?? "test-job",
|
||||
created_by: data.created_by ?? null,
|
||||
context: data.context ?? {},
|
||||
})
|
||||
|
||||
return await manager.save(job)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import BatchJobService from "../../../services/batch-job"
|
||||
|
||||
export async function canAccessBatchJob(req, res, next) {
|
||||
const id = req.params.id
|
||||
const batchJobService: BatchJobService = req.scope.resolve("batchJobService")
|
||||
const batch_job = req.batch_job ?? (await batchJobService.retrieve(id))
|
||||
|
||||
const userId = req.user.id ?? req.user.userId
|
||||
if (batch_job.created_by !== userId) {
|
||||
return next(
|
||||
new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Cannot access a batch job that does not belong to the logged in user"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import BatchJobService from "../../../services/batch-job"
|
||||
|
||||
export async function getRequestedBatchJob(req, res, next) {
|
||||
const id = req.params.id
|
||||
const batchJobService: BatchJobService = req.scope.resolve("batchJobService")
|
||||
|
||||
try {
|
||||
req.batch_job = await batchJobService.retrieve(id)
|
||||
} catch (error) {
|
||||
return next(error)
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
@@ -3,6 +3,9 @@ import { default as authenticate } from "./authenticate"
|
||||
import { default as normalizeQuery } from "./normalized-query"
|
||||
import { default as wrap } from "./await-middleware"
|
||||
|
||||
export { getRequestedBatchJob } from "./batch-job/get-requested-batch-job"
|
||||
export { canAccessBatchJob } from "./batch-job/can-access-batch-job"
|
||||
|
||||
export default {
|
||||
authenticate,
|
||||
authenticateCustomer,
|
||||
|
||||
41
packages/medusa/src/api/routes/admin/batch/index.ts
Normal file
41
packages/medusa/src/api/routes/admin/batch/index.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Router } from "express"
|
||||
import { BatchJob } from "../../../.."
|
||||
import { DeleteResponse, PaginatedResponse } from "../../../../types/common"
|
||||
import middlewares from "../../../middlewares"
|
||||
|
||||
export default (app) => {
|
||||
const route = Router()
|
||||
|
||||
app.use("/batch", route)
|
||||
|
||||
route.get(
|
||||
"/",
|
||||
middlewares.normalizeQuery(),
|
||||
middlewares.wrap(require("./list-batch-jobs").default)
|
||||
)
|
||||
return app
|
||||
}
|
||||
|
||||
export type AdminBatchJobRes = {
|
||||
batch_job: BatchJob
|
||||
}
|
||||
|
||||
export type AdminBatchJobDeleteRes = DeleteResponse
|
||||
|
||||
export type AdminBatchJobListRes = PaginatedResponse & {
|
||||
batch_jobs: BatchJob[]
|
||||
}
|
||||
|
||||
export const defaultAdminBatchFields = [
|
||||
"id",
|
||||
"status",
|
||||
"type",
|
||||
"context",
|
||||
"result",
|
||||
"created_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"deleted_at",
|
||||
]
|
||||
|
||||
export * from "./list-batch-jobs"
|
||||
142
packages/medusa/src/api/routes/admin/batch/list-batch-jobs.ts
Normal file
142
packages/medusa/src/api/routes/admin/batch/list-batch-jobs.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { Type } from "class-transformer"
|
||||
import {
|
||||
IsArray,
|
||||
IsEnum,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from "class-validator"
|
||||
import { pickBy, omit, identity } from "lodash"
|
||||
import { defaultAdminBatchFields } from "."
|
||||
import BatchJobService from "../../../../services/batch-job"
|
||||
import { BatchJob } from "../../../../models"
|
||||
import { BatchJobStatus } from "../../../../types/batch-job"
|
||||
import { DateComparisonOperator } from "../../../../types/common"
|
||||
import { IsType } from "../../../../utils/validators/is-type"
|
||||
import { getListConfig } from "../../../../utils/get-query-config"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
|
||||
/**
|
||||
* @oas [get] /batch
|
||||
* operationId: "GetBatch"
|
||||
* summary: "List Batch Jobs"
|
||||
* description: "Retrieve a list of Batch Jobs."
|
||||
* x-authenticated: true
|
||||
* parameters:
|
||||
* - (query) limit {string} The number of collections to return.
|
||||
* - (query) offset {string} The offset of collections to return.
|
||||
* - (query) type {string | string[]} Filter by the batch type
|
||||
* - (query) status {string} Filter by the status of the batch operation
|
||||
* - (query) order {string} Order used when retrieving batch jobs
|
||||
* - (query) expand[] {string} (Comma separated) Which fields should be expanded in each order of the result.
|
||||
* - (query) fields[] {string} (Comma separated) Which fields should be included in each order of the result.
|
||||
* - (query) deleted_at {DateComparisonOperator} Date comparison for when resulting collections was deleted, i.e. less than, greater than etc.
|
||||
* - (query) created_at {DateComparisonOperator} Date comparison for when resulting collections was created, i.e. less than, greater than etc.
|
||||
* - (query) updated_at {DateComparisonOperator} Date comparison for when resulting collections was updated, i.e. less than, greater than etc.
|
||||
* tags:
|
||||
* - Batch Job
|
||||
* responses:
|
||||
* "200":
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* batch_job:
|
||||
* $ref: "#/components/schemas/batch_job"
|
||||
*/
|
||||
export default async (req, res) => {
|
||||
const { fields, expand, order, limit, offset, ...filterableFields } =
|
||||
await validator(AdminGetBatchParams, req.query)
|
||||
|
||||
const batchService: BatchJobService = req.scope.resolve("batchJobService")
|
||||
|
||||
let orderBy: { [k: symbol]: "DESC" | "ASC" } | undefined
|
||||
if (typeof order !== "undefined") {
|
||||
if (order.startsWith("-")) {
|
||||
const [, field] = order.split("-")
|
||||
orderBy = { [field]: "DESC" }
|
||||
} else {
|
||||
orderBy = { [order]: "ASC" }
|
||||
}
|
||||
}
|
||||
|
||||
const listConfig = getListConfig<BatchJob>(
|
||||
defaultAdminBatchFields as (keyof BatchJob)[],
|
||||
[],
|
||||
fields?.split(",") as (keyof BatchJob)[],
|
||||
expand?.split(","),
|
||||
limit,
|
||||
offset,
|
||||
orderBy
|
||||
)
|
||||
|
||||
const created_by: string = req.user.id || req.user.userId
|
||||
|
||||
const [jobs, count] = await batchService.listAndCount(
|
||||
pickBy(
|
||||
{ created_by, ...filterableFields },
|
||||
(val) => typeof val !== "undefined"
|
||||
),
|
||||
listConfig
|
||||
)
|
||||
|
||||
res.status(200).json({
|
||||
batch_jobs: jobs,
|
||||
count,
|
||||
offset: offset,
|
||||
limit: limit,
|
||||
})
|
||||
}
|
||||
|
||||
export class AdminGetBatchPaginationParams {
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
limit = 10
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
offset = 0
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
expand?: string
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
fields?: string
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
order?: string
|
||||
}
|
||||
|
||||
export class AdminGetBatchParams extends AdminGetBatchPaginationParams {
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsType([String, [String]])
|
||||
id?: string | string[]
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsEnum(BatchJobStatus, { each: true })
|
||||
status?: BatchJobStatus[]
|
||||
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
type?: string[]
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => DateComparisonOperator)
|
||||
created_at?: DateComparisonOperator
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => DateComparisonOperator)
|
||||
updated_at?: DateComparisonOperator
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import noteRoutes from "./notes"
|
||||
import notificationRoutes from "./notifications"
|
||||
import orderRoutes from "./orders"
|
||||
import priceListRoutes from "./price-lists"
|
||||
import batchRoutes from "./batch"
|
||||
import productTagRoutes from "./product-tags"
|
||||
import productTypesRoutes from "./product-types"
|
||||
import productRoutes from "./products"
|
||||
@@ -63,6 +64,7 @@ export default (app, container, config) => {
|
||||
|
||||
appRoutes(route)
|
||||
productRoutes(route)
|
||||
batchRoutes(route)
|
||||
userRoutes(route)
|
||||
regionRoutes(route)
|
||||
shippingOptionRoutes(route)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from "./api"
|
||||
export * from "./interfaces"
|
||||
export * from "./types/price-list"
|
||||
export * from "./types/batch-job"
|
||||
export * from "./models"
|
||||
export * from "./services"
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
export * from "./shipping-tax-rate"
|
||||
export * from "./product-tax-rate"
|
||||
export * from "./product-type-tax-rate"
|
||||
export * from "./tax-rate"
|
||||
export * from "./shipping-method-tax-line"
|
||||
export * from "./line-item-tax-line"
|
||||
export * from "./line-item-adjustment"
|
||||
export * from "./address"
|
||||
export * from "./batch-job"
|
||||
export * from "./cart"
|
||||
export * from "./claim-image"
|
||||
export * from "./claim-item"
|
||||
@@ -17,12 +11,12 @@ export * from "./custom-shipping-option"
|
||||
export * from "./customer"
|
||||
export * from "./customer-group"
|
||||
export * from "./discount"
|
||||
export * from "./discount-condition"
|
||||
export * from "./discount-condition-customer-group"
|
||||
export * from "./discount-condition-product"
|
||||
export * from "./discount-condition-product-collection"
|
||||
export * from "./discount-condition-product-tag"
|
||||
export * from "./discount-condition-product-type"
|
||||
export * from "./discount-condition-product"
|
||||
export * from "./discount-condition"
|
||||
export * from "./discount-rule"
|
||||
export * from "./draft-order"
|
||||
export * from "./fulfillment"
|
||||
@@ -34,6 +28,8 @@ export * from "./idempotency-key"
|
||||
export * from "./image"
|
||||
export * from "./invite"
|
||||
export * from "./line-item"
|
||||
export * from "./line-item-adjustment"
|
||||
export * from "./line-item-tax-line"
|
||||
export * from "./money-amount"
|
||||
export * from "./note"
|
||||
export * from "./notification"
|
||||
@@ -48,7 +44,9 @@ export * from "./product-collection"
|
||||
export * from "./product-option"
|
||||
export * from "./product-option-value"
|
||||
export * from "./product-tag"
|
||||
export * from "./product-tax-rate"
|
||||
export * from "./product-type"
|
||||
export * from "./product-type-tax-rate"
|
||||
export * from "./product-variant"
|
||||
export * from "./refund"
|
||||
export * from "./region"
|
||||
@@ -56,11 +54,14 @@ export * from "./return"
|
||||
export * from "./return-item"
|
||||
export * from "./return-reason"
|
||||
export * from "./shipping-method"
|
||||
export * from "./shipping-method-tax-line"
|
||||
export * from "./shipping-option"
|
||||
export * from "./shipping-option-requirement"
|
||||
export * from "./shipping-profile"
|
||||
export * from "./shipping-tax-rate"
|
||||
export * from "./staged-job"
|
||||
export * from "./store"
|
||||
export * from "./swap"
|
||||
export * from "./user"
|
||||
export * from "./tax-provider"
|
||||
export * from "./tax-rate"
|
||||
export * from "./user"
|
||||
|
||||
5
packages/medusa/src/repositories/batch-job.ts
Normal file
5
packages/medusa/src/repositories/batch-job.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { EntityRepository, Repository } from "typeorm"
|
||||
import { BatchJob } from "../models"
|
||||
|
||||
@EntityRepository(BatchJob)
|
||||
export class BatchJobRepository extends Repository<BatchJob> {}
|
||||
76
packages/medusa/src/services/batch-job.ts
Normal file
76
packages/medusa/src/services/batch-job.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { EntityManager } from "typeorm"
|
||||
|
||||
import { BatchJob } from "../models"
|
||||
import { BatchJobRepository } from "../repositories/batch-job"
|
||||
import { FilterableBatchJobProps } from "../types/batch-job"
|
||||
import { FindConfig } from "../types/common"
|
||||
import { TransactionBaseService } from "../interfaces"
|
||||
import { buildQuery, validateId } from "../utils"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
|
||||
type InjectedDependencies = {
|
||||
manager: EntityManager
|
||||
batchJobRepository: typeof BatchJobRepository
|
||||
}
|
||||
|
||||
class BatchJobService extends TransactionBaseService<BatchJobService> {
|
||||
protected manager_: EntityManager
|
||||
protected transactionManager_: EntityManager | undefined
|
||||
protected readonly batchJobRepository_: typeof BatchJobRepository
|
||||
|
||||
static readonly Events = {
|
||||
CREATED: "batch.created",
|
||||
UPDATED: "batch.updated",
|
||||
CANCELED: "batch.canceled",
|
||||
}
|
||||
|
||||
constructor({ manager, batchJobRepository }: InjectedDependencies) {
|
||||
super({ manager, batchJobRepository })
|
||||
|
||||
this.manager_ = manager
|
||||
this.batchJobRepository_ = batchJobRepository
|
||||
}
|
||||
|
||||
async retrieve(
|
||||
batchJobId: string,
|
||||
config: FindConfig<BatchJob> = {}
|
||||
): Promise<BatchJob | never> {
|
||||
return await this.atomicPhase_(
|
||||
async (transactionManager: EntityManager) => {
|
||||
const batchJobRepo = transactionManager.getCustomRepository(
|
||||
this.batchJobRepository_
|
||||
)
|
||||
|
||||
const query = buildQuery<BatchJob>({ id: batchJobId }, config)
|
||||
const batchJob = await batchJobRepo.findOne(query)
|
||||
|
||||
if (!batchJob) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Batch job with id ${batchJobId} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
return batchJob
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async listAndCount(
|
||||
selector: FilterableBatchJobProps = {},
|
||||
config: FindConfig<BatchJob> = { skip: 0, take: 20 }
|
||||
): Promise<[BatchJob[], number]> {
|
||||
return await this.atomicPhase_(
|
||||
async (manager: EntityManager): Promise<[BatchJob[], number]> => {
|
||||
const batchJobRepo = manager.getCustomRepository(
|
||||
this.batchJobRepository_
|
||||
)
|
||||
|
||||
const query = buildQuery(selector, config)
|
||||
return await batchJobRepo.findAndCount(query)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default BatchJobService
|
||||
@@ -1,6 +1,46 @@
|
||||
import { Type } from "class-transformer"
|
||||
import {
|
||||
IsArray,
|
||||
IsEnum,
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from "class-validator"
|
||||
import { IsType } from "../utils/validators/is-type"
|
||||
import { DateComparisonOperator } from "./common"
|
||||
|
||||
export enum BatchJobStatus {
|
||||
CREATED = "created",
|
||||
PROCESSING = "processing",
|
||||
AWAITING_CONFIRMATION = "awaiting_confirmation",
|
||||
COMPLETED = "completed",
|
||||
}
|
||||
|
||||
export class FilterableBatchJobProps {
|
||||
@IsOptional()
|
||||
@IsType([String, [String]])
|
||||
id?: string | string[]
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(BatchJobStatus, { each: true })
|
||||
status?: BatchJobStatus[]
|
||||
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
type?: string[]
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@IsType([String, [String]])
|
||||
created_by?: string | string[]
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => DateComparisonOperator)
|
||||
created_at?: DateComparisonOperator
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => DateComparisonOperator)
|
||||
updated_at?: DateComparisonOperator
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user