feat: make AbstractModuleService create method type-safe (#11216)
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
import { expectTypeOf } from "expect-type"
|
||||
import { model } from "../../dml"
|
||||
import { MedusaService } from "../medusa-service"
|
||||
import { InferTypeOf } from "@medusajs/types"
|
||||
|
||||
const Blog = model.define("Blog", {
|
||||
id: model.text(),
|
||||
title: model.text(),
|
||||
description: model.text().nullable(),
|
||||
})
|
||||
|
||||
type BlogDTO = {
|
||||
id: number
|
||||
title: string
|
||||
}
|
||||
|
||||
type CreateBlogDTO = {
|
||||
title: string | null
|
||||
}
|
||||
|
||||
const baseRepoMock = {
|
||||
serialize: jest.fn().mockImplementation((item) => item),
|
||||
transaction: (task) => task("transactionManager"),
|
||||
getFreshManager: jest.fn().mockReturnThis(),
|
||||
}
|
||||
|
||||
const containerMock = {
|
||||
baseRepository: baseRepoMock,
|
||||
mainModelMockRepository: baseRepoMock,
|
||||
otherModelMock1Repository: baseRepoMock,
|
||||
otherModelMock2Repository: baseRepoMock,
|
||||
mainModelMockService: {
|
||||
retrieve: jest.fn().mockResolvedValue({ id: "1", name: "Item" }),
|
||||
list: jest.fn().mockResolvedValue([{ id: "1", name: "Item" }]),
|
||||
delete: jest.fn().mockResolvedValue(undefined),
|
||||
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" }]),
|
||||
delete: jest.fn().mockResolvedValue(undefined),
|
||||
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" }]),
|
||||
delete: jest.fn().mockResolvedValue(undefined),
|
||||
softDelete: jest.fn().mockResolvedValue([[], {}]),
|
||||
restore: jest.fn().mockResolvedValue([[], {}]),
|
||||
},
|
||||
}
|
||||
|
||||
describe("Medusa Service typings", () => {
|
||||
describe("create<Service>", () => {
|
||||
test("type-hint model properties", () => {
|
||||
class BlogService extends MedusaService({ Blog }) {}
|
||||
const blogService = new BlogService(containerMock)
|
||||
|
||||
expectTypeOf(blogService.createBlogs).parameters.toEqualTypeOf<
|
||||
| [
|
||||
Partial<{
|
||||
id: string | undefined
|
||||
title: string | undefined
|
||||
description: string | null | undefined
|
||||
}>,
|
||||
...rest: any[]
|
||||
]
|
||||
| [
|
||||
Partial<{
|
||||
id: string | undefined
|
||||
title: string | undefined
|
||||
description: string | null | undefined
|
||||
}>[],
|
||||
...rest: any[]
|
||||
]
|
||||
>()
|
||||
expectTypeOf(blogService.createBlogs).returns.toEqualTypeOf<
|
||||
Promise<InferTypeOf<typeof Blog>> | Promise<InferTypeOf<typeof Blog>[]>
|
||||
>()
|
||||
})
|
||||
|
||||
test("type-hint DTO properties", () => {
|
||||
class BlogService extends MedusaService<{ Blog: { dto: BlogDTO } }>({
|
||||
Blog,
|
||||
}) {}
|
||||
const blogService = new BlogService(containerMock)
|
||||
|
||||
expectTypeOf(blogService.createBlogs).parameters.toEqualTypeOf<
|
||||
| [Partial<BlogDTO>, ...rest: any[]]
|
||||
| [Partial<BlogDTO>[], ...rest: any[]]
|
||||
>()
|
||||
expectTypeOf(blogService.createBlogs).returns.toEqualTypeOf<
|
||||
Promise<BlogDTO> | Promise<BlogDTO[]>
|
||||
>()
|
||||
})
|
||||
|
||||
test("type-hint force overridden properties", () => {
|
||||
class BlogService extends MedusaService<{ Blog: { dto: BlogDTO } }>({
|
||||
Blog,
|
||||
}) {
|
||||
// @ts-expect-error
|
||||
async createBlogs(_: CreateBlogDTO): Promise<BlogDTO> {
|
||||
return {} as BlogDTO
|
||||
}
|
||||
}
|
||||
const blogService = new BlogService(containerMock)
|
||||
|
||||
expectTypeOf(blogService.createBlogs).parameters.toEqualTypeOf<
|
||||
[CreateBlogDTO]
|
||||
>()
|
||||
expectTypeOf(blogService.createBlogs).returns.toEqualTypeOf<
|
||||
Promise<BlogDTO>
|
||||
>()
|
||||
})
|
||||
|
||||
test("define custom DTO for inputs", () => {
|
||||
class BlogService extends MedusaService<{
|
||||
Blog: { dto: BlogDTO; inputDto: Omit<BlogDTO, "id"> }
|
||||
}>({
|
||||
Blog,
|
||||
}) {}
|
||||
const blogService = new BlogService(containerMock)
|
||||
|
||||
expectTypeOf(blogService.createBlogs).parameters.toEqualTypeOf<
|
||||
| [Partial<{ title: string | undefined }>, ...rest: any[]]
|
||||
| [Partial<{ title: string | undefined }>[], ...rest: any[]]
|
||||
>()
|
||||
expectTypeOf(blogService.createBlogs).returns.toEqualTypeOf<
|
||||
Promise<BlogDTO> | Promise<BlogDTO[]>
|
||||
>()
|
||||
})
|
||||
})
|
||||
|
||||
describe("update<Service>", () => {
|
||||
test("type-hint model properties", () => {
|
||||
class BlogService extends MedusaService({ Blog }) {}
|
||||
const blogService = new BlogService(containerMock)
|
||||
|
||||
expectTypeOf(blogService.updateBlogs).parameters.toEqualTypeOf<
|
||||
| [
|
||||
Partial<{
|
||||
id: string | undefined
|
||||
title: string | undefined
|
||||
description: string | null | undefined
|
||||
}>,
|
||||
...rest: any[]
|
||||
]
|
||||
| [
|
||||
(
|
||||
| Partial<{
|
||||
id: string | undefined
|
||||
title: string | undefined
|
||||
description: string | null | undefined
|
||||
}>[]
|
||||
| {
|
||||
selector: Record<string, any>
|
||||
data:
|
||||
| Partial<{
|
||||
id: string | undefined
|
||||
title: string | undefined
|
||||
description: string | null | undefined
|
||||
}>
|
||||
| Partial<{
|
||||
id: string | undefined
|
||||
title: string | undefined
|
||||
description: string | null | undefined
|
||||
}>[]
|
||||
}
|
||||
| {
|
||||
selector: Record<string, any>
|
||||
data:
|
||||
| Partial<{
|
||||
id: string | undefined
|
||||
title: string | undefined
|
||||
description: string | null | undefined
|
||||
}>
|
||||
| Partial<{
|
||||
id: string | undefined
|
||||
title: string | undefined
|
||||
description: string | null | undefined
|
||||
}>[]
|
||||
}[]
|
||||
),
|
||||
...rest: any[]
|
||||
]
|
||||
>()
|
||||
expectTypeOf(blogService.updateBlogs).returns.toEqualTypeOf<
|
||||
Promise<InferTypeOf<typeof Blog>> | Promise<InferTypeOf<typeof Blog>[]>
|
||||
>()
|
||||
})
|
||||
|
||||
test("type-hint DTO properties", () => {
|
||||
class BlogService extends MedusaService<{ Blog: { dto: BlogDTO } }>({
|
||||
Blog,
|
||||
}) {}
|
||||
const blogService = new BlogService(containerMock)
|
||||
|
||||
expectTypeOf(blogService.updateBlogs).parameters.toEqualTypeOf<
|
||||
| [Partial<BlogDTO>, ...rest: any[]]
|
||||
| [
|
||||
(
|
||||
| Partial<BlogDTO>[]
|
||||
| {
|
||||
selector: Record<string, any>
|
||||
data: Partial<BlogDTO> | Partial<BlogDTO>[]
|
||||
}
|
||||
| {
|
||||
selector: Record<string, any>
|
||||
data: Partial<BlogDTO> | Partial<BlogDTO>[]
|
||||
}[]
|
||||
),
|
||||
...rest: any[]
|
||||
]
|
||||
>()
|
||||
expectTypeOf(blogService.updateBlogs).returns.toEqualTypeOf<
|
||||
Promise<BlogDTO> | Promise<BlogDTO[]>
|
||||
>()
|
||||
})
|
||||
|
||||
test("type-hint force overridden properties", () => {
|
||||
class BlogService extends MedusaService<{ Blog: { dto: BlogDTO } }>({
|
||||
Blog,
|
||||
}) {
|
||||
// @ts-expect-error
|
||||
async updateBlogs(_: string, __: CreateBlogDTO): Promise<BlogDTO> {
|
||||
return {} as BlogDTO
|
||||
}
|
||||
}
|
||||
const blogService = new BlogService(containerMock)
|
||||
|
||||
expectTypeOf(blogService.updateBlogs).parameters.toEqualTypeOf<
|
||||
[id: string, data: CreateBlogDTO]
|
||||
>()
|
||||
expectTypeOf(blogService.updateBlogs).returns.toEqualTypeOf<
|
||||
Promise<BlogDTO>
|
||||
>()
|
||||
})
|
||||
|
||||
test("define custom DTO for inputs", () => {
|
||||
class BlogService extends MedusaService<{
|
||||
Blog: { dto: BlogDTO; inputDto: Omit<BlogDTO, "id"> }
|
||||
}>({
|
||||
Blog,
|
||||
}) {}
|
||||
const blogService = new BlogService(containerMock)
|
||||
|
||||
expectTypeOf(blogService.updateBlogs).parameters.toEqualTypeOf<
|
||||
| [Partial<{ title: string | undefined }>, ...rest: any[]]
|
||||
| [
|
||||
(
|
||||
| Partial<{ title: string | undefined }>[]
|
||||
| {
|
||||
selector: Record<string, any>
|
||||
data:
|
||||
| Partial<{ title: string | undefined }>
|
||||
| Partial<{ title: string | undefined }>[]
|
||||
}
|
||||
| {
|
||||
selector: Record<string, any>
|
||||
data:
|
||||
| Partial<{ title: string | undefined }>
|
||||
| Partial<{ title: string | undefined }>[]
|
||||
}[]
|
||||
),
|
||||
...rest: any[]
|
||||
]
|
||||
>()
|
||||
expectTypeOf(blogService.createBlogs).returns.toEqualTypeOf<
|
||||
Promise<BlogDTO> | Promise<BlogDTO[]>
|
||||
>()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
IDmlEntity,
|
||||
InferEntityType,
|
||||
Pluralize,
|
||||
Prettify,
|
||||
RestoreReturn,
|
||||
SoftDeleteReturn,
|
||||
} from "@medusajs/types"
|
||||
@@ -29,6 +30,13 @@ export type ModelDTOConfig = {
|
||||
|
||||
export type ModelsConfigTemplate = { [key: string]: ModelDTOConfig }
|
||||
|
||||
/**
|
||||
* We do not want the DML DTO to accept auto-managed timestamps
|
||||
* as part of the input for the "create" and the "update"
|
||||
* methods
|
||||
*/
|
||||
type DMLDTOExcludeProperties = "created_at" | "updated_at" | "deleted_at"
|
||||
|
||||
export type ModelConfigurationsToConfigTemplate<T extends ModelEntries> = {
|
||||
[Key in keyof T]: {
|
||||
dto: T[Key] extends DmlEntity<any, any>
|
||||
@@ -36,6 +44,11 @@ export type ModelConfigurationsToConfigTemplate<T extends ModelEntries> = {
|
||||
: T[Key] extends Constructor<any>
|
||||
? InstanceType<T[Key]>
|
||||
: any
|
||||
inputDto: T[Key] extends DmlEntity<any, any>
|
||||
? Omit<InferEntityType<T[Key]>, DMLDTOExcludeProperties>
|
||||
: T[Key] extends Constructor<any>
|
||||
? InstanceType<T[Key]>
|
||||
: any
|
||||
model: T[Key] extends { model: infer MODEL }
|
||||
? MODEL
|
||||
: T[Key] extends IDmlEntity<any, any>
|
||||
@@ -83,6 +96,16 @@ export type ModelEntries<Keys = string> = Record<
|
||||
| { name?: string; singular?: string; plural?: string }
|
||||
>
|
||||
|
||||
/**
|
||||
* Returns the input DTO for the servide
|
||||
*/
|
||||
type GetServiceInput<ModelConfig extends { dto: any; inputDto?: any }> =
|
||||
Partial<
|
||||
[undefined] extends ModelConfig["inputDto"]
|
||||
? ModelConfig["dto"]
|
||||
: ModelConfig["inputDto"]
|
||||
>
|
||||
|
||||
export type ExtractKeysFromConfig<ModelsConfig> = ModelsConfig extends {
|
||||
__empty: any
|
||||
}
|
||||
@@ -90,7 +113,7 @@ export type ExtractKeysFromConfig<ModelsConfig> = ModelsConfig extends {
|
||||
: keyof ModelsConfig
|
||||
|
||||
export type AbstractModuleService<
|
||||
TModelsDtoConfig extends Record<string, any>
|
||||
TModelsDtoConfig extends Record<string, { dto: any; inputDto?: any }>
|
||||
> = {
|
||||
[TModelName in keyof TModelsDtoConfig as `retrieve${ExtractSingularName<
|
||||
TModelsDtoConfig,
|
||||
@@ -155,14 +178,41 @@ export type AbstractModuleService<
|
||||
TModelsDtoConfig,
|
||||
TModelName
|
||||
>}`]: {
|
||||
(...args: any[]): Promise<any>
|
||||
(
|
||||
data: Prettify<GetServiceInput<TModelsDtoConfig[TModelName]>>,
|
||||
...rest: any[]
|
||||
): Promise<TModelsDtoConfig[TModelName]["dto"]>
|
||||
(
|
||||
data: Prettify<GetServiceInput<TModelsDtoConfig[TModelName]>>[],
|
||||
...rest: any[]
|
||||
): Promise<TModelsDtoConfig[TModelName]["dto"][]>
|
||||
}
|
||||
} & {
|
||||
[TModelName in keyof TModelsDtoConfig as `update${ExtractPluralName<
|
||||
TModelsDtoConfig,
|
||||
TModelName
|
||||
>}`]: {
|
||||
(...args: any[]): Promise<any>
|
||||
(
|
||||
data: Prettify<GetServiceInput<TModelsDtoConfig[TModelName]>>,
|
||||
...rest: any[]
|
||||
): Promise<TModelsDtoConfig[TModelName]["dto"]>
|
||||
(
|
||||
dataOrOptions:
|
||||
| Prettify<GetServiceInput<TModelsDtoConfig[TModelName]>>[]
|
||||
| {
|
||||
selector: Record<string, any>
|
||||
data:
|
||||
| Prettify<GetServiceInput<TModelsDtoConfig[TModelName]>>
|
||||
| Prettify<GetServiceInput<TModelsDtoConfig[TModelName]>>[]
|
||||
}
|
||||
| {
|
||||
selector: Record<string, any>
|
||||
data:
|
||||
| Prettify<GetServiceInput<TModelsDtoConfig[TModelName]>>
|
||||
| Prettify<GetServiceInput<TModelsDtoConfig[TModelName]>>[]
|
||||
}[],
|
||||
...rest: any[]
|
||||
): Promise<TModelsDtoConfig[TModelName]["dto"][]>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user