Feat: Add product tag endpoints, move tests to HTTP folder (#7591)

* chore: Move product type tests to HTTP folder

* feat: Add product tags endpoints and move tests to HTTP folder
This commit is contained in:
Stevche Radevski
2024-06-04 10:56:22 +02:00
committed by GitHub
parent ce40fe88f5
commit 0929c4f457
24 changed files with 805 additions and 399 deletions

View File

@@ -1,178 +0,0 @@
const path = require("path")
const { IdMap } = require("medusa-test-utils")
const setupServer = require("../../../environment-helpers/setup-server")
const { useApi } = require("../../../environment-helpers/use-api")
const { initDb, useDb } = require("../../../environment-helpers/use-db")
const adminSeeder = require("../../../helpers/admin-seeder")
const productSeeder = require("../../../helpers/product-seeder")
const {
DiscountConditionType,
DiscountConditionOperator,
} = require("@medusajs/medusa")
const { simpleDiscountFactory } = require("../../../factories")
const { DiscountRuleType, AllocationType } = require("@medusajs/medusa/dist")
jest.setTimeout(50000)
const adminReqConfig = {
headers: {
"x-medusa-access-token": "test_token",
},
}
describe("/admin/product-tags", () => {
let medusaProcess
let dbConnection
beforeAll(async () => {
const cwd = path.resolve(path.join(__dirname, "..", ".."))
dbConnection = await initDb({ cwd })
medusaProcess = await setupServer({ cwd })
})
afterAll(async () => {
const db = useDb()
await db.shutdown()
medusaProcess.kill()
})
describe("GET /admin/product-tags", () => {
beforeEach(async () => {
await adminSeeder(dbConnection)
await productSeeder(dbConnection)
})
afterEach(async () => {
const db = useDb()
await db.teardown()
})
it("returns a list of product tags", async () => {
const api = useApi()
const res = await api
.get("/admin/product-tags", adminReqConfig)
.catch((err) => {
console.log(err)
})
expect(res.status).toEqual(200)
const tagMatch = {
created_at: expect.any(String),
updated_at: expect.any(String),
}
expect(res.data.product_tags).toMatchSnapshot([
tagMatch,
tagMatch,
tagMatch,
])
})
it("returns a list of product tags matching free text search param", async () => {
const api = useApi()
const res = await api
.get("/admin/product-tags?q=123", adminReqConfig)
.catch((err) => {
console.log(err)
})
expect(res.status).toEqual(200)
const tagMatch = {
created_at: expect.any(String),
updated_at: expect.any(String),
}
expect(res.data.product_tags.length).toEqual(3)
expect(res.data.product_tags.map((pt) => pt.value)).toEqual(
expect.arrayContaining(["123", "1235", "1234"])
)
expect(res.data.product_tags).toMatchSnapshot([
tagMatch,
tagMatch,
tagMatch,
])
})
it("returns a list of product tags filtered by discount condition id", async () => {
const api = useApi()
const resTags = await api.get("/admin/product-tags", adminReqConfig)
const tag1 = resTags.data.product_tags[0]
const tag2 = resTags.data.product_tags[2]
const buildDiscountData = (code, conditionId, tags) => {
return {
code,
rule: {
type: DiscountRuleType.PERCENTAGE,
value: 10,
allocation: AllocationType.TOTAL,
conditions: [
{
id: conditionId,
type: DiscountConditionType.PRODUCT_TAGS,
operator: DiscountConditionOperator.IN,
product_tags: tags,
},
],
},
}
}
const discountConditionId = IdMap.getId("discount-condition-tag-1")
await simpleDiscountFactory(
dbConnection,
buildDiscountData("code-1", discountConditionId, [tag1.id])
)
const discountConditionId2 = IdMap.getId("discount-condition-tag-2")
await simpleDiscountFactory(
dbConnection,
buildDiscountData("code-2", discountConditionId2, [tag2.id])
)
let res = await api.get(
`/admin/product-tags?discount_condition_id=${discountConditionId}`,
adminReqConfig
)
expect(res.status).toEqual(200)
expect(res.data.product_tags).toHaveLength(1)
expect(res.data.product_tags).toEqual(
expect.arrayContaining([expect.objectContaining({ id: tag1.id })])
)
res = await api.get(
`/admin/product-tags?discount_condition_id=${discountConditionId2}`,
adminReqConfig
)
expect(res.status).toEqual(200)
expect(res.data.product_tags).toHaveLength(1)
expect(res.data.product_tags).toEqual(
expect.arrayContaining([expect.objectContaining({ id: tag2.id })])
)
res = await api.get(`/admin/product-tags`, adminReqConfig)
expect(res.status).toEqual(200)
expect(res.data.product_tags).toHaveLength(3)
expect(res.data.product_tags).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: tag1.id }),
expect.objectContaining({ id: tag2.id }),
])
)
})
})
})

