Skip to content
Closed
5 changes: 5 additions & 0 deletions .changeset/young-fishes-grab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@medusajs/utils": patch
---

fix: medusa-service generated functions always return array of data even when only single object is passed
180 changes: 180 additions & 0 deletions packages/core/utils/src/modules-sdk/__tests__/medusa-service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,40 @@ describe("Abstract Module Service Factory", () => {
mainModelMockService: {
retrieve: jest.fn().mockResolvedValue({ id: "1", name: "Item" }),
list: jest.fn().mockResolvedValue([{ id: "1", name: "Item" }]),
create: jest.fn().mockImplementation((data) => {
// Return single object if single object passed, array if array passed
return Array.isArray(data) ? data.map(item => ({ ...item, id: "1" })) : { ...data, id: "1" }
}),
update: jest.fn().mockImplementation((data) => {
// Return single object if single object passed, array if array passed
return Array.isArray(data) ? data.map(item => ({ ...item, updated: true })) : { ...data, updated: true }
}),
delete: jest.fn().mockResolvedValue([]),
softDelete: jest.fn().mockResolvedValue([[], {}]),
restore: jest.fn().mockResolvedValue([[], {}]),
},
otherModelMock1Service: {
retrieve: jest.fn().mockResolvedValue({ id: "1", name: "Item" }),
list: jest.fn().mockResolvedValue([{ id: "1", name: "Item" }]),
create: jest.fn().mockImplementation((data) => {
return Array.isArray(data) ? data.map(item => ({ ...item, id: "1" })) : { ...data, id: "1" }
}),
update: jest.fn().mockImplementation((data) => {
return Array.isArray(data) ? data.map(item => ({ ...item, updated: true })) : { ...data, updated: true }
}),
delete: jest.fn().mockResolvedValue([]),
softDelete: jest.fn().mockResolvedValue([[], {}]),
restore: jest.fn().mockResolvedValue([[], {}]),
},
otherModelMock2Service: {
retrieve: jest.fn().mockResolvedValue({ id: "1", name: "Item" }),
list: jest.fn().mockResolvedValue([{ id: "1", name: "Item" }]),
create: jest.fn().mockImplementation((data) => {
return Array.isArray(data) ? data.map(item => ({ ...item, id: "1" })) : { ...data, id: "1" }
}),
update: jest.fn().mockImplementation((data) => {
return Array.isArray(data) ? data.map(item => ({ ...item, updated: true })) : { ...data, updated: true }
}),
delete: jest.fn().mockResolvedValue([]),
softDelete: jest.fn().mockResolvedValue([[], {}]),
restore: jest.fn().mockResolvedValue([[], {}]),
Expand Down Expand Up @@ -122,6 +142,77 @@ describe("Abstract Module Service Factory", () => {
defaultTransactionContext
)
})

it("should have create method that handles single object input", async () => {
const inputData = { name: "Test Item" }
const result = await instance.createMainModelMocks(inputData)

// Should return single object when single object is passed
expect(result).toEqual({ name: "Test Item", id: "1" })
expect(Array.isArray(result)).toBe(false)

// Should call underlying service with single object (not wrapped in array)
expect(containerMock.mainModelMockService.create).toHaveBeenCalledWith(
inputData,
defaultTransactionContext
)
})

it("should have create method that handles array input", async () => {
const inputData = [{ name: "Test Item 1" }, { name: "Test Item 2" }]
const result = await instance.createMainModelMocks(inputData)

// Should return array when array is passed
expect(result).toEqual([
{ name: "Test Item 1", id: "1" },
{ name: "Test Item 2", id: "1" }
])
expect(Array.isArray(result)).toBe(true)
expect(result).toHaveLength(2)

// Should call underlying service with array
expect(containerMock.mainModelMockService.create).toHaveBeenCalledWith(
inputData,
defaultTransactionContext
)
})

it("should have update method that handles single object input", async () => {
const inputData = { id: "1", name: "Updated Item" }
const result = await instance.updateMainModelMocks(inputData)

// Should return single object when single object is passed
expect(result).toEqual({ id: "1", name: "Updated Item", updated: true })
expect(Array.isArray(result)).toBe(false)

// Should call underlying service with single object (not wrapped in array)
expect(containerMock.mainModelMockService.update).toHaveBeenCalledWith(
inputData,
defaultTransactionContext
)
})

it("should have update method that handles array input", async () => {
const inputData = [
{ id: "1", name: "Updated Item 1" },
{ id: "2", name: "Updated Item 2" }
]
const result = await instance.updateMainModelMocks(inputData)

// Should return array when array is passed
expect(result).toEqual([
{ id: "1", name: "Updated Item 1", updated: true },
{ id: "2", name: "Updated Item 2", updated: true }
])
expect(Array.isArray(result)).toBe(true)
expect(result).toHaveLength(2)

// Should call underlying service with array
expect(containerMock.mainModelMockService.update).toHaveBeenCalledWith(
inputData,
defaultTransactionContext
)
})
})

