feat: refactor module joiner config and links generation (#7859)

* feat: refactor module joiner config and links generation

* improve typings

* WIP

* WIP

* WIP

* rename type file

* create link config

* finish typings and add utils

* improve links

* WIP typings

* finalize ExportModule utils

* finalize ExportModule utils

* fix: dml tests

* improve and fixes

* simplify typings with id changes

* add toJSON

* multiple fixes and entity builder fixes

* fix currency searchable

* fix tests

* medusa service refactoring

* cleanup

* cleanup and fixes

* make module name optional

* renaming

---------

Co-authored-by: Harminder Virk <virk.officials@gmail.com>
This commit is contained in:
Adrien de Peretti
2024-07-03 13:12:49 +02:00
committed by GitHub
parent 5aa62e59e4
commit 617a5972bf
89 changed files with 1706 additions and 950 deletions
@@ -34,7 +34,6 @@ describe("Base property", () => {
dataType: {
name: "text",
options: {
primaryKey: false,
searchable: true,
},
},
@@ -1,4 +1,4 @@
import { ArrayType, MetadataStorage } from "@mikro-orm/core"
import { ArrayType, EntityMetadata, MetadataStorage } from "@mikro-orm/core"
import { expectTypeOf } from "expect-type"
import { DmlEntity } from "../entity"
import { model } from "../entity-builder"
@@ -72,7 +72,7 @@ describe("Entity builder", () => {
phones: model.array(),
})
expect(user.name).toEqual("user")
expect(user.name).toEqual("User")
expect(user.parse().tableName).toEqual("user")
const User = toMikroORMEntity(user)
@@ -202,7 +202,7 @@ describe("Entity builder", () => {
}
)
expect(user.name).toEqual("user")
expect(user.name).toEqual("User")
expect(user.parse().tableName).toEqual("user_table")
const User = toMikroORMEntity(user)
@@ -324,7 +324,7 @@ describe("Entity builder", () => {
}
)
expect(user.name).toEqual("userRole")
expect(user.name).toEqual("UserRole")
expect(user.parse().tableName).toEqual("user_role")
const User = toMikroORMEntity(user)
@@ -1558,9 +1558,10 @@ describe("Entity builder", () => {
account_id: model.number(),
})
const entityBuilder = createMikrORMEntity()
const User = entityBuilder(user)
const metaData = MetadataStorage.getMetadataFromDecorator(User)
const User = toMikroORMEntity(user)
const metaData = MetadataStorage.getMetadataFromDecorator(
User
) as unknown as EntityMetadata<InstanceType<typeof User>>
expect(metaData.properties.id).toEqual({
columnType: "text",
@@ -3848,7 +3849,7 @@ describe("Entity builder", () => {
})
expect(defineEmail).toThrow(
'Cannot cascade delete "user" relationship(s) from "email" entity. Child to parent cascades are not allowed'
'Cannot cascade delete "user" relationship(s) from "Email" entity. Child to parent cascades are not allowed'
)
})
@@ -10,9 +10,7 @@ describe("Id property", () => {
fieldName: "id",
dataType: {
name: "id",
options: {
primaryKey: false,
},
options: {},
},
nullable: false,
indexes: [],
@@ -28,13 +26,12 @@ describe("Id property", () => {
fieldName: "id",
dataType: {
name: "id",
options: {
primaryKey: true,
},
options: {},
},
nullable: false,
indexes: [],
relationships: [],
primaryKey: true,
})
})
})
@@ -10,9 +10,7 @@ describe("Number property", () => {
fieldName: "age",
dataType: {
name: "number",
options: {
primaryKey: false,
},
options: {},
},
nullable: false,
indexes: [],
@@ -10,7 +10,7 @@ describe("Text property", () => {
fieldName: "username",
dataType: {
name: "text",
options: { primaryKey: false, searchable: false },
options: { searchable: false },
},
nullable: false,
indexes: [],
@@ -26,11 +26,12 @@ describe("Text property", () => {
fieldName: "username",
dataType: {
name: "text",
options: { primaryKey: true, searchable: false },
options: { searchable: false },
},
nullable: false,
indexes: [],
relationships: [],
primaryKey: true,
})
})
})
+25 -9
View File
@@ -1,7 +1,17 @@
import type { DMLSchema, RelationshipOptions } from "@medusajs/types"
import {
DMLSchema,
IDmlEntityConfig,
RelationshipOptions,
} from "@medusajs/types"
import { DmlEntity } from "./entity"
import { createBigNumberProperties } from "./helpers/entity-builder/create-big-number-properties"
import { createDefaultProperties } from "./helpers/entity-builder/create-default-properties"
import {
createBigNumberProperties,
DMLSchemaWithBigNumber,
} from "./helpers/entity-builder/create-big-number-properties"
import {
createDefaultProperties,
DMLSchemaDefaults,
} from "./helpers/entity-builder/create-default-properties"
import { ArrayProperty } from "./properties/array"
import { BigNumberProperty } from "./properties/big-number"
import { BooleanProperty } from "./properties/boolean"
@@ -57,7 +67,7 @@ export type ManyToManyOptions = RelationshipOptions &
* representing the pivot table created in the
* database for this relationship.
*/
pivotEntity?: () => DmlEntity<any>
pivotEntity?: () => DmlEntity<any, any>
}
)
@@ -99,17 +109,23 @@ export class EntityBuilder {
*
* export default MyCustom
*/
define<Schema extends DMLSchema>(
nameOrConfig: DefineOptions,
define<Schema extends DMLSchema, TConfig extends IDmlEntityConfig>(
nameOrConfig: TConfig,
schema: Schema
) {
): DmlEntity<
Schema & DMLSchemaWithBigNumber<Schema> & DMLSchemaDefaults,
TConfig
> {
this.#disallowImplicitProperties(schema)
return new DmlEntity(nameOrConfig, {
return new DmlEntity<Schema, TConfig>(nameOrConfig, {
...schema,
...createBigNumberProperties(schema),
...createDefaultProperties(),
})
}) as unknown as DmlEntity<
Schema & DMLSchemaWithBigNumber<Schema> & DMLSchemaDefaults,
TConfig
>
}
/**
+26 -11
View File
@@ -4,25 +4,32 @@ import {
EntityIndex,
ExtractEntityRelations,
IDmlEntity,
IDmlEntityConfig,
InferDmlEntityNameFromConfig,
IsDmlEntity,
QueryCondition,
} from "@medusajs/types"
import { isObject, isString, toCamelCase } from "../common"
import { isObject, isString, toCamelCase, upperCaseFirst } from "../common"
import { transformIndexWhere } from "./helpers/entity-builder/build-indexes"
import { BelongsTo } from "./relations/belongs-to"
type Config = string | { name?: string; tableName: string }
function extractNameAndTableName(nameOrConfig: Config) {
function extractNameAndTableName<Config extends IDmlEntityConfig>(
nameOrConfig: Config
) {
const result = {
name: "",
tableName: "",
} as {
name: InferDmlEntityNameFromConfig<Config>
tableName: string
}
if (isString(nameOrConfig)) {
const [schema, ...rest] = nameOrConfig.split(".")
const name = rest.length ? rest.join(".") : schema
result.name = toCamelCase(name)
result.name = upperCaseFirst(
toCamelCase(name)
) as InferDmlEntityNameFromConfig<Config>
result.tableName = nameOrConfig
}
@@ -37,7 +44,9 @@ function extractNameAndTableName(nameOrConfig: Config) {
const [schema, ...rest] = potentialName.split(".")
const name = rest.length ? rest.join(".") : schema
result.name = toCamelCase(name)
result.name = upperCaseFirst(
toCamelCase(name)
) as InferDmlEntityNameFromConfig<Config>
result.tableName = nameOrConfig.tableName
}
@@ -48,17 +57,23 @@ function extractNameAndTableName(nameOrConfig: Config) {
* Dml entity is a representation of a DML model with a unique
* name, its schema and relationships.
*/
export class DmlEntity<Schema extends DMLSchema> implements IDmlEntity<Schema> {
export class DmlEntity<
Schema extends DMLSchema,
TConfig extends IDmlEntityConfig
> implements IDmlEntity<Schema, TConfig>
{
[IsDmlEntity]: true = true
name: string
name: InferDmlEntityNameFromConfig<TConfig>
schema: Schema
readonly #tableName: string
#cascades: EntityCascades<string[]> = {}
#indexes: EntityIndex<Schema>[] = []
constructor(nameOrConfig: Config, public schema: Schema) {
constructor(nameOrConfig: TConfig, schema: Schema) {
const { name, tableName } = extractNameAndTableName(nameOrConfig)
this.schema = schema
this.name = name
this.#tableName = tableName
}
@@ -70,7 +85,7 @@ export class DmlEntity<Schema extends DMLSchema> implements IDmlEntity<Schema> {
*
* @param entity
*/
static isDmlEntity(entity: unknown): entity is DmlEntity<any> {
static isDmlEntity(entity: unknown): entity is DmlEntity<any, any> {
return !!entity?.[IsDmlEntity]
}
@@ -78,7 +93,7 @@ export class DmlEntity<Schema extends DMLSchema> implements IDmlEntity<Schema> {
* Parse entity to get its underlying information
*/
parse(): {
name: string
name: InferDmlEntityNameFromConfig<TConfig>
tableName: string
schema: DMLSchema
cascades: EntityCascades<string[]>
@@ -1,4 +1,10 @@
import type { EntityConstructor, Infer } from "@medusajs/types"
import type {
DMLSchema,
EntityConstructor,
IDmlEntity,
Infer,
PropertyType,
} from "@medusajs/types"
import { Entity, Filter } from "@mikro-orm/core"
import { mikroOrmSoftDeletableFilterOptions } from "../../dal"
import { DmlEntity } from "../entity"
@@ -34,7 +40,9 @@ export function createMikrORMEntity() {
* A helper function to define a Mikro ORM entity from a
* DML entity.
*/
return function createEntity<T extends DmlEntity<any>>(entity: T): Infer<T> {
return function createEntity<T extends DmlEntity<any, any>>(
entity: T
): Infer<T> {
class MikroORMEntity {}
const { schema, cascades, indexes: entityIndexes = [] } = entity.parse()
@@ -57,11 +65,11 @@ export function createMikrORMEntity() {
/**
* Processing schema fields
*/
Object.entries(schema).forEach(([name, property]) => {
Object.entries(schema as DMLSchema).forEach(([name, property]) => {
const field = property.parse(name)
if ("fieldName" in field) {
defineProperty(MikroORMEntity, field)
defineProperty(MikroORMEntity, name, property as PropertyType<any>)
applyIndexes(MikroORMEntity, tableName, field)
applySearchable(MikroORMEntity, field)
} else {
@@ -85,14 +93,16 @@ export function createMikrORMEntity() {
* return the input idempotently
* @param entity
*/
export const toMikroORMEntity = <T>(entity: T): Infer<T> => {
export const toMikroORMEntity = <T>(
entity: T
): T extends IDmlEntity<any, any> ? Infer<T> : T => {
let mikroOrmEntity: T | EntityConstructor<any> = entity
if (DmlEntity.isDmlEntity(entity)) {
mikroOrmEntity = createMikrORMEntity()(entity)
}
return mikroOrmEntity as Infer<T>
return mikroOrmEntity as T extends IDmlEntity<any, any> ? Infer<T> : T
}
/**
@@ -110,6 +120,6 @@ export const toMikroOrmEntities = function <T extends any[]>(entities: T) {
return entity
}) as {
[K in keyof T]: T[K] extends DmlEntity<any> ? Infer<T[K]> : T[K]
[K in keyof T]: T[K] extends IDmlEntity<any, any> ? Infer<T[K]> : T[K]
}
}
@@ -13,7 +13,7 @@ import { NullableModifier } from "../../properties/nullable"
* test.amount // valid | type = number
* test.raw_amount // valid | type = Record<string, any>
*/
type DMLSchemaWithBigNumber<T extends DMLSchema> = {
export type DMLSchemaWithBigNumber<T extends DMLSchema> = {
[K in keyof T]: T[K]
} & {
[K in keyof T as T[K] extends
@@ -2,6 +2,7 @@ import {
EntityConstructor,
KnownDataTypes,
PropertyMetadata,
PropertyType,
} from "@medusajs/types"
import { MikroOrmBigNumberProperty } from "../../../dal"
import { generateEntityId, isDefined } from "../../../common"
@@ -14,6 +15,7 @@ import {
Property,
Utils,
} from "@mikro-orm/core"
import { PrimaryKeyModifier } from "../../properties/primary-key"
/**
* DML entity data types to PostgreSQL data types via
@@ -91,8 +93,10 @@ const SPECIAL_PROPERTIES: {
*/
export function defineProperty(
MikroORMEntity: EntityConstructor<any>,
field: PropertyMetadata
propertyName: string,
property: PropertyType<any>
) {
const field = property.parse(propertyName)
/**
* Here we initialize nullable properties with a null value
*/
@@ -169,7 +173,7 @@ export function defineProperty(
* Defining an id property
*/
if (field.dataType.name === "id") {
const IdDecorator = field.dataType.options?.primaryKey
const IdDecorator = PrimaryKeyModifier.isPrimaryKeyModifier(property)
? PrimaryKey({
columnType: "text",
type: "string",
@@ -211,7 +215,7 @@ export function defineProperty(
/**
* Defining a primary key property
*/
if (field.dataType.options?.primaryKey) {
if (PrimaryKeyModifier.isPrimaryKeyModifier(property)) {
PrimaryKey({
columnType,
type: propertyType,
@@ -9,9 +9,9 @@ import {
BeforeCreate,
ManyToMany,
ManyToOne,
OnInit,
OneToMany,
OneToOne,
OnInit,
Property,
} from "@mikro-orm/core"
import { camelToSnakeCase, pluralize } from "../../../common"
@@ -81,7 +81,8 @@ export function defineBelongsToRelationship(
MikroORMEntity: EntityConstructor<any>,
relationship: RelationshipMetadata,
relatedEntity: DmlEntity<
Record<string, PropertyType<any> | RelationshipType<any>>
Record<string, PropertyType<any> | RelationshipType<any>>,
any
>,
{ relatedModelName }: { relatedModelName: string }
) {
@@ -213,7 +214,8 @@ export function defineManyToManyRelationship(
MikroORMEntity: EntityConstructor<any>,
relationship: RelationshipMetadata,
relatedEntity: DmlEntity<
Record<string, PropertyType<any> | RelationshipType<any>>
Record<string, PropertyType<any> | RelationshipType<any>>,
any
>,
{
relatedModelName,
@@ -5,7 +5,7 @@ import { camelToSnakeCase, toCamelCase, upperCaseFirst } from "../../../common"
* Parses entity name and returns model and table name from
* it
*/
export function parseEntityName(entity: DmlEntity<any>) {
export function parseEntityName(entity: DmlEntity<any, any>) {
const parsedEntity = entity.parse()
/**
+11 -4
View File
@@ -1,19 +1,27 @@
import { BaseProperty } from "./base"
import { PrimaryKeyModifier } from "./primary-key"
const IsIdProperty = Symbol("IsIdProperty")
/**
* The Id property defines a unique identifier for the schema.
* Most of the times it will be the primary as well.
*/
export class IdProperty extends BaseProperty<string> {
[IsIdProperty] = true
static isIdProperty(value: any): value is IdProperty {
return !!value?.[IsIdProperty]
}
protected dataType: {
name: "id"
options: {
primaryKey: boolean
prefix?: string
}
} = {
name: "id",
options: { primaryKey: false },
options: {},
}
constructor(options?: { prefix?: string }) {
@@ -37,7 +45,6 @@ export class IdProperty extends BaseProperty<string> {
* @customNamespace Property Configuration Methods
*/
primaryKey() {
this.dataType.options.primaryKey = true
return this
return new PrimaryKeyModifier<string, IdProperty>(this)
}
}
@@ -1,4 +1,5 @@
import { BaseProperty } from "./base"
import { PrimaryKeyModifier } from "./primary-key"
/**
* The NumberProperty is used to define a numeric/integer
@@ -7,15 +8,11 @@ import { BaseProperty } from "./base"
export class NumberProperty extends BaseProperty<number> {
protected dataType: {
name: "number"
options: {
primaryKey: boolean
}
options: {}
}
primaryKey() {
this.dataType.options.primaryKey = true
return this
return new PrimaryKeyModifier<number, NumberProperty>(this)
}
constructor(options?: { primaryKey?: boolean }) {
@@ -23,7 +20,7 @@ export class NumberProperty extends BaseProperty<number> {
this.dataType = {
name: "number",
options: { primaryKey: false, ...options },
options: { ...options },
}
}
}
@@ -0,0 +1,39 @@
import { PropertyType } from "@medusajs/types"
const IsPrimaryKeyModifier = Symbol.for("isPrimaryKeyModifier")
/**
* PrimaryKey modifier marks a schema node as primaryKey
*/
export class PrimaryKeyModifier<T, Schema extends PropertyType<T>>
implements PropertyType<T>
{
[IsPrimaryKeyModifier]: true = true
static isPrimaryKeyModifier(obj: any): obj is PrimaryKeyModifier<any, any> {
return !!obj?.[IsPrimaryKeyModifier]
}
/**
* A type-only property to infer the JavScript data-type
* of the schema property
*/
declare $dataType: T
/**
* The parent schema on which the primaryKey modifier is
* applied
*/
#schema: Schema
constructor(schema: Schema) {
this.#schema = schema
}
/**
* Returns the serialized metadata
*/
parse(fieldName: string) {
const schema = this.#schema.parse(fieldName)
schema.primaryKey = true
return schema
}
}
@@ -1,4 +1,5 @@
import { BaseProperty } from "./base"
import { PrimaryKeyModifier } from "./primary-key"
/**
* The TextProperty is used to define a textual property
@@ -7,14 +8,12 @@ export class TextProperty extends BaseProperty<string> {
protected dataType: {
name: "text"
options: {
primaryKey: boolean
prefix?: string
searchable: boolean
}
} = {
name: "text",
options: {
primaryKey: false,
searchable: false,
},
}
@@ -35,9 +34,7 @@ export class TextProperty extends BaseProperty<string> {
* @customNamespace Property Configuration Methods
*/
primaryKey() {
this.dataType.options.primaryKey = true
return this
return new PrimaryKeyModifier<string, TextProperty>(this)
}
/**
@@ -5,11 +5,15 @@ import {
RelationshipTypes,
} from "@medusajs/types"
export const IsRelationship = Symbol.for("isRelationship")
/**
* The BaseRelationship encapsulates the repetitive parts of defining
* a relationship
*/
export abstract class BaseRelationship<T> implements RelationshipType<T> {
[IsRelationship]: true = true
#referencedEntity: T
/**
@@ -28,6 +32,12 @@ export abstract class BaseRelationship<T> implements RelationshipType<T> {
*/
declare $dataType: T
static isRelationship<T>(
relationship: any
): relationship is BaseRelationship<T> {
return !!relationship?.[IsRelationship]
}
constructor(referencedEntity: T, options: RelationshipOptions) {
this.#referencedEntity = referencedEntity
this.options = options
@@ -1,4 +1,5 @@
import { RelationshipType } from "@medusajs/types"
import { IsRelationship } from "./base"
const IsNullableModifier = Symbol.for("isNullableModifier")
@@ -8,7 +9,8 @@ const IsNullableModifier = Symbol.for("isNullableModifier")
export class NullableModifier<T, Relation extends RelationshipType<T>>
implements RelationshipType<T | null>
{
[IsNullableModifier]: true = true
[IsNullableModifier]: true = true;
[IsRelationship]: true = true
static isNullableModifier<T>(
modifier: any