View File

@@ -1,167 +0,0 @@
const path = require("path")
const { IdMap } = require("medusa-test-utils")
const setupServer = require("../../../environment-helpers/setup-server")
const { useApi } = require("../../../environment-helpers/use-api")
const { initDb, useDb } = require("../../../environment-helpers/use-db")
const adminSeeder = require("../../../helpers/admin-seeder")
const productSeeder = require("../../../helpers/product-seeder")
const {
DiscountRuleType,
AllocationType,
DiscountConditionType,
DiscountConditionOperator,
} = require("@medusajs/medusa")
const { simpleDiscountFactory } = require("../../../factories")
jest.setTimeout(50000)
const adminReqConfig = {
headers: {
"x-medusa-access-token": "test_token",
},
}
describe("/admin/product-types", () => {
let medusaProcess
let dbConnection
beforeAll(async () => {
const cwd = path.resolve(path.join(__dirname, "..", ".."))
dbConnection = await initDb({ cwd })
medusaProcess = await setupServer({ cwd })
})
afterAll(async () => {
const db = useDb()
await db.shutdown()
medusaProcess.kill()
})
describe("GET /admin/product-types", () => {
beforeEach(async () => {
await productSeeder(dbConnection)
await adminSeeder(dbConnection)
})
afterEach(async () => {
const db = useDb()
await db.teardown()
})
it("returns a list of product types", async () => {
const api = useApi()
const res = await api.get("/admin/product-types", adminReqConfig)
expect(res.status).toEqual(200)
const typeMatch = {
created_at: expect.any(String),
updated_at: expect.any(String),
}
expect(res.data.product_types).toMatchSnapshot([typeMatch, typeMatch])
})
it("returns a list of product types matching free text search param", async () => {
const api = useApi()
const res = await api.get(
"/admin/product-types?q=test-type-new",
adminReqConfig
)
expect(res.status).toEqual(200)
const typeMatch = {
created_at: expect.any(String),
updated_at: expect.any(String),
}
// The value of the type should match the search param
expect(res.data.product_types.map((pt) => pt.value)).toEqual([
"test-type-new",
])
// Should only return one type as there is only one match to the search param
expect(res.data.product_types).toMatchSnapshot([typeMatch])
})
it("returns a list of product type filtered by discount condition id", async () => {
const api = useApi()
const resTypes = await api.get("/admin/product-types", adminReqConfig)
const type1 = resTypes.data.product_types[0]
const type2 = resTypes.data.product_types[1]
const buildDiscountData = (code, conditionId, types) => {
return {
code,
rule: {
type: DiscountRuleType.PERCENTAGE,
value: 10,
allocation: AllocationType.TOTAL,
conditions: [
{
id: conditionId,
type: DiscountConditionType.PRODUCT_TYPES,
operator: DiscountConditionOperator.IN,
product_types: types,
},
],
},
}
}
const discountConditionId = IdMap.getId("discount-condition-type-1")
await simpleDiscountFactory(
dbConnection,
buildDiscountData("code-1", discountConditionId, [type1.id])
)
const discountConditionId2 = IdMap.getId("discount-condition-type-2")
await simpleDiscountFactory(
dbConnection,
buildDiscountData("code-2", discountConditionId2, [type2.id])
)
let res = await api.get(
`/admin/product-types?discount_condition_id=${discountConditionId}`,
adminReqConfig
)
expect(res.status).toEqual(200)
expect(res.data.product_types).toHaveLength(1)
expect(res.data.product_types).toEqual(
expect.arrayContaining([expect.objectContaining({ id: type1.id })])
)
res = await api.get(
`/admin/product-types?discount_condition_id=${discountConditionId2}`,
adminReqConfig
)
expect(res.status).toEqual(200)
expect(res.data.product_types).toHaveLength(1)
expect(res.data.product_types).toEqual(
expect.arrayContaining([expect.objectContaining({ id: type2.id })])
)
res = await api.get(`/admin/product-types`, adminReqConfig)
expect(res.status).toEqual(200)
expect(res.data.product_types).toHaveLength(2)
expect(res.data.product_types).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: type1.id }),
expect.objectContaining({ id: type2.id }),
])
)
})
})
})

View File

