Skip to content

Commit 016e332

Browse files
authored
feat: make AbstractModuleService create method type-safe (medusajs#11216)
1 parent 6cd8249 commit 016e332

File tree

23 files changed

+677
-35
lines changed

23 files changed

+677
-35
lines changed

.changeset/green-insects-invite.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
"@medusajs/api-key": patch
3+
"@medusajs/auth": patch
4+
"@medusajs/cart": patch
5+
"@medusajs/customer": patch
6+
"@medusajs/fulfillment": patch
7+
"@medusajs/inventory": patch
8+
"@medusajs/notification": patch
9+
"@medusajs/order": patch
10+
"@medusajs/payment": patch
11+
"@medusajs/pricing": patch
12+
"@medusajs/product": patch
13+
"@medusajs/promotion": patch
14+
"@medusajs/region": patch
15+
"@medusajs/sales-channel": patch
16+
"@medusajs/stock-location": patch
17+
"@medusajs/store": patch
18+
"@medusajs/tax": patch
19+
"@medusajs/user": patch
20+
"@medusajs/utils": patch
21+
---
22+
23+
feat: make AbstractModuleService create method type-safe
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
import { expectTypeOf } from "expect-type"
2+
import { model } from "../../dml"
3+
import { MedusaService } from "../medusa-service"
4+
import { InferTypeOf } from "@medusajs/types"
5+
6+
const Blog = model.define("Blog", {
7+
id: model.text(),
8+
title: model.text(),
9+
description: model.text().nullable(),
10+
})
11+
12+
type BlogDTO = {
13+
id: number
14+
title: string
15+
}
16+
17+
type CreateBlogDTO = {
18+
title: string | null
19+
}
20+
21+
const baseRepoMock = {
22+
serialize: jest.fn().mockImplementation((item) => item),
23+
transaction: (task) => task("transactionManager"),
24+
getFreshManager: jest.fn().mockReturnThis(),
25+
}
26+
27+
const containerMock = {
28+
baseRepository: baseRepoMock,
29+
mainModelMockRepository: baseRepoMock,
30+
otherModelMock1Repository: baseRepoMock,
31+
otherModelMock2Repository: baseRepoMock,
32+
mainModelMockService: {
33+
retrieve: jest.fn().mockResolvedValue({ id: "1", name: "Item" }),
34+
list: jest.fn().mockResolvedValue([{ id: "1", name: "Item" }]),
35+
delete: jest.fn().mockResolvedValue(undefined),
36+
softDelete: jest.fn().mockResolvedValue([[], {}]),
37+
restore: jest.fn().mockResolvedValue([[], {}]),
38+
},
39+
otherModelMock1Service: {
40+
retrieve: jest.fn().mockResolvedValue({ id: "1", name: "Item" }),
41+
list: jest.fn().mockResolvedValue([{ id: "1", name: "Item" }]),
42+
delete: jest.fn().mockResolvedValue(undefined),
43+
softDelete: jest.fn().mockResolvedValue([[], {}]),
44+
restore: jest.fn().mockResolvedValue([[], {}]),
45+
},
46+
otherModelMock2Service: {
47+
retrieve: jest.fn().mockResolvedValue({ id: "1", name: "Item" }),
48+
list: jest.fn().mockResolvedValue([{ id: "1", name: "Item" }]),
49+
delete: jest.fn().mockResolvedValue(undefined),
50+
softDelete: jest.fn().mockResolvedValue([[], {}]),
51+
restore: jest.fn().mockResolvedValue([[], {}]),
52+
},
53+
}
54+
55+
describe("Medusa Service typings", () => {
56+
describe("create<Service>", () => {
57+
test("type-hint model properties", () => {
58+
class BlogService extends MedusaService({ Blog }) {}
59+
const blogService = new BlogService(containerMock)
60+
61+
expectTypeOf(blogService.createBlogs).parameters.toEqualTypeOf<
62+
| [
63+
Partial<{
64+
id: string | undefined
65+
title: string | undefined
66+
description: string | null | undefined
67+
}>,
68+
...rest: any[]
69+
]
70+
| [
71+
Partial<{
72+
id: string | undefined
73+
title: string | undefined
74+
description: string | null | undefined
75+
}>[],
76+
...rest: any[]
77+
]
78+
>()
79+
expectTypeOf(blogService.createBlogs).returns.toEqualTypeOf<
80+
Promise<InferTypeOf<typeof Blog>> | Promise<InferTypeOf<typeof Blog>[]>
81+
>()
82+
})
83+
84+
test("type-hint DTO properties", () => {
85+
class BlogService extends MedusaService<{ Blog: { dto: BlogDTO } }>({
86+
Blog,
87+
}) {}
88+
const blogService = new BlogService(containerMock)
89+
90+
expectTypeOf(blogService.createBlogs).parameters.toEqualTypeOf<
91+
| [Partial<BlogDTO>, ...rest: any[]]
92+
| [Partial<BlogDTO>[], ...rest: any[]]
93+
>()
94+
expectTypeOf(blogService.createBlogs).returns.toEqualTypeOf<
95+
Promise<BlogDTO> | Promise<BlogDTO[]>
96+
>()
97+
})
98+
99+
test("type-hint force overridden properties", () => {
100+
class BlogService extends MedusaService<{ Blog: { dto: BlogDTO } }>({
101+
Blog,
102+
}) {
103+
// @ts-expect-error
104+
async createBlogs(_: CreateBlogDTO): Promise<BlogDTO> {
105+
return {} as BlogDTO
106+
}
107+
}
108+
const blogService = new BlogService(containerMock)
109+
110+
expectTypeOf(blogService.createBlogs).parameters.toEqualTypeOf<
111+
[CreateBlogDTO]
112+
>()
113+
expectTypeOf(blogService.createBlogs).returns.toEqualTypeOf<
114+
Promise<BlogDTO>
115+
>()
116+
})
117+
118+
test("define custom DTO for inputs", () => {
119+
class BlogService extends MedusaService<{
120+
Blog: { dto: BlogDTO; inputDto: Omit<BlogDTO, "id"> }
121+
}>({
122+
Blog,
123+
}) {}
124+
const blogService = new BlogService(containerMock)
125+
126+
expectTypeOf(blogService.createBlogs).parameters.toEqualTypeOf<
127+
| [Partial<{ title: string | undefined }>, ...rest: any[]]
128+
| [Partial<{ title: string | undefined }>[], ...rest: any[]]
129+
>()
130+
expectTypeOf(blogService.createBlogs).returns.toEqualTypeOf<
131+
Promise<BlogDTO> | Promise<BlogDTO[]>
132+
>()
133+
})
134+
})
135+
136+
describe("update<Service>", () => {
137+
test("type-hint model properties", () => {
138+
class BlogService extends MedusaService({ Blog }) {}
139+
const blogService = new BlogService(containerMock)
140+
141+
expectTypeOf(blogService.updateBlogs).parameters.toEqualTypeOf<
142+
| [
143+
Partial<{
144+
id: string | undefined
145+
title: string | undefined
146+
description: string | null | undefined
147+
}>,
148+
...rest: any[]
149+
]
150+
| [
151+
(
152+
| Partial<{
153+
id: string | undefined
154+
title: string | undefined
155+
description: string | null | undefined
156+
}>[]
157+
| {
158+
selector: Record<string, any>
159+
data:
160+
| Partial<{
161+
id: string | undefined
162+
title: string | undefined
163+
description: string | null | undefined
164+
}>
165+
| Partial<{
166+
id: string | undefined
167+
title: string | undefined
168+
description: string | null | undefined
169+
}>[]
170+
}
171+
| {
172+
selector: Record<string, any>
173+
data:
174+
| Partial<{
175+
id: string | undefined
176+
title: string | undefined
177+
description: string | null | undefined
178+
}>
179+
| Partial<{
180+
id: string | undefined
181+
title: string | undefined
182+
description: string | null | undefined
183+
}>[]
184+
}[]
185+
),
186+
...rest: any[]
187+
]
188+
>()
189+
expectTypeOf(blogService.updateBlogs).returns.toEqualTypeOf<
190+
Promise<InferTypeOf<typeof Blog>> | Promise<InferTypeOf<typeof Blog>[]>
191+
>()
192+
})
193+
194+
test("type-hint DTO properties", () => {
195+
class BlogService extends MedusaService<{ Blog: { dto: BlogDTO } }>({
196+
Blog,
197+
}) {}
198+
const blogService = new BlogService(containerMock)
199+
200+
expectTypeOf(blogService.updateBlogs).parameters.toEqualTypeOf<
201+
| [Partial<BlogDTO>, ...rest: any[]]
202+
| [
203+
(
204+
| Partial<BlogDTO>[]
205+
| {
206+
selector: Record<string, any>
207+
data: Partial<BlogDTO> | Partial<BlogDTO>[]
208+
}
209+
| {
210+
selector: Record<string, any>
211+
data: Partial<BlogDTO> | Partial<BlogDTO>[]
212+
}[]
213+
),
214+
...rest: any[]
215+
]
216+
>()
217+
expectTypeOf(blogService.updateBlogs).returns.toEqualTypeOf<
218+
Promise<BlogDTO> | Promise<BlogDTO[]>
219+
>()
220+
})
221+
222+
test("type-hint force overridden properties", () => {
223+
class BlogService extends MedusaService<{ Blog: { dto: BlogDTO } }>({
224+
Blog,
225+
}) {
226+
// @ts-expect-error
227+
async updateBlogs(_: string, __: CreateBlogDTO): Promise<BlogDTO> {
228+
return {} as BlogDTO
229+
}
230+
}
231+
const blogService = new BlogService(containerMock)
232+
233+
expectTypeOf(blogService.updateBlogs).parameters.toEqualTypeOf<
234+
[id: string, data: CreateBlogDTO]
235+
>()
236+
expectTypeOf(blogService.updateBlogs).returns.toEqualTypeOf<
237+
Promise<BlogDTO>
238+
>()
239+
})
240+
241+
test("define custom DTO for inputs", () => {
242+
class BlogService extends MedusaService<{
243+
Blog: { dto: BlogDTO; inputDto: Omit<BlogDTO, "id"> }
244+
}>({
245+
Blog,
246+
}) {}
247+
const blogService = new BlogService(containerMock)
248+
249+
expectTypeOf(blogService.updateBlogs).parameters.toEqualTypeOf<
250+
| [Partial<{ title: string | undefined }>, ...rest: any[]]
251+
| [
252+
(
253+
| Partial<{ title: string | undefined }>[]
254+
| {
255+
selector: Record<string, any>
256+
data:
257+
| Partial<{ title: string | undefined }>
258+
| Partial<{ title: string | undefined }>[]
259+
}
260+
| {
261+
selector: Record<string, any>
262+
data:
263+
| Partial<{ title: string | undefined }>
264+
| Partial<{ title: string | undefined }>[]
265+
}[]
266+
),
267+
...rest: any[]
268+
]
269+
>()
270+
expectTypeOf(blogService.createBlogs).returns.toEqualTypeOf<
271+
Promise<BlogDTO> | Promise<BlogDTO[]>
272+
>()
273+
})
274+
})
275+
})

0 commit comments

Comments
 (0)