describe("Other Models Methods", () => {
Expand Down Expand Up @@ -185,6 +276,77 @@ describe("Abstract Module Service Factory", () => {
defaultTransactionContext
)
})

it("should have create method for other models that handles single object input", async () => {
const inputData = { name: "Test Other Item" }
const result = await instance.createOtherModelMock1s(inputData)

// Should return single object when single object is passed
expect(result).toEqual({ name: "Test Other Item", id: "1" })
expect(Array.isArray(result)).toBe(false)

// Should call underlying service with single object (not wrapped in array)
expect(containerMock.otherModelMock1Service.create).toHaveBeenCalledWith(
inputData,
defaultTransactionContext
)
})

it("should have create method for other models that handles array input", async () => {
const inputData = [{ name: "Test Other Item 1" }, { name: "Test Other Item 2" }]
const result = await instance.createOtherModelMock1s(inputData)

// Should return array when array is passed
expect(result).toEqual([
{ name: "Test Other Item 1", id: "1" },
{ name: "Test Other Item 2", id: "1" }
])
expect(Array.isArray(result)).toBe(true)
expect(result).toHaveLength(2)

// Should call underlying service with array
expect(containerMock.otherModelMock1Service.create).toHaveBeenCalledWith(
inputData,
defaultTransactionContext
)
})

it("should have update method for other models that handles single object input", async () => {
const inputData = { id: "1", name: "Updated Other Item" }
const result = await instance.updateOtherModelMock1s(inputData)

// Should return single object when single object is passed
expect(result).toEqual({ id: "1", name: "Updated Other Item", updated: true })
expect(Array.isArray(result)).toBe(false)

// Should call underlying service with single object (not wrapped in array)
expect(containerMock.otherModelMock1Service.update).toHaveBeenCalledWith(
inputData,
defaultTransactionContext
)
})

it("should have update method for other models that handles array input", async () => {
const inputData = [
{ id: "1", name: "Updated Other Item 1" },
{ id: "2", name: "Updated Other Item 2" }
]
const result = await instance.updateOtherModelMock1s(inputData)

// Should return array when array is passed
expect(result).toEqual([
{ id: "1", name: "Updated Other Item 1", updated: true },
{ id: "2", name: "Updated Other Item 2", updated: true }
])
expect(Array.isArray(result)).toBe(true)
expect(result).toHaveLength(2)

// Should call underlying service with array
expect(containerMock.otherModelMock1Service.update).toHaveBeenCalledWith(
inputData,
defaultTransactionContext
)
})
})

describe("Custom configuration using DML", () => {
Expand All @@ -196,20 +358,38 @@ describe("Abstract Module Service Factory", () => {
houseService: {
retrieve: jest.fn().mockResolvedValue({ id: "1", name: "Item" }),
list: jest.fn().mockResolvedValue([{ id: "1", name: "Item" }]),
create: jest.fn().mockImplementation((data) => {
return Array.isArray(data) ? data.map(item => ({ ...item, id: "1" })) : { ...data, id: "1" }
}),
update: jest.fn().mockImplementation((data) => {
return Array.isArray(data) ? data.map(item => ({ ...item, updated: true })) : { ...data, updated: true }
}),
delete: jest.fn().mockResolvedValue(undefined),
softDelete: jest.fn().mockResolvedValue([[], {}]),
restore: jest.fn().mockResolvedValue([[], {}]),
},
carService: {
retrieve: jest.fn().mockResolvedValue({ id: "1", name: "Item" }),
list: jest.fn().mockResolvedValue([{ id: "1", name: "Item" }]),
create: jest.fn().mockImplementation((data) => {
return Array.isArray(data) ? data.map(item => ({ ...item, id: "1" })) : { ...data, id: "1" }
}),
update: jest.fn().mockImplementation((data) => {
return Array.isArray(data) ? data.map(item => ({ ...item, updated: true })) : { ...data, updated: true }
}),
delete: jest.fn().mockResolvedValue(undefined),
softDelete: jest.fn().mockResolvedValue([[], {}]),
restore: jest.fn().mockResolvedValue([[], {}]),
},
userService: {
retrieve: jest.fn().mockResolvedValue({ id: "1", name: "Item" }),
list: jest.fn().mockResolvedValue([{ id: "1", name: "Item" }]),
create: jest.fn().mockImplementation((data) => {
return Array.isArray(data) ? data.map(item => ({ ...item, id: "1" })) : { ...data, id: "1" }
}),
update: jest.fn().mockImplementation((data) => {
return Array.isArray(data) ? data.map(item => ({ ...item, updated: true })) : { ...data, updated: true }
}),
delete: jest.fn().mockResolvedValue(undefined),
softDelete: jest.fn().mockResolvedValue([[], {}]),
restore: jest.fn().mockResolvedValue([[], {}]),
Expand Down
7 changes: 2 additions & 5 deletions packages/core/utils/src/modules-sdk/medusa-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,10 +219,8 @@ export function MedusaService<
data = [],
sharedContext: Context = {}
): Promise<T | T[]> {
const serviceData = Array.isArray(data) ? data : [data]
const service = this.__container__[serviceRegistrationName]
const models = await service.create(serviceData, sharedContext)
const response = Array.isArray(data) ? models : models[0]
const response = await service.create(data, sharedContext)

klassPrototype.aggregatedEvents.bind(this)({
action: CommonEvents.CREATED,
Expand All @@ -243,9 +241,8 @@ export function MedusaService<
data = [],
sharedContext: Context = {}
): Promise<T | T[]> {
const serviceData = Array.isArray(data) ? data : [data]
const service = this.__container__[serviceRegistrationName]
const response = await service.update(serviceData, sharedContext)
const response = await service.update(data, sharedContext)

klassPrototype.aggregatedEvents.bind(this)({
action: CommonEvents.UPDATED,
Expand Down
Loading