@@ -0,0 +1,69 @@
import { medusaIntegrationTestRunner } from "medusa-test-utils"
import {
createAdminUser,
adminHeaders,
} from "../../../../helpers/create-admin-user"
jest.setTimeout(30000)
medusaIntegrationTestRunner({
env: {},
testSuite: ({ dbConnection, getContainer, api }) => {
let tag1
let tag2
beforeEach(async () => {
const container = getContainer()
await createAdminUser(dbConnection, adminHeaders, container)
tag1 = (
await api.post(
"/admin/product-tags",
{
value: "test1",
},
adminHeaders
)
).data.product_tag
tag2 = (
await api.post(
"/admin/product-tags",
{
value: "test2",
},
adminHeaders
)
).data.product_tag
})
describe("GET /admin/product-tags", () => {
it("returns a list of product tags", async () => {
const res = await api.get("/admin/product-tags", adminHeaders)
expect(res.status).toEqual(200)
expect(res.data.product_tags).toEqual(
expect.arrayContaining([
expect.objectContaining({
value: "test1",
}),
expect.objectContaining({
value: "test2",
}),
])
)
})
it("returns a list of product tags matching free text search param", async () => {
const res = await api.get("/admin/product-tags?q=1", adminHeaders)
expect(res.status).toEqual(200)
expect(res.data.product_tags.length).toEqual(1)
expect(res.data.product_tags).toEqual(
expect.arrayContaining([expect.objectContaining({ value: "test1" })])
)
})
})
// BREAKING: Removed a test that filtered tags by discount condition id, which is no longer supported
},
})

View File

@@ -0,0 +1,73 @@
import { medusaIntegrationTestRunner } from "medusa-test-utils"
import {
createAdminUser,
adminHeaders,
} from "../../../../helpers/create-admin-user"
jest.setTimeout(30000)
medusaIntegrationTestRunner({
env: {},
testSuite: ({ dbConnection, getContainer, api }) => {
let type1
let type2
beforeEach(async () => {
const container = getContainer()
await createAdminUser(dbConnection, adminHeaders, container)
type1 = (
await api.post(
"/admin/product-types",
{
value: "test1",
},
adminHeaders
)
).data.product_type
type2 = (
await api.post(
"/admin/product-types",
{
value: "test2",
},
adminHeaders
)
).data.product_type
})
describe("/admin/product-types", () => {
it("returns a list of product types", async () => {
const res = await api.get("/admin/product-types", adminHeaders)
expect(res.status).toEqual(200)
expect(res.data.product_types).toEqual(
expect.arrayContaining([
expect.objectContaining({
value: "test1",
}),
expect.objectContaining({
value: "test2",
}),
])
)
})
it("returns a list of product types matching free text search param", async () => {
const res = await api.get("/admin/product-types?q=test1", adminHeaders)
expect(res.status).toEqual(200)
// The value of the type should match the search param
expect(res.data.product_types).toEqual([
expect.objectContaining({
value: "test1",
}),
])
})
// BREAKING: Removed a test around filtering based on discount condition id, which is no longer supported
})
},
})

View File

@@ -0,0 +1,30 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IProductModuleService, ProductTypes } from "@medusajs/types"
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
export const createProductTagsStepId = "create-product-tags"
export const createProductTagsStep = createStep(
createProductTagsStepId,
async (data: ProductTypes.CreateProductTagDTO[], { container }) => {
const service = container.resolve<IProductModuleService>(
ModuleRegistrationName.PRODUCT
)
const created = await service.createTags(data)
return new StepResponse(
created,
created.map((productTag) => productTag.id)
)
},
async (createdIds, { container }) => {
if (!createdIds?.length) {
return
}
const service = container.resolve<IProductModuleService>(
ModuleRegistrationName.PRODUCT
)
await service.deleteTags(createdIds)
}
)

View File

@@ -0,0 +1,27 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IProductModuleService } from "@medusajs/types"
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
export const deleteProductTagsStepId = "delete-product-tags"
export const deleteProductTagsStep = createStep(
deleteProductTagsStepId,
async (ids: string[], { container }) => {
const service = container.resolve<IProductModuleService>(
ModuleRegistrationName.PRODUCT
)
await service.softDeleteTags(ids)
return new StepResponse(void 0, ids)
},
async (prevIds, { container }) => {
if (!prevIds?.length) {
return
}
const service = container.resolve<IProductModuleService>(
ModuleRegistrationName.PRODUCT
)
await service.restoreTags(prevIds)
}
)

View File

@@ -16,3 +16,6 @@ export * from "./batch-link-products-collection"
export * from "./create-product-types"
export * from "./update-product-types"
export * from "./delete-product-types"
export * from "./create-product-tags"
export * from "./update-product-tags"
export * from "./delete-product-tags"

View File

@@ -0,0 +1,42 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IProductModuleService, ProductTypes } from "@medusajs/types"
import { getSelectsAndRelationsFromObjectArray } from "@medusajs/utils"
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
type UpdateProductTagsStepInput = {
selector: ProductTypes.FilterableProductTagProps
update: ProductTypes.UpdateProductTagDTO
}
export const updateProductTagsStepId = "update-product-tags"
export const updateProductTagsStep = createStep(
updateProductTagsStepId,
async (data: UpdateProductTagsStepInput, { container }) => {
const service = container.resolve<IProductModuleService>(
ModuleRegistrationName.PRODUCT
)
const { selects, relations } = getSelectsAndRelationsFromObjectArray([
data.update,
])
const prevData = await service.listTags(data.selector, {
select: selects,
relations,
})
const productTags = await service.updateTags(data.selector, data.update)
return new StepResponse(productTags, prevData)
},
async (prevData, { container }) => {
if (!prevData?.length) {
return
}
const service = container.resolve<IProductModuleService>(
ModuleRegistrationName.PRODUCT
)
await service.upsertTags(prevData)
}
)

View File

@@ -0,0 +1,15 @@
import { ProductTypes } from "@medusajs/types"
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
import { createProductTagsStep } from "../steps"
type WorkflowInput = { product_tags: ProductTypes.CreateProductTagDTO[] }
export const createProductTagsWorkflowId = "create-product-tags"
export const createProductTagsWorkflow = createWorkflow(
createProductTagsWorkflowId,
(
input: WorkflowData<WorkflowInput>
): WorkflowData<ProductTypes.ProductTagDTO[]> => {
return createProductTagsStep(input.product_tags)
}
)

View File

@@ -0,0 +1,12 @@
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
import { deleteProductTagsStep } from "../steps"
type WorkflowInput = { ids: string[] }
export const deleteProductTagsWorkflowId = "delete-product-tags"
export const deleteProductTagsWorkflow = createWorkflow(
deleteProductTagsWorkflowId,
(input: WorkflowData<WorkflowInput>): WorkflowData<void> => {
return deleteProductTagsStep(input.ids)
}
)

View File

@@ -5,15 +5,18 @@ export * from "./batch-products-in-category"
export * from "./create-collections"
export * from "./create-product-options"
export * from "./create-product-types"
export * from "./create-product-tags"
export * from "./create-product-variants"
export * from "./create-products"
export * from "./delete-collections"
export * from "./delete-product-options"
export * from "./delete-product-types"
export * from "./delete-product-tags"
export * from "./delete-product-variants"
export * from "./delete-products"
export * from "./update-collections"
export * from "./update-product-options"
export * from "./update-product-types"
export * from "./update-product-tags"
export * from "./update-product-variants"
export * from "./update-products"

View File

@@ -0,0 +1,20 @@
import { ProductTypes } from "@medusajs/types"
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
import { updateProductTagsStep } from "../steps"
type UpdateProductTagsStepInput = {
selector: ProductTypes.FilterableProductTypeProps
update: ProductTypes.UpdateProductTypeDTO
}
type WorkflowInput = UpdateProductTagsStepInput
export const updateProductTagsWorkflowId = "update-product-tags"
export const updateProductTagsWorkflow = createWorkflow(
updateProductTagsWorkflowId,
(
input: WorkflowData<WorkflowInput>
): WorkflowData<ProductTypes.ProductTagDTO[]> => {
return updateProductTagsStep(input)
}
)

View File

@@ -31,6 +31,7 @@ import {
UpsertProductCollectionDTO,
UpsertProductDTO,
UpsertProductOptionDTO,
UpsertProductTagDTO,
UpsertProductTypeDTO,
UpsertProductVariantDTO,
} from "./common"
@@ -512,19 +513,16 @@ export interface IProductModuleService extends IModuleService {
): Promise<[ProductTagDTO[], number]>
/**
* This method is used to create product tags.
* This method is used to create a product tag.
*
* @param {CreateProductTagDTO[]} data - The product tags to create.
* @param {CreateProductTagDTO[]} data - The product tags to be created.
* @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
* @returns {Promise<ProductTagDTO[]>} The list of product tags.
* @return {Promise<ProductTagDTO[]>} The list of created product tags.
*
* @example
* const tags = await productModuleService.createTags([
* const productTags = await productModuleService.createTags([
* {
* value: "Clothes",
* },
* {
* value: "Accessories",
* value: "digital",
* },
* ])
*/
@@ -534,29 +532,111 @@ export interface IProductModuleService extends IModuleService {
): Promise<ProductTagDTO[]>
/**
* This method is used to update existing product tags.
* This method is used to create a product tag.
*
* @param {UpdateProductTagDTO[]} data - The product tags to be updated, each having the attributes that should be updated in a product tag.
* @param {CreateProductTagDTO} data - The product tag to be created.
* @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
* @returns {Promise<ProductTagDTO[]>} The list of updated product tags.
* @returns {Promise<ProductTagDTO>} The created product tag.
*
* @example
* const productTags = await productModule.updateTags([
* const productTag = await productModuleService.createTags({
* value: "digital",
* })
*
*/
createTags(
data: CreateProductTagDTO,
sharedContext?: Context
): Promise<ProductTagDTO>
/**
* This method updates existing tags, or creates new ones if they don't exist.
*
* @param {UpsertProductTagDTO[]} data - The attributes to update or create for each tag.
* @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
* @returns {Promise<ProductTagDTO[]>} The updated and created tags.
*
* @example
* const productTags = await productModuleService.upsertTags([
* {
* id,
* value
* }
* id: "ptag_123",
* metadata: {
* test: true,
* },
* },
* {
* value: "Digital",
* },
* ])
*/
upsertTags(
data: UpsertProductTagDTO[],
sharedContext?: Context
): Promise<ProductTagDTO[]>
/**
* This method updates an existing tag, or creates a new one if it doesn't exist.
*
* @ignore
* @param {UpsertProductTagDTO} data - The attributes to update or create for the tag.
* @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
* @returns {Promise<ProductTagDTO>} The updated or created tag.
*
* @privateRemarks
* This method needs an update as it doesn't allow passing an ID of the tag to update
* So, for now, we've added the `@\ignore` tag to not show it in the generated docs.
* Once fixed, the `@\ignore` tag (and this comment) can be removed safely.
* @example
* const productTag = await productModuleService.upsertTags({
* id: "ptag_123",
* metadata: {
* test: true,
* },
* })
*/
upsertTags(
data: UpsertProductTagDTO,
sharedContext?: Context
): Promise<ProductTagDTO>
/**
* This method is used to update a tag.
*
* @param {string} id - The ID of the tag to be updated.
* @param {UpdateProductTagDTO} data - The attributes of the tag to be updated
* @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
* @returns {Promise<ProductTagDTO>} The updated tag.
*
* @example
* const productTag = await productModuleService.updateTags(
* "ptag_123",
* {
* value: "Digital",
* }
* )
*/
updateTags(
data: UpdateProductTagDTO[],
id: string,
data: UpdateProductTagDTO,
sharedContext?: Context
): Promise<ProductTagDTO>
/**
* This method is used to update a list of tags matching the specified filters.
*
* @param {FilterableProductTagProps} selector - The filters specifying which tags to update.
* @param {UpdateProductTagDTO} data - The attributes to be updated on the selected tags
* @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
* @returns {Promise<ProductTagDTO[]>} The updated tags.
*
* @example
* const productTags = await productModuleService.updateTags(
* {
* id: ["ptag_123", "ptag_321"],
* },
* {
* value: "Digital",
* }
* )
*/
updateTags(
selector: FilterableProductTagProps,
data: UpdateProductTagDTO,
sharedContext?: Context
): Promise<ProductTagDTO[]>
@@ -575,6 +655,58 @@ export interface IProductModuleService extends IModuleService {
*/
deleteTags(productTagIds: string[], sharedContext?: Context): Promise<void>
/**
* This method is used to delete tags. Unlike the {@link delete} method, this method won't completely remove the tag. It can still be accessed or retrieved using methods like {@link retrieve} if you pass the `withDeleted` property to the `config` object parameter.
*
* The soft-deleted tags can be restored using the {@link restore} method.
*
* @param {string[]} tagIds - The IDs of the tags to soft-delete.
* @param {SoftDeleteReturn<TReturnableLinkableKeys>} config -
* Configurations determining which relations to soft delete along with the each of the tags. You can pass to its `returnLinkableKeys`
* property any of the tag's relation attribute names.
* @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
* @returns {Promise<Record<string, string[]> | void>}
* An object that includes the IDs of related records that were also soft deleted. The object's keys are the ID attribute names of the tag entity's relations, and its value is an array of strings, each being the ID of a record associated with the tag through this relation.
*
* If there are no related records, the promise resolved to `void`.
*
* @example
* await productModuleService.softDeleteTags([
* "ptag_123",
* "ptag_321",
* ])
*/
softDeleteTags<TReturnableLinkableKeys extends string = string>(
tagIds: string[],
config?: SoftDeleteReturn<TReturnableLinkableKeys>,
sharedContext?: Context
): Promise<Record<string, string[]> | void>
/**
* This method is used to restore tags which were deleted using the {@link softDelete} method.
*
* @param {string[]} tagIds - The IDs of the tags to restore.
* @param {RestoreReturn<TReturnableLinkableKeys>} config -
* Configurations determining which relations to restore along with each of the tags. You can pass to its `returnLinkableKeys`
* property any of the tag's relation attribute names.
* @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
* @returns {Promise<Record<string, string[]> | void>}
* An object that includes the IDs of related records that were restored. The object's keys are the ID attribute names of the tag entity's relations, and its value is an array of strings, each being the ID of the record associated with the tag through this relation.
*
* If there are no related records that were restored, the promise resolved to `void`.
*
* @example
* await productModuleService.restoreTags([
* "ptag_123",
* "ptag_321",
* ])
*/
restoreTags<TReturnableLinkableKeys extends string = string>(
tagIds: string[],
config?: RestoreReturn<TReturnableLinkableKeys>,
sharedContext?: Context
): Promise<Record<string, string[]> | void>
/**
* This method is used to retrieve a product type by its ID.
*

View File

@@ -0,0 +1,66 @@
import {
deleteProductTagsWorkflow,
updateProductTagsWorkflow,
} from "@medusajs/core-flows"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../../types/routing"
import {
AdminGetProductTagParamsType,
AdminUpdateProductTagType,
} from "../validators"
import { refetchEntity } from "../../../utils/refetch-entity"
export const GET = async (
req: AuthenticatedMedusaRequest<AdminGetProductTagParamsType>,
res: MedusaResponse
) => {
const productTag = await refetchEntity(
"product_tag",
req.params.id,
req.scope,
req.remoteQueryConfig.fields
)
res.status(200).json({ product_tag: productTag })
}
export const POST = async (
req: AuthenticatedMedusaRequest<AdminUpdateProductTagType>,
res: MedusaResponse
) => {
const { result } = await updateProductTagsWorkflow(req.scope).run({
input: {
selector: { id: req.params.id },
update: req.validatedBody,
},
})
const productTag = await refetchEntity(
"product_tag",
result[0].id,
req.scope,
req.remoteQueryConfig.fields
)
res.status(200).json({ product_tag: productTag })
}
export const DELETE = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const id = req.params.id
await deleteProductTagsWorkflow(req.scope).run({
input: { ids: [id] },
})
res.status(200).json({
id,
object: "product_tag",
deleted: true,
})
}

View File

@@ -0,0 +1,61 @@
import * as QueryConfig from "./query-config"
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
import { validateAndTransformQuery } from "../../utils/validate-query"
import {
AdminCreateProductTag,
AdminGetProductTagParams,
AdminGetProductTagsParams,
AdminUpdateProductTag,
} from "./validators"
import { validateAndTransformBody } from "../../utils/validate-body"
export const adminProductTagRoutesMiddlewares: MiddlewareRoute[] = [
{
method: ["GET"],
matcher: "/admin/product-tags",
middlewares: [
validateAndTransformQuery(
AdminGetProductTagsParams,
QueryConfig.listProductTagsTransformQueryConfig
),
],
},
{
method: ["GET"],
matcher: "/admin/product-tags/:id",
middlewares: [
validateAndTransformQuery(
AdminGetProductTagParams,
QueryConfig.retrieveProductTagTransformQueryConfig
),
],
},
// Create/update/delete methods are new in v2
{
method: ["POST"],
matcher: "/admin/product-tags",
middlewares: [
validateAndTransformBody(AdminCreateProductTag),
validateAndTransformQuery(
AdminGetProductTagParams,
QueryConfig.retrieveProductTagTransformQueryConfig
),
],
},
{
method: ["POST"],
matcher: "/admin/product-tags/:id",
middlewares: [
validateAndTransformBody(AdminUpdateProductTag),
validateAndTransformQuery(
AdminGetProductTagParams,
QueryConfig.retrieveProductTagTransformQueryConfig
),
],
},
{
method: ["DELETE"],
matcher: "/admin/product-tags/:id",
middlewares: [],
},
]

View File

@@ -0,0 +1,17 @@
export const defaultAdminProductTagFields = [
"id",
"value",
"created_at",
"updated_at",
]
export const retrieveProductTagTransformQueryConfig = {
defaults: defaultAdminProductTagFields,
isList: false,
}
export const listProductTagsTransformQueryConfig = {
...retrieveProductTagTransformQueryConfig,
defaultLimit: 20,
isList: true,
}

View File

@@ -0,0 +1,51 @@
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../types/routing"
import { createProductTagsWorkflow } from "@medusajs/core-flows"
import {
AdminCreateProductTagType,
AdminGetProductTagsParamsType,
} from "./validators"
import { refetchEntities, refetchEntity } from "../../utils/refetch-entity"
export const GET = async (
req: AuthenticatedMedusaRequest<AdminGetProductTagsParamsType>,
res: MedusaResponse
) => {
const { rows: product_tags, metadata } = await refetchEntities(
"product_tag",
req.filterableFields,
req.scope,
req.remoteQueryConfig.fields,
req.remoteQueryConfig.pagination
)
res.json({
product_tags: product_tags,
count: metadata.count,
offset: metadata.skip,
limit: metadata.take,
})
}
export const POST = async (
req: AuthenticatedMedusaRequest<AdminCreateProductTagType>,
res: MedusaResponse
) => {
const input = [req.validatedBody]
const { result } = await createProductTagsWorkflow(req.scope).run({
input: { product_tags: input },
})
const productTag = await refetchEntity(
"product_tag",
result[0].id,
req.scope,
req.remoteQueryConfig.fields
)
res.status(200).json({ product_tag: productTag })
}

View File

@@ -0,0 +1,46 @@
import { z } from "zod"
import {
createFindParams,
createOperatorMap,
createSelectParams,
} from "../../utils/validators"
export type AdminGetProductTagParamsType = z.infer<
typeof AdminGetProductTagParams
>
export const AdminGetProductTagParams = createSelectParams()
export type AdminGetProductTagsParamsType = z.infer<
typeof AdminGetProductTagsParams
>
export const AdminGetProductTagsParams = createFindParams({
limit: 20,
offset: 0,
}).merge(
z.object({
q: z.string().optional(),
id: z.union([z.string(), z.array(z.string())]).optional(),
value: z.union([z.string(), z.array(z.string())]).optional(),
created_at: createOperatorMap().optional(),
updated_at: createOperatorMap().optional(),
deleted_at: createOperatorMap().optional(),
$and: z.lazy(() => AdminGetProductTagsParams.array()).optional(),
$or: z.lazy(() => AdminGetProductTagsParams.array()).optional(),
})
)
export type AdminCreateProductTagType = z.infer<typeof AdminCreateProductTag>
export const AdminCreateProductTag = z
.object({
value: z.string(),
metadata: z.record(z.string(), z.unknown()).optional(),
})
.strict()
export type AdminUpdateProductTagType = z.infer<typeof AdminUpdateProductTag>
export const AdminUpdateProductTag = z
.object({
value: z.string().optional(),
metadata: z.record(z.string(), z.unknown()).optional(),
})
.strict()

View File

@@ -17,6 +17,7 @@ import { adminPriceListsRoutesMiddlewares } from "./admin/price-lists/middleware
import { adminPricingRoutesMiddlewares } from "./admin/pricing/middlewares"
import { adminProductCategoryRoutesMiddlewares } from "./admin/product-categories/middlewares"
import { adminProductTypeRoutesMiddlewares } from "./admin/product-types/middlewares"
import { adminProductTagRoutesMiddlewares } from "./admin/product-tags/middlewares"
import { adminProductRoutesMiddlewares } from "./admin/products/middlewares"
import { adminPromotionRoutesMiddlewares } from "./admin/promotions/middlewares"
import { adminRegionRoutesMiddlewares } from "./admin/regions/middlewares"
@@ -86,6 +87,7 @@ export const config: MiddlewaresConfig = {
...adminSalesChannelRoutesMiddlewares,
...adminStockLocationRoutesMiddlewares,
...adminProductTypeRoutesMiddlewares,
...adminProductTagRoutesMiddlewares,
...adminUploadRoutesMiddlewares,
...adminFulfillmentSetsRoutesMiddlewares,
...adminOrderRoutesMiddlewares,

View File

@@ -24,7 +24,7 @@ export const refetchEntities = async (
delete filters.context
}
let variables = { filters, ...context, ...pagination }
const variables = { filters, ...context, ...pagination }
const queryObject = remoteQueryObjectFromString({
entryPoint,

View File

@@ -251,12 +251,9 @@ moduleIntegrationTestRunner({
const tagId = "tag-1"
it("should update the value of the tag successfully", async () => {
await service.updateTags([
{
id: tagId,
await service.updateTags(tagId, {
value: "UK",
},
])
})
const productTag = await service.retrieveTag(tagId)
@@ -267,18 +264,15 @@ moduleIntegrationTestRunner({
let error
try {
await service.updateTags([
{
id: "does-not-exist",
await service.updateTags("does-not-exist", {
value: "UK",
},
])
})
} catch (e) {
error = e
}
expect(error.message).toEqual(
'ProductTag with id "does-not-exist" not found'
"ProductTag with id: does-not-exist was not found"
)
})
})

View File

@@ -1,8 +1,7 @@
import { Modules } from "@medusajs/modules-sdk"
import { IProductModuleService } from "@medusajs/types"
import { IProductModuleService, ModulesSdkTypes } from "@medusajs/types"
import { ProductStatus } from "@medusajs/utils"
import { Product } from "@models"
import { ProductTagService } from "@services"
import { Product, ProductTag } from "@models"
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
import { createProductAndTags } from "../../../__fixtures__/product"
@@ -16,7 +15,7 @@ moduleIntegrationTestRunner({
}: SuiteOptions<IProductModuleService>) => {
describe("ProductTag Service", () => {
let data!: Product[]
let service: ProductTagService
let service: ModulesSdkTypes.InternalModuleService<ProductTag>
beforeEach(() => {
service = medusaApp.modules["productService"].productTagService_

View File

@@ -47,6 +47,7 @@ import {
UpdateProductInput,
UpdateProductOptionInput,
UpdateProductVariantInput,
UpdateTagInput,
UpdateTypeInput,
} from "../types"
import { entityNameToLinkableKeysMap, joinerConfig } from "./../joiner-config"
@@ -384,30 +385,114 @@ export default class ProductModuleService<
)
}
@InjectTransactionManager("baseRepository_")
async createTags(
createTags(
data: ProductTypes.CreateProductTagDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<ProductTypes.ProductTagDTO[]> {
const productTags = await this.productTagService_.create(
data,
sharedContext
)
sharedContext?: Context
): Promise<ProductTypes.ProductTagDTO[]>
createTags(
data: ProductTypes.CreateProductTagDTO,
sharedContext?: Context
): Promise<ProductTypes.ProductTagDTO>
return await this.baseRepository_.serialize(productTags)
@InjectManager("baseRepository_")
async createTags(
data: ProductTypes.CreateProductTagDTO[] | ProductTypes.CreateProductTagDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<ProductTypes.ProductTagDTO[] | ProductTypes.ProductTagDTO> {
const input = Array.isArray(data) ? data : [data]
const tags = await this.productTagService_.create(input, sharedContext)
const createdTags = await this.baseRepository_.serialize<
ProductTypes.ProductTagDTO[]
>(tags)
return Array.isArray(data) ? createdTags : createdTags[0]
}
async upsertTags(
data: ProductTypes.UpsertProductTagDTO[],
sharedContext?: Context
): Promise<ProductTypes.ProductTagDTO[]>
async upsertTags(
data: ProductTypes.UpsertProductTagDTO,
sharedContext?: Context
): Promise<ProductTypes.ProductTagDTO>
@InjectTransactionManager("baseRepository_")
async updateTags(
data: ProductTypes.UpdateProductTagDTO[],
async upsertTags(
data: ProductTypes.UpsertProductTagDTO[] | ProductTypes.UpsertProductTagDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<ProductTypes.ProductTagDTO[]> {
const productTags = await this.productTagService_.update(
data,
): Promise<ProductTypes.ProductTagDTO[] | ProductTypes.ProductTagDTO> {
const input = Array.isArray(data) ? data : [data]
const forUpdate = input.filter((tag): tag is UpdateTagInput => !!tag.id)
const forCreate = input.filter(
(tag): tag is ProductTypes.CreateProductTagDTO => !tag.id
)
let created: ProductTag[] = []
let updated: ProductTag[] = []
if (forCreate.length) {
created = await this.productTagService_.create(forCreate, sharedContext)
}
if (forUpdate.length) {
updated = await this.productTagService_.update(forUpdate, sharedContext)
}
const result = [...created, ...updated]
const allTags = await this.baseRepository_.serialize<
ProductTypes.ProductTagDTO[] | ProductTypes.ProductTagDTO
>(result)
return Array.isArray(data) ? allTags : allTags[0]
}
updateTags(
id: string,
data: ProductTypes.UpdateProductTagDTO,
sharedContext?: Context
): Promise<ProductTypes.ProductTagDTO>
updateTags(
selector: ProductTypes.FilterableProductTagProps,
data: ProductTypes.UpdateProductTagDTO,
sharedContext?: Context
): Promise<ProductTypes.ProductTagDTO[]>
@InjectManager("baseRepository_")
async updateTags(
idOrSelector: string | ProductTypes.FilterableProductTagProps,
data: ProductTypes.UpdateProductTagDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<ProductTypes.ProductTagDTO[] | ProductTypes.ProductTagDTO> {
let normalizedInput: UpdateTagInput[] = []
if (isString(idOrSelector)) {
// Check if the tag exists in the first place
await this.productTagService_.retrieve(idOrSelector, {}, sharedContext)
normalizedInput = [{ id: idOrSelector, ...data }]
} else {
const tags = await this.productTagService_.list(
idOrSelector,
{},
sharedContext
)
return await this.baseRepository_.serialize(productTags)
normalizedInput = tags.map((tag) => ({
id: tag.id,
...data,
}))
}
const tags = await this.productTagService_.update(
normalizedInput,
sharedContext
)
const updatedTags = await this.baseRepository_.serialize<
ProductTypes.ProductTagDTO[]
>(tags)
return isString(idOrSelector) ? updatedTags[0] : updatedTags
}
createTypes(

View File

@@ -57,6 +57,10 @@ export type UpdateTypeInput = ProductTypes.UpdateProductTypeDTO & {
id: string
}
export type UpdateTagInput = ProductTypes.UpdateProductTagDTO & {
id: string
}
export type UpdateProductVariantInput = ProductTypes.UpdateProductVariantDTO & {
id: string
product_id?: string | null