feat: Add support for array properties DML (#7836)
This commit is contained in:
@@ -17,6 +17,7 @@ export type KnownDataTypes =
|
|||||||
| "number"
|
| "number"
|
||||||
| "bigNumber"
|
| "bigNumber"
|
||||||
| "dateTime"
|
| "dateTime"
|
||||||
|
| "array"
|
||||||
| "json"
|
| "json"
|
||||||
| "id"
|
| "id"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { expectTypeOf } from "expect-type"
|
||||||
|
import { ArrayProperty } from "../properties/array"
|
||||||
|
|
||||||
|
describe("Array property", () => {
|
||||||
|
test("should create an array property type", () => {
|
||||||
|
const property = new ArrayProperty()
|
||||||
|
|
||||||
|
expectTypeOf(property["$dataType"]).toEqualTypeOf<[]>()
|
||||||
|
expect(property.parse("codes")).toEqual({
|
||||||
|
fieldName: "codes",
|
||||||
|
dataType: {
|
||||||
|
name: "array",
|
||||||
|
},
|
||||||
|
nullable: false,
|
||||||
|
indexes: [],
|
||||||
|
relationships: [],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { EntityConstructor } from "@medusajs/types"
|
import { EntityConstructor } from "@medusajs/types"
|
||||||
import { MetadataStorage } from "@mikro-orm/core"
|
import { ArrayType, MetadataStorage } from "@mikro-orm/core"
|
||||||
import { expectTypeOf } from "expect-type"
|
import { expectTypeOf } from "expect-type"
|
||||||
import { DmlEntity } from "../entity"
|
import { DmlEntity } from "../entity"
|
||||||
import { model } from "../entity-builder"
|
import { model } from "../entity-builder"
|
||||||
@@ -35,6 +35,7 @@ describe("Entity builder", () => {
|
|||||||
username: model.text(),
|
username: model.text(),
|
||||||
email: model.text(),
|
email: model.text(),
|
||||||
spend_limit: model.bigNumber(),
|
spend_limit: model.bigNumber(),
|
||||||
|
phones: model.array(),
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(user.name).toEqual("user")
|
expect(user.name).toEqual("user")
|
||||||
@@ -48,6 +49,7 @@ describe("Entity builder", () => {
|
|||||||
email: string
|
email: string
|
||||||
spend_limit: number
|
spend_limit: number
|
||||||
raw_spend_limit: Record<string, unknown>
|
raw_spend_limit: Record<string, unknown>
|
||||||
|
phones: string[]
|
||||||
created_at: Date
|
created_at: Date
|
||||||
updated_at: Date
|
updated_at: Date
|
||||||
deleted_at: Date | null
|
deleted_at: Date | null
|
||||||
@@ -124,6 +126,14 @@ describe("Entity builder", () => {
|
|||||||
setter: false,
|
setter: false,
|
||||||
type: "any",
|
type: "any",
|
||||||
},
|
},
|
||||||
|
phones: {
|
||||||
|
getter: false,
|
||||||
|
name: "phones",
|
||||||
|
nullable: false,
|
||||||
|
reference: "scalar",
|
||||||
|
setter: false,
|
||||||
|
type: ArrayType,
|
||||||
|
},
|
||||||
updated_at: {
|
updated_at: {
|
||||||
reference: "scalar",
|
reference: "scalar",
|
||||||
type: "date",
|
type: "date",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { BelongsTo } from "./relations/belongs-to"
|
|||||||
import { HasMany } from "./relations/has-many"
|
import { HasMany } from "./relations/has-many"
|
||||||
import { HasOne } from "./relations/has-one"
|
import { HasOne } from "./relations/has-one"
|
||||||
import { ManyToMany } from "./relations/many-to-many"
|
import { ManyToMany } from "./relations/many-to-many"
|
||||||
|
import { ArrayProperty } from "./properties/array"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The implicit properties added by EntityBuilder in every schema
|
* The implicit properties added by EntityBuilder in every schema
|
||||||
@@ -105,6 +106,13 @@ export class EntityBuilder {
|
|||||||
return new BigNumberProperty()
|
return new BigNumberProperty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define an array column
|
||||||
|
*/
|
||||||
|
array() {
|
||||||
|
return new ArrayProperty()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define a timestampz column
|
* Define a timestampz column
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,113 +1,12 @@
|
|||||||
import type {
|
import type { EntityConstructor, Infer } from "@medusajs/types"
|
||||||
EntityCascades,
|
import { Entity, Filter } from "@mikro-orm/core"
|
||||||
EntityConstructor,
|
import { mikroOrmSoftDeletableFilterOptions } from "../../dal"
|
||||||
Infer,
|
|
||||||
KnownDataTypes,
|
|
||||||
PropertyMetadata,
|
|
||||||
PropertyType,
|
|
||||||
RelationshipMetadata,
|
|
||||||
RelationshipType,
|
|
||||||
} from "@medusajs/types"
|
|
||||||
import {
|
|
||||||
BeforeCreate,
|
|
||||||
Entity,
|
|
||||||
Enum,
|
|
||||||
Filter,
|
|
||||||
ManyToMany,
|
|
||||||
ManyToOne,
|
|
||||||
OneToMany,
|
|
||||||
OneToOne,
|
|
||||||
OnInit,
|
|
||||||
PrimaryKey,
|
|
||||||
Property,
|
|
||||||
} from "@mikro-orm/core"
|
|
||||||
import {
|
|
||||||
camelToSnakeCase,
|
|
||||||
createPsqlIndexStatementHelper,
|
|
||||||
generateEntityId,
|
|
||||||
isDefined,
|
|
||||||
pluralize,
|
|
||||||
toCamelCase,
|
|
||||||
} from "../../common"
|
|
||||||
import { upperCaseFirst } from "../../common/upper-case-first"
|
|
||||||
import {
|
|
||||||
MikroOrmBigNumberProperty,
|
|
||||||
mikroOrmSoftDeletableFilterOptions,
|
|
||||||
Searchable,
|
|
||||||
} from "../../dal"
|
|
||||||
import { DmlEntity } from "../entity"
|
import { DmlEntity } from "../entity"
|
||||||
import { HasMany } from "../relations/has-many"
|
import { defineProperty } from "./entity-builder/define-property"
|
||||||
import { HasOne } from "../relations/has-one"
|
import { applyIndexes } from "./entity-builder/apply-indexes"
|
||||||
import { ManyToMany as DmlManyToMany } from "../relations/many-to-many"
|
import { applySearchable } from "./entity-builder/apply-searchable"
|
||||||
|
import { defineRelationship } from "./entity-builder/define-relationship"
|
||||||
/**
|
import { parseEntityName } from "./entity-builder/parse-entity-name"
|
||||||
* DML entity data types to PostgreSQL data types via
|
|
||||||
* Mikro ORM.
|
|
||||||
*
|
|
||||||
* We remove "enum" type from here, because we use a dedicated
|
|
||||||
* mikro orm decorator for that
|
|
||||||
*/
|
|
||||||
const COLUMN_TYPES: {
|
|
||||||
[K in Exclude<KnownDataTypes, "enum" | "id">]: string
|
|
||||||
} = {
|
|
||||||
boolean: "boolean",
|
|
||||||
dateTime: "timestamptz",
|
|
||||||
number: "integer",
|
|
||||||
bigNumber: "numeric",
|
|
||||||
text: "text",
|
|
||||||
json: "jsonb",
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DML entity data types to Mikro ORM property
|
|
||||||
* types.
|
|
||||||
*
|
|
||||||
* We remove "enum" type from here, because we use a dedicated
|
|
||||||
* mikro orm decorator for that
|
|
||||||
*/
|
|
||||||
const PROPERTY_TYPES: {
|
|
||||||
[K in Exclude<KnownDataTypes, "enum" | "id">]: string
|
|
||||||
} = {
|
|
||||||
boolean: "boolean",
|
|
||||||
dateTime: "date",
|
|
||||||
number: "number",
|
|
||||||
bigNumber: "number",
|
|
||||||
text: "string",
|
|
||||||
json: "any",
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Properties that needs special treatment based upon their name.
|
|
||||||
* We can safely rely on these names because they are never
|
|
||||||
* provided by the end-user. Instead we output them
|
|
||||||
* implicitly via the DML.
|
|
||||||
*/
|
|
||||||
const SPECIAL_PROPERTIES: {
|
|
||||||
[propertyName: string]: (
|
|
||||||
MikroORMEntity: EntityConstructor<any>,
|
|
||||||
field: PropertyMetadata
|
|
||||||
) => void
|
|
||||||
} = {
|
|
||||||
created_at: (MikroORMEntity, field) => {
|
|
||||||
Property({
|
|
||||||
columnType: "timestamptz",
|
|
||||||
type: "date",
|
|
||||||
nullable: false,
|
|
||||||
defaultRaw: "now()",
|
|
||||||
onCreate: () => new Date(),
|
|
||||||
})(MikroORMEntity.prototype, field.fieldName)
|
|
||||||
},
|
|
||||||
updated_at: (MikroORMEntity, field) => {
|
|
||||||
Property({
|
|
||||||
columnType: "timestamptz",
|
|
||||||
type: "date",
|
|
||||||
nullable: false,
|
|
||||||
defaultRaw: "now()",
|
|
||||||
onCreate: () => new Date(),
|
|
||||||
onUpdate: () => new Date(),
|
|
||||||
})(MikroORMEntity.prototype, field.fieldName)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory function to create the mikro orm entity builder. The return
|
* Factory function to create the mikro orm entity builder. The return
|
||||||
@@ -115,33 +14,6 @@ const SPECIAL_PROPERTIES: {
|
|||||||
* to Mikro ORM entities.
|
* to Mikro ORM entities.
|
||||||
*/
|
*/
|
||||||
export function createMikrORMEntity() {
|
export function createMikrORMEntity() {
|
||||||
/**
|
|
||||||
* Parses entity name and returns model and table name from
|
|
||||||
* it
|
|
||||||
*/
|
|
||||||
function parseEntityName(entity: DmlEntity<any>) {
|
|
||||||
const parsedEntity = entity.parse()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Table name is going to be the snake case version of the entity name.
|
|
||||||
* Here we should preserve PG schema (if defined).
|
|
||||||
*
|
|
||||||
* For example: "platform.user" should stay as "platform.user"
|
|
||||||
*/
|
|
||||||
const tableName = camelToSnakeCase(parsedEntity.tableName)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Entity name is going to be the camelCase version of the
|
|
||||||
* name defined by the user
|
|
||||||
*/
|
|
||||||
const [pgSchema, ...rest] = tableName.split(".")
|
|
||||||
return {
|
|
||||||
tableName,
|
|
||||||
modelName: upperCaseFirst(toCamelCase(parsedEntity.name)),
|
|
||||||
pgSchema: rest.length ? pgSchema : undefined,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The following property is used to track many to many relationship
|
* The following property is used to track many to many relationship
|
||||||
* between two entities. It is needed because we have to mark one
|
* between two entities. It is needed because we have to mark one
|
||||||
@@ -158,545 +30,6 @@ export function createMikrORMEntity() {
|
|||||||
// TODO: if we use the util toMikroOrmEntities then a new builder will be used each time, lets think about this. Currently if means that with many to many we need to use the same builder
|
// TODO: if we use the util toMikroOrmEntities then a new builder will be used each time, lets think about this. Currently if means that with many to many we need to use the same builder
|
||||||
const MANY_TO_MANY_TRACKED_REALTIONS: Record<string, boolean> = {}
|
const MANY_TO_MANY_TRACKED_REALTIONS: Record<string, boolean> = {}
|
||||||
|
|
||||||
/**
|
|
||||||
* Defines a DML entity schema field as a Mikro ORM property
|
|
||||||
*/
|
|
||||||
function defineProperty(
|
|
||||||
MikroORMEntity: EntityConstructor<any>,
|
|
||||||
field: PropertyMetadata
|
|
||||||
) {
|
|
||||||
/**
|
|
||||||
* Here we initialize nullable properties with a null value
|
|
||||||
*/
|
|
||||||
if (field.nullable) {
|
|
||||||
Object.defineProperty(MikroORMEntity.prototype, field.fieldName, {
|
|
||||||
value: null,
|
|
||||||
configurable: true,
|
|
||||||
enumerable: true,
|
|
||||||
writable: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SPECIAL_PROPERTIES[field.fieldName]) {
|
|
||||||
SPECIAL_PROPERTIES[field.fieldName](MikroORMEntity, field)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Defining an big number property
|
|
||||||
* A big number property always comes with a raw_{{ fieldName }} column
|
|
||||||
* where the config of the bigNumber is set.
|
|
||||||
* The `raw_` field is generated during DML schema generation as a json
|
|
||||||
* dataType.
|
|
||||||
*/
|
|
||||||
if (field.dataType.name === "bigNumber") {
|
|
||||||
MikroOrmBigNumberProperty({
|
|
||||||
nullable: field.nullable,
|
|
||||||
/**
|
|
||||||
* MikroORM does not ignore undefined values for default when generating
|
|
||||||
* the database schema SQL. Conditionally add it here to prevent undefined
|
|
||||||
* from being set as default value in SQL.
|
|
||||||
*/
|
|
||||||
...(isDefined(field.defaultValue) && { default: field.defaultValue }),
|
|
||||||
})(MikroORMEntity.prototype, field.fieldName)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Defining an enum property
|
|
||||||
*/
|
|
||||||
if (field.dataType.name === "enum") {
|
|
||||||
Enum({
|
|
||||||
items: () => field.dataType.options!.choices,
|
|
||||||
nullable: field.nullable,
|
|
||||||
/**
|
|
||||||
* MikroORM does not ignore undefined values for default when generating
|
|
||||||
* the database schema SQL. Conditionally add it here to prevent undefined
|
|
||||||
* from being set as default value in SQL.
|
|
||||||
*/
|
|
||||||
...(isDefined(field.defaultValue) && { default: field.defaultValue }),
|
|
||||||
})(MikroORMEntity.prototype, field.fieldName)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Defining an id property
|
|
||||||
*/
|
|
||||||
if (field.dataType.name === "id") {
|
|
||||||
const IdDecorator = field.dataType.options?.primaryKey
|
|
||||||
? PrimaryKey({
|
|
||||||
columnType: "text",
|
|
||||||
type: "string",
|
|
||||||
nullable: false,
|
|
||||||
})
|
|
||||||
: Property({
|
|
||||||
columnType: "text",
|
|
||||||
type: "string",
|
|
||||||
nullable: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
IdDecorator(MikroORMEntity.prototype, field.fieldName)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook to generate entity within the code
|
|
||||||
*/
|
|
||||||
MikroORMEntity.prototype.generateId = function () {
|
|
||||||
this[field.fieldName] = generateEntityId(
|
|
||||||
this[field.fieldName],
|
|
||||||
field.dataType.options?.prefix
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute hook via lifecycle decorators
|
|
||||||
*/
|
|
||||||
BeforeCreate()(MikroORMEntity.prototype, "generateId")
|
|
||||||
OnInit()(MikroORMEntity.prototype, "generateId")
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Define rest of properties
|
|
||||||
*/
|
|
||||||
const columnType = COLUMN_TYPES[field.dataType.name]
|
|
||||||
const propertyType = PROPERTY_TYPES[field.dataType.name]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Defining a primary key property
|
|
||||||
*/
|
|
||||||
if (field.dataType.options?.primaryKey) {
|
|
||||||
PrimaryKey({
|
|
||||||
columnType,
|
|
||||||
type: propertyType,
|
|
||||||
nullable: false,
|
|
||||||
})(MikroORMEntity.prototype, field.fieldName)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Property({
|
|
||||||
columnType,
|
|
||||||
type: propertyType,
|
|
||||||
nullable: field.nullable,
|
|
||||||
/**
|
|
||||||
* MikroORM does not ignore undefined values for default when generating
|
|
||||||
* the database schema SQL. Conditionally add it here to prevent undefined
|
|
||||||
* from being set as default value in SQL.
|
|
||||||
*/
|
|
||||||
...(isDefined(field.defaultValue) && { default: field.defaultValue }),
|
|
||||||
})(MikroORMEntity.prototype, field.fieldName)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prepares indexes for a given field
|
|
||||||
*/
|
|
||||||
function applyIndexes(
|
|
||||||
MikroORMEntity: EntityConstructor<any>,
|
|
||||||
tableName: string,
|
|
||||||
field: PropertyMetadata
|
|
||||||
) {
|
|
||||||
field.indexes.forEach((index) => {
|
|
||||||
const providerEntityIdIndexStatement = createPsqlIndexStatementHelper({
|
|
||||||
tableName,
|
|
||||||
columns: [field.fieldName],
|
|
||||||
unique: index.type === "unique",
|
|
||||||
where: "deleted_at IS NULL",
|
|
||||||
})
|
|
||||||
|
|
||||||
providerEntityIdIndexStatement.MikroORMIndex()(MikroORMEntity)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply the searchable decorator to the property marked as searchable to enable the free text search
|
|
||||||
*/
|
|
||||||
function applySearchable(
|
|
||||||
MikroORMEntity: EntityConstructor<any>,
|
|
||||||
field: PropertyMetadata
|
|
||||||
) {
|
|
||||||
if (!field.dataType.options?.searchable) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Searchable()(MikroORMEntity.prototype, field.fieldName)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Defines has one relationship on the Mikro ORM entity.
|
|
||||||
*/
|
|
||||||
function defineHasOneRelationship(
|
|
||||||
MikroORMEntity: EntityConstructor<any>,
|
|
||||||
relationship: RelationshipMetadata,
|
|
||||||
{ relatedModelName }: { relatedModelName: string },
|
|
||||||
cascades: EntityCascades<string[]>
|
|
||||||
) {
|
|
||||||
const shouldRemoveRelated = !!cascades.delete?.includes(relationship.name)
|
|
||||||
|
|
||||||
OneToOne({
|
|
||||||
entity: relatedModelName,
|
|
||||||
nullable: relationship.nullable,
|
|
||||||
mappedBy: relationship.mappedBy || camelToSnakeCase(MikroORMEntity.name),
|
|
||||||
cascade: shouldRemoveRelated
|
|
||||||
? (["perist", "soft-remove"] as any)
|
|
||||||
: undefined,
|
|
||||||
})(MikroORMEntity.prototype, relationship.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Defines has many relationship on the Mikro ORM entity
|
|
||||||
*/
|
|
||||||
function defineHasManyRelationship(
|
|
||||||
MikroORMEntity: EntityConstructor<any>,
|
|
||||||
relationship: RelationshipMetadata,
|
|
||||||
{ relatedModelName }: { relatedModelName: string },
|
|
||||||
cascades: EntityCascades<string[]>
|
|
||||||
) {
|
|
||||||
const shouldRemoveRelated = !!cascades.delete?.includes(relationship.name)
|
|
||||||
|
|
||||||
OneToMany({
|
|
||||||
entity: relatedModelName,
|
|
||||||
orphanRemoval: true,
|
|
||||||
mappedBy: relationship.mappedBy || camelToSnakeCase(MikroORMEntity.name),
|
|
||||||
cascade: shouldRemoveRelated
|
|
||||||
? (["perist", "soft-remove"] as any)
|
|
||||||
: undefined,
|
|
||||||
})(MikroORMEntity.prototype, relationship.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Defines belongs to relationship on the Mikro ORM entity. The belongsTo
|
|
||||||
* relationship inspects the related entity for the other side of
|
|
||||||
* the relationship and then uses one of the following Mikro ORM
|
|
||||||
* relationship.
|
|
||||||
*
|
|
||||||
* - OneToOne: When the other side uses "hasOne" with "owner: true"
|
|
||||||
* - ManyToOne: When the other side uses "hasMany"
|
|
||||||
*/
|
|
||||||
function defineBelongsToRelationship(
|
|
||||||
MikroORMEntity: EntityConstructor<any>,
|
|
||||||
relationship: RelationshipMetadata,
|
|
||||||
relatedEntity: DmlEntity<
|
|
||||||
Record<string, PropertyType<any> | RelationshipType<any>>
|
|
||||||
>,
|
|
||||||
{ relatedModelName }: { relatedModelName: string }
|
|
||||||
) {
|
|
||||||
const mappedBy =
|
|
||||||
relationship.mappedBy || camelToSnakeCase(MikroORMEntity.name)
|
|
||||||
const { schema: relationSchema, cascades: relationCascades } =
|
|
||||||
relatedEntity.parse()
|
|
||||||
|
|
||||||
const otherSideRelation = relationSchema[mappedBy]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* In DML the relationships are cascaded from parent to child. A belongsTo
|
|
||||||
* relationship is always a child, therefore we look at the parent and
|
|
||||||
* define a onDelete: cascade when we are included in the delete
|
|
||||||
* list of parent cascade.
|
|
||||||
*/
|
|
||||||
const shouldCascade = relationCascades.delete?.includes(mappedBy)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensure the mapped by is defined as relationship on the other side
|
|
||||||
*/
|
|
||||||
if (!otherSideRelation) {
|
|
||||||
throw new Error(
|
|
||||||
`Missing property "${mappedBy}" on "${relatedModelName}" entity. Make sure to define it as a relationship`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyForeignKeyAssignationHooks(foreignKeyName: string) {
|
|
||||||
const hookName = `assignRelationFromForeignKeyValue${foreignKeyName}`
|
|
||||||
/**
|
|
||||||
* Hook to handle foreign key assignation
|
|
||||||
*/
|
|
||||||
MikroORMEntity.prototype[hookName] = function () {
|
|
||||||
this[relationship.name] ??= this[foreignKeyName]
|
|
||||||
this[foreignKeyName] ??= this[relationship.name]?.id
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute hook via lifecycle decorators
|
|
||||||
*/
|
|
||||||
BeforeCreate()(MikroORMEntity.prototype, hookName)
|
|
||||||
OnInit()(MikroORMEntity.prototype, hookName)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Otherside is a has many. Hence we should defined a ManyToOne
|
|
||||||
*/
|
|
||||||
if (
|
|
||||||
HasMany.isHasMany(otherSideRelation) ||
|
|
||||||
DmlManyToMany.isManyToMany(otherSideRelation)
|
|
||||||
) {
|
|
||||||
const foreignKeyName = camelToSnakeCase(`${relationship.name}Id`)
|
|
||||||
|
|
||||||
ManyToOne({
|
|
||||||
entity: relatedModelName,
|
|
||||||
columnType: "text",
|
|
||||||
mapToPk: true,
|
|
||||||
fieldName: camelToSnakeCase(`${relationship.name}Id`),
|
|
||||||
nullable: relationship.nullable,
|
|
||||||
onDelete: shouldCascade ? "cascade" : undefined,
|
|
||||||
})(MikroORMEntity.prototype, camelToSnakeCase(`${relationship.name}Id`))
|
|
||||||
|
|
||||||
if (DmlManyToMany.isManyToMany(otherSideRelation)) {
|
|
||||||
Property({
|
|
||||||
type: relatedModelName,
|
|
||||||
persist: false,
|
|
||||||
nullable: relationship.nullable,
|
|
||||||
})(MikroORMEntity.prototype, relationship.name)
|
|
||||||
} else {
|
|
||||||
// HasMany case
|
|
||||||
ManyToOne({
|
|
||||||
entity: relatedModelName,
|
|
||||||
persist: false,
|
|
||||||
nullable: relationship.nullable,
|
|
||||||
})(MikroORMEntity.prototype, relationship.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
applyForeignKeyAssignationHooks(foreignKeyName)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Otherside is a has one. Hence we should defined a OneToOne
|
|
||||||
*/
|
|
||||||
if (HasOne.isHasOne(otherSideRelation)) {
|
|
||||||
const foreignKeyName = camelToSnakeCase(`${relationship.name}Id`)
|
|
||||||
|
|
||||||
OneToOne({
|
|
||||||
entity: relatedModelName,
|
|
||||||
nullable: relationship.nullable,
|
|
||||||
mappedBy: mappedBy,
|
|
||||||
owner: true,
|
|
||||||
onDelete: shouldCascade ? "cascade" : undefined,
|
|
||||||
})(MikroORMEntity.prototype, relationship.name)
|
|
||||||
|
|
||||||
if (relationship.nullable) {
|
|
||||||
Object.defineProperty(MikroORMEntity.prototype, foreignKeyName, {
|
|
||||||
value: null,
|
|
||||||
configurable: true,
|
|
||||||
enumerable: true,
|
|
||||||
writable: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
Property({
|
|
||||||
type: "string",
|
|
||||||
columnType: "text",
|
|
||||||
nullable: relationship.nullable,
|
|
||||||
})(MikroORMEntity.prototype, foreignKeyName)
|
|
||||||
|
|
||||||
applyForeignKeyAssignationHooks(foreignKeyName)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Other side is some unsupported data-type
|
|
||||||
*/
|
|
||||||
throw new Error(
|
|
||||||
`Invalid relationship reference for "${mappedBy}" on "${relatedModelName}" entity. Make sure to define a hasOne or hasMany relationship`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Defines a many to many relationship on the Mikro ORM entity
|
|
||||||
*/
|
|
||||||
function defineManyToManyRelationship(
|
|
||||||
MikroORMEntity: EntityConstructor<any>,
|
|
||||||
relationship: RelationshipMetadata,
|
|
||||||
relatedEntity: DmlEntity<
|
|
||||||
Record<string, PropertyType<any> | RelationshipType<any>>
|
|
||||||
>,
|
|
||||||
{
|
|
||||||
relatedModelName,
|
|
||||||
pgSchema,
|
|
||||||
}: { relatedModelName: string; pgSchema: string | undefined }
|
|
||||||
) {
|
|
||||||
let mappedBy = relationship.mappedBy
|
|
||||||
let inversedBy: undefined | string
|
|
||||||
let pivotEntityName: undefined | string
|
|
||||||
let pivotTableName: undefined | string
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validating other side of relationship when mapped by is defined
|
|
||||||
*/
|
|
||||||
if (mappedBy) {
|
|
||||||
const otherSideRelation = relatedEntity.parse().schema[mappedBy]
|
|
||||||
if (!otherSideRelation) {
|
|
||||||
throw new Error(
|
|
||||||
`Missing property "${mappedBy}" on "${relatedModelName}" entity. Make sure to define it as a relationship`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!DmlManyToMany.isManyToMany(otherSideRelation)) {
|
|
||||||
throw new Error(
|
|
||||||
`Invalid relationship reference for "${mappedBy}" on "${relatedModelName}" entity. Make sure to define a manyToMany relationship`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the other side has defined a mapped by and if that
|
|
||||||
* mapping is already tracked as the owner.
|
|
||||||
*
|
|
||||||
* - If yes, we will inverse our mapped by
|
|
||||||
* - Otherwise, we will track ourselves as the owner.
|
|
||||||
*/
|
|
||||||
if (
|
|
||||||
otherSideRelation.parse(mappedBy).mappedBy &&
|
|
||||||
MANY_TO_MANY_TRACKED_REALTIONS[`${relatedModelName}.${mappedBy}`]
|
|
||||||
) {
|
|
||||||
inversedBy = mappedBy
|
|
||||||
mappedBy = undefined
|
|
||||||
} else {
|
|
||||||
MANY_TO_MANY_TRACKED_REALTIONS[
|
|
||||||
`${MikroORMEntity.name}.${relationship.name}`
|
|
||||||
] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validating pivot entity when it is defined and computing
|
|
||||||
* its name
|
|
||||||
*/
|
|
||||||
if (relationship.options.pivotEntity) {
|
|
||||||
if (typeof relationship.options.pivotEntity !== "function") {
|
|
||||||
throw new Error(
|
|
||||||
`Invalid pivotEntity reference for "${MikroORMEntity.name}.${relationship.name}". Make sure to define the pivotEntity using a factory function`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const pivotEntity = relationship.options.pivotEntity()
|
|
||||||
if (!DmlEntity.isDmlEntity(pivotEntity)) {
|
|
||||||
throw new Error(
|
|
||||||
`Invalid pivotEntity reference for "${MikroORMEntity.name}.${relationship.name}". Make sure to return a DML entity from the pivotEntity callback`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pivotEntityName = parseEntityName(pivotEntity).modelName
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!pivotEntityName) {
|
|
||||||
/**
|
|
||||||
* Pivot table name is created as follows (when not explicitly provided)
|
|
||||||
*
|
|
||||||
* - Combining both the entity's names.
|
|
||||||
* - Sorting them by alphabetical order
|
|
||||||
* - Converting them from camelCase to snake_case.
|
|
||||||
* - And finally pluralizing the second entity name.
|
|
||||||
*/
|
|
||||||
pivotTableName =
|
|
||||||
relationship.options.pivotTable ??
|
|
||||||
[MikroORMEntity.name.toLowerCase(), relatedModelName.toLowerCase()]
|
|
||||||
.sort()
|
|
||||||
.map((token, index) => {
|
|
||||||
if (index === 1) {
|
|
||||||
return pluralize(camelToSnakeCase(token))
|
|
||||||
}
|
|
||||||
return camelToSnakeCase(token)
|
|
||||||
})
|
|
||||||
.join("_")
|
|
||||||
}
|
|
||||||
|
|
||||||
ManyToMany({
|
|
||||||
entity: relatedModelName,
|
|
||||||
...(pivotTableName
|
|
||||||
? {
|
|
||||||
pivotTable: pgSchema
|
|
||||||
? `${pgSchema}.${pivotTableName}`
|
|
||||||
: pivotTableName,
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
...(pivotEntityName ? { pivotEntity: pivotEntityName } : {}),
|
|
||||||
...(mappedBy ? { mappedBy: mappedBy as any } : {}),
|
|
||||||
...(inversedBy ? { inversedBy: inversedBy as any } : {}),
|
|
||||||
})(MikroORMEntity.prototype, relationship.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Defines a DML entity schema field as a Mikro ORM relationship
|
|
||||||
*/
|
|
||||||
function defineRelationship(
|
|
||||||
MikroORMEntity: EntityConstructor<any>,
|
|
||||||
relationship: RelationshipMetadata,
|
|
||||||
cascades: EntityCascades<string[]>
|
|
||||||
) {
|
|
||||||
/**
|
|
||||||
* We expect the relationship.entity to be a function that
|
|
||||||
* lazily returns the related entity
|
|
||||||
*/
|
|
||||||
const relatedEntity =
|
|
||||||
typeof relationship.entity === "function"
|
|
||||||
? (relationship.entity() as unknown)
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Since we don't type-check relationships, we should validate
|
|
||||||
* them at runtime
|
|
||||||
*/
|
|
||||||
if (!relatedEntity) {
|
|
||||||
throw new Error(
|
|
||||||
`Invalid relationship reference for "${MikroORMEntity.name}.${relationship.name}". Make sure to define the relationship using a factory function`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensure the return value is a DML entity instance
|
|
||||||
*/
|
|
||||||
if (!DmlEntity.isDmlEntity(relatedEntity)) {
|
|
||||||
throw new Error(
|
|
||||||
`Invalid relationship reference for "${MikroORMEntity.name}.${relationship.name}". Make sure to return a DML entity from the relationship callback`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const { modelName, tableName, pgSchema } = parseEntityName(relatedEntity)
|
|
||||||
const relatedEntityInfo = {
|
|
||||||
relatedModelName: modelName,
|
|
||||||
relatedTableName: tableName,
|
|
||||||
pgSchema,
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Defining relationships
|
|
||||||
*/
|
|
||||||
switch (relationship.type) {
|
|
||||||
case "hasOne":
|
|
||||||
defineHasOneRelationship(
|
|
||||||
MikroORMEntity,
|
|
||||||
relationship,
|
|
||||||
relatedEntityInfo,
|
|
||||||
cascades
|
|
||||||
)
|
|
||||||
break
|
|
||||||
case "hasMany":
|
|
||||||
defineHasManyRelationship(
|
|
||||||
MikroORMEntity,
|
|
||||||
relationship,
|
|
||||||
relatedEntityInfo,
|
|
||||||
cascades
|
|
||||||
)
|
|
||||||
break
|
|
||||||
case "belongsTo":
|
|
||||||
defineBelongsToRelationship(
|
|
||||||
MikroORMEntity,
|
|
||||||
relationship,
|
|
||||||
relatedEntity,
|
|
||||||
relatedEntityInfo
|
|
||||||
)
|
|
||||||
break
|
|
||||||
case "manyToMany":
|
|
||||||
defineManyToManyRelationship(
|
|
||||||
MikroORMEntity,
|
|
||||||
relationship,
|
|
||||||
relatedEntity,
|
|
||||||
relatedEntityInfo
|
|
||||||
)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A helper function to define a Mikro ORM entity from a
|
* A helper function to define a Mikro ORM entity from a
|
||||||
* DML entity.
|
* DML entity.
|
||||||
@@ -717,6 +50,10 @@ export function createMikrORMEntity() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
MANY_TO_MANY_TRACKED_REALTIONS,
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processing schema fields
|
* Processing schema fields
|
||||||
*/
|
*/
|
||||||
@@ -728,7 +65,7 @@ export function createMikrORMEntity() {
|
|||||||
applyIndexes(MikroORMEntity, tableName, field)
|
applyIndexes(MikroORMEntity, tableName, field)
|
||||||
applySearchable(MikroORMEntity, field)
|
applySearchable(MikroORMEntity, field)
|
||||||
} else {
|
} else {
|
||||||
defineRelationship(MikroORMEntity, field, cascades)
|
defineRelationship(MikroORMEntity, field, cascades, context)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { EntityConstructor, PropertyMetadata } from "@medusajs/types"
|
||||||
|
import { createPsqlIndexStatementHelper } from "../../../common"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepares indexes for a given field
|
||||||
|
*/
|
||||||
|
export function applyIndexes(
|
||||||
|
MikroORMEntity: EntityConstructor<any>,
|
||||||
|
tableName: string,
|
||||||
|
field: PropertyMetadata
|
||||||
|
) {
|
||||||
|
field.indexes.forEach((index) => {
|
||||||
|
const providerEntityIdIndexStatement = createPsqlIndexStatementHelper({
|
||||||
|
tableName,
|
||||||
|
columns: [field.fieldName],
|
||||||
|
unique: index.type === "unique",
|
||||||
|
where: "deleted_at IS NULL",
|
||||||
|
})
|
||||||
|
|
||||||
|
providerEntityIdIndexStatement.MikroORMIndex()(MikroORMEntity)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { EntityConstructor, PropertyMetadata } from "@medusajs/types"
|
||||||
|
import { Searchable } from "../../../dal"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the searchable decorator to the property marked as searchable to enable the free text search
|
||||||
|
*/
|
||||||
|
export function applySearchable(
|
||||||
|
MikroORMEntity: EntityConstructor<any>,
|
||||||
|
field: PropertyMetadata
|
||||||
|
) {
|
||||||
|
if (!field.dataType.options?.searchable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Searchable()(MikroORMEntity.prototype, field.fieldName)
|
||||||
|
}
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
import {
|
||||||
|
EntityConstructor,
|
||||||
|
KnownDataTypes,
|
||||||
|
PropertyMetadata,
|
||||||
|
} from "@medusajs/types"
|
||||||
|
import { MikroOrmBigNumberProperty } from "../../../dal"
|
||||||
|
import { generateEntityId, isDefined } from "../../../common"
|
||||||
|
import {
|
||||||
|
ArrayType,
|
||||||
|
BeforeCreate,
|
||||||
|
Enum,
|
||||||
|
OnInit,
|
||||||
|
PrimaryKey,
|
||||||
|
Property,
|
||||||
|
} from "@mikro-orm/core"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DML entity data types to PostgreSQL data types via
|
||||||
|
* Mikro ORM.
|
||||||
|
*
|
||||||
|
* We remove "enum" type from here, because we use a dedicated
|
||||||
|
* mikro orm decorator for that
|
||||||
|
*/
|
||||||
|
const COLUMN_TYPES: {
|
||||||
|
[K in Exclude<KnownDataTypes, "enum" | "id">]: string
|
||||||
|
} = {
|
||||||
|
boolean: "boolean",
|
||||||
|
dateTime: "timestamptz",
|
||||||
|
number: "integer",
|
||||||
|
bigNumber: "numeric",
|
||||||
|
text: "text",
|
||||||
|
json: "jsonb",
|
||||||
|
array: "array",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DML entity data types to Mikro ORM property
|
||||||
|
* types.
|
||||||
|
*
|
||||||
|
* We remove "enum" type from here, because we use a dedicated
|
||||||
|
* mikro orm decorator for that
|
||||||
|
*/
|
||||||
|
const PROPERTY_TYPES: {
|
||||||
|
[K in Exclude<KnownDataTypes, "enum" | "id">]: string
|
||||||
|
} = {
|
||||||
|
boolean: "boolean",
|
||||||
|
dateTime: "date",
|
||||||
|
number: "number",
|
||||||
|
bigNumber: "number",
|
||||||
|
text: "string",
|
||||||
|
json: "any",
|
||||||
|
array: "string[]",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Properties that needs special treatment based upon their name.
|
||||||
|
* We can safely rely on these names because they are never
|
||||||
|
* provided by the end-user. Instead we output them
|
||||||
|
* implicitly via the DML.
|
||||||
|
*/
|
||||||
|
const SPECIAL_PROPERTIES: {
|
||||||
|
[propertyName: string]: (
|
||||||
|
MikroORMEntity: EntityConstructor<any>,
|
||||||
|
field: PropertyMetadata
|
||||||
|
) => void
|
||||||
|
} = {
|
||||||
|
created_at: (MikroORMEntity, field) => {
|
||||||
|
Property({
|
||||||
|
columnType: "timestamptz",
|
||||||
|
type: "date",
|
||||||
|
nullable: false,
|
||||||
|
defaultRaw: "now()",
|
||||||
|
onCreate: () => new Date(),
|
||||||
|
})(MikroORMEntity.prototype, field.fieldName)
|
||||||
|
},
|
||||||
|
updated_at: (MikroORMEntity, field) => {
|
||||||
|
Property({
|
||||||
|
columnType: "timestamptz",
|
||||||
|
type: "date",
|
||||||
|
nullable: false,
|
||||||
|
defaultRaw: "now()",
|
||||||
|
onCreate: () => new Date(),
|
||||||
|
onUpdate: () => new Date(),
|
||||||
|
})(MikroORMEntity.prototype, field.fieldName)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines a DML entity schema field as a Mikro ORM property
|
||||||
|
*/
|
||||||
|
export function defineProperty(
|
||||||
|
MikroORMEntity: EntityConstructor<any>,
|
||||||
|
field: PropertyMetadata
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Here we initialize nullable properties with a null value
|
||||||
|
*/
|
||||||
|
if (field.nullable) {
|
||||||
|
Object.defineProperty(MikroORMEntity.prototype, field.fieldName, {
|
||||||
|
value: null,
|
||||||
|
configurable: true,
|
||||||
|
enumerable: true,
|
||||||
|
writable: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SPECIAL_PROPERTIES[field.fieldName]) {
|
||||||
|
SPECIAL_PROPERTIES[field.fieldName](MikroORMEntity, field)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defining an big number property
|
||||||
|
* A big number property always comes with a raw_{{ fieldName }} column
|
||||||
|
* where the config of the bigNumber is set.
|
||||||
|
* The `raw_` field is generated during DML schema generation as a json
|
||||||
|
* dataType.
|
||||||
|
*/
|
||||||
|
if (field.dataType.name === "bigNumber") {
|
||||||
|
MikroOrmBigNumberProperty({
|
||||||
|
nullable: field.nullable,
|
||||||
|
/**
|
||||||
|
* MikroORM does not ignore undefined values for default when generating
|
||||||
|
* the database schema SQL. Conditionally add it here to prevent undefined
|
||||||
|
* from being set as default value in SQL.
|
||||||
|
*/
|
||||||
|
...(isDefined(field.defaultValue) && { default: field.defaultValue }),
|
||||||
|
})(MikroORMEntity.prototype, field.fieldName)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.dataType.name === "array") {
|
||||||
|
Property({
|
||||||
|
type: ArrayType,
|
||||||
|
nullable: field.nullable,
|
||||||
|
/**
|
||||||
|
* MikroORM does not ignore undefined values for default when generating
|
||||||
|
* the database schema SQL. Conditionally add it here to prevent undefined
|
||||||
|
* from being set as default value in SQL.
|
||||||
|
*/
|
||||||
|
...(isDefined(field.defaultValue) && { default: field.defaultValue }),
|
||||||
|
})(MikroORMEntity.prototype, field.fieldName)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defining an enum property
|
||||||
|
*/
|
||||||
|
if (field.dataType.name === "enum") {
|
||||||
|
Enum({
|
||||||
|
items: () => field.dataType.options!.choices,
|
||||||
|
nullable: field.nullable,
|
||||||
|
/**
|
||||||
|
* MikroORM does not ignore undefined values for default when generating
|
||||||
|
* the database schema SQL. Conditionally add it here to prevent undefined
|
||||||
|
* from being set as default value in SQL.
|
||||||
|
*/
|
||||||
|
...(isDefined(field.defaultValue) && { default: field.defaultValue }),
|
||||||
|
})(MikroORMEntity.prototype, field.fieldName)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defining an id property
|
||||||
|
*/
|
||||||
|
if (field.dataType.name === "id") {
|
||||||
|
const IdDecorator = field.dataType.options?.primaryKey
|
||||||
|
? PrimaryKey({
|
||||||
|
columnType: "text",
|
||||||
|
type: "string",
|
||||||
|
nullable: false,
|
||||||
|
})
|
||||||
|
: Property({
|
||||||
|
columnType: "text",
|
||||||
|
type: "string",
|
||||||
|
nullable: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
IdDecorator(MikroORMEntity.prototype, field.fieldName)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to generate entity within the code
|
||||||
|
*/
|
||||||
|
MikroORMEntity.prototype.generateId = function () {
|
||||||
|
this[field.fieldName] = generateEntityId(
|
||||||
|
this[field.fieldName],
|
||||||
|
field.dataType.options?.prefix
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute hook via lifecycle decorators
|
||||||
|
*/
|
||||||
|
BeforeCreate()(MikroORMEntity.prototype, "generateId")
|
||||||
|
OnInit()(MikroORMEntity.prototype, "generateId")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define rest of properties
|
||||||
|
*/
|
||||||
|
const columnType = COLUMN_TYPES[field.dataType.name]
|
||||||
|
const propertyType = PROPERTY_TYPES[field.dataType.name]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defining a primary key property
|
||||||
|
*/
|
||||||
|
if (field.dataType.options?.primaryKey) {
|
||||||
|
PrimaryKey({
|
||||||
|
columnType,
|
||||||
|
type: propertyType,
|
||||||
|
nullable: false,
|
||||||
|
})(MikroORMEntity.prototype, field.fieldName)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Property({
|
||||||
|
columnType,
|
||||||
|
type: propertyType,
|
||||||
|
nullable: field.nullable,
|
||||||
|
/**
|
||||||
|
* MikroORM does not ignore undefined values for default when generating
|
||||||
|
* the database schema SQL. Conditionally add it here to prevent undefined
|
||||||
|
* from being set as default value in SQL.
|
||||||
|
*/
|
||||||
|
...(isDefined(field.defaultValue) && { default: field.defaultValue }),
|
||||||
|
})(MikroORMEntity.prototype, field.fieldName)
|
||||||
|
}
|
||||||
@@ -0,0 +1,403 @@
|
|||||||
|
import {
|
||||||
|
EntityCascades,
|
||||||
|
EntityConstructor,
|
||||||
|
PropertyType,
|
||||||
|
RelationshipMetadata,
|
||||||
|
RelationshipType,
|
||||||
|
} from "@medusajs/types"
|
||||||
|
import { DmlEntity } from "../../entity"
|
||||||
|
import { parseEntityName } from "./parse-entity-name"
|
||||||
|
import {
|
||||||
|
BeforeCreate,
|
||||||
|
ManyToMany,
|
||||||
|
ManyToOne,
|
||||||
|
OneToMany,
|
||||||
|
OneToOne,
|
||||||
|
OnInit,
|
||||||
|
Property,
|
||||||
|
} from "@mikro-orm/core"
|
||||||
|
import { camelToSnakeCase, pluralize } from "../../../common"
|
||||||
|
import { HasMany } from "../../relations/has-many"
|
||||||
|
import { HasOne } from "../../relations/has-one"
|
||||||
|
import { ManyToMany as DmlManyToMany } from "../../relations/many-to-many"
|
||||||
|
|
||||||
|
type Context = {
|
||||||
|
MANY_TO_MANY_TRACKED_REALTIONS: Record<string, boolean>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines has one relationship on the Mikro ORM entity.
|
||||||
|
*/
|
||||||
|
export function defineHasOneRelationship(
|
||||||
|
MikroORMEntity: EntityConstructor<any>,
|
||||||
|
relationship: RelationshipMetadata,
|
||||||
|
{ relatedModelName }: { relatedModelName: string },
|
||||||
|
cascades: EntityCascades<string[]>
|
||||||
|
) {
|
||||||
|
const shouldRemoveRelated = !!cascades.delete?.includes(relationship.name)
|
||||||
|
|
||||||
|
OneToOne({
|
||||||
|
entity: relatedModelName,
|
||||||
|
nullable: relationship.nullable,
|
||||||
|
mappedBy: relationship.mappedBy || camelToSnakeCase(MikroORMEntity.name),
|
||||||
|
cascade: shouldRemoveRelated
|
||||||
|
? (["perist", "soft-remove"] as any)
|
||||||
|
: undefined,
|
||||||
|
})(MikroORMEntity.prototype, relationship.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines has many relationship on the Mikro ORM entity
|
||||||
|
*/
|
||||||
|
export function defineHasManyRelationship(
|
||||||
|
MikroORMEntity: EntityConstructor<any>,
|
||||||
|
relationship: RelationshipMetadata,
|
||||||
|
{ relatedModelName }: { relatedModelName: string },
|
||||||
|
cascades: EntityCascades<string[]>
|
||||||
|
) {
|
||||||
|
const shouldRemoveRelated = !!cascades.delete?.includes(relationship.name)
|
||||||
|
|
||||||
|
OneToMany({
|
||||||
|
entity: relatedModelName,
|
||||||
|
orphanRemoval: true,
|
||||||
|
mappedBy: relationship.mappedBy || camelToSnakeCase(MikroORMEntity.name),
|
||||||
|
cascade: shouldRemoveRelated
|
||||||
|
? (["perist", "soft-remove"] as any)
|
||||||
|
: undefined,
|
||||||
|
})(MikroORMEntity.prototype, relationship.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines belongs to relationship on the Mikro ORM entity. The belongsTo
|
||||||
|
* relationship inspects the related entity for the other side of
|
||||||
|
* the relationship and then uses one of the following Mikro ORM
|
||||||
|
* relationship.
|
||||||
|
*
|
||||||
|
* - OneToOne: When the other side uses "hasOne" with "owner: true"
|
||||||
|
* - ManyToOne: When the other side uses "hasMany"
|
||||||
|
*/
|
||||||
|
export function defineBelongsToRelationship(
|
||||||
|
MikroORMEntity: EntityConstructor<any>,
|
||||||
|
relationship: RelationshipMetadata,
|
||||||
|
relatedEntity: DmlEntity<
|
||||||
|
Record<string, PropertyType<any> | RelationshipType<any>>
|
||||||
|
>,
|
||||||
|
{ relatedModelName }: { relatedModelName: string }
|
||||||
|
) {
|
||||||
|
const mappedBy =
|
||||||
|
relationship.mappedBy || camelToSnakeCase(MikroORMEntity.name)
|
||||||
|
const { schema: relationSchema, cascades: relationCascades } =
|
||||||
|
relatedEntity.parse()
|
||||||
|
|
||||||
|
const otherSideRelation = relationSchema[mappedBy]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In DML the relationships are cascaded from parent to child. A belongsTo
|
||||||
|
* relationship is always a child, therefore we look at the parent and
|
||||||
|
* define a onDelete: cascade when we are included in the delete
|
||||||
|
* list of parent cascade.
|
||||||
|
*/
|
||||||
|
const shouldCascade = relationCascades.delete?.includes(mappedBy)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the mapped by is defined as relationship on the other side
|
||||||
|
*/
|
||||||
|
if (!otherSideRelation) {
|
||||||
|
throw new Error(
|
||||||
|
`Missing property "${mappedBy}" on "${relatedModelName}" entity. Make sure to define it as a relationship`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyForeignKeyAssignationHooks(foreignKeyName: string) {
|
||||||
|
const hookName = `assignRelationFromForeignKeyValue${foreignKeyName}`
|
||||||
|
/**
|
||||||
|
* Hook to handle foreign key assignation
|
||||||
|
*/
|
||||||
|
MikroORMEntity.prototype[hookName] = function () {
|
||||||
|
this[relationship.name] ??= this[foreignKeyName]
|
||||||
|
this[foreignKeyName] ??= this[relationship.name]?.id
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute hook via lifecycle decorators
|
||||||
|
*/
|
||||||
|
BeforeCreate()(MikroORMEntity.prototype, hookName)
|
||||||
|
OnInit()(MikroORMEntity.prototype, hookName)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Otherside is a has many. Hence we should defined a ManyToOne
|
||||||
|
*/
|
||||||
|
if (
|
||||||
|
HasMany.isHasMany(otherSideRelation) ||
|
||||||
|
DmlManyToMany.isManyToMany(otherSideRelation)
|
||||||
|
) {
|
||||||
|
const foreignKeyName = camelToSnakeCase(`${relationship.name}Id`)
|
||||||
|
|
||||||
|
ManyToOne({
|
||||||
|
entity: relatedModelName,
|
||||||
|
columnType: "text",
|
||||||
|
mapToPk: true,
|
||||||
|
fieldName: camelToSnakeCase(`${relationship.name}Id`),
|
||||||
|
nullable: relationship.nullable,
|
||||||
|
onDelete: shouldCascade ? "cascade" : undefined,
|
||||||
|
})(MikroORMEntity.prototype, camelToSnakeCase(`${relationship.name}Id`))
|
||||||
|
|
||||||
|
if (DmlManyToMany.isManyToMany(otherSideRelation)) {
|
||||||
|
Property({
|
||||||
|
type: relatedModelName,
|
||||||
|
persist: false,
|
||||||
|
nullable: relationship.nullable,
|
||||||
|
})(MikroORMEntity.prototype, relationship.name)
|
||||||
|
} else {
|
||||||
|
// HasMany case
|
||||||
|
ManyToOne({
|
||||||
|
entity: relatedModelName,
|
||||||
|
persist: false,
|
||||||
|
nullable: relationship.nullable,
|
||||||
|
})(MikroORMEntity.prototype, relationship.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
applyForeignKeyAssignationHooks(foreignKeyName)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Otherside is a has one. Hence we should defined a OneToOne
|
||||||
|
*/
|
||||||
|
if (HasOne.isHasOne(otherSideRelation)) {
|
||||||
|
const foreignKeyName = camelToSnakeCase(`${relationship.name}Id`)
|
||||||
|
|
||||||
|
OneToOne({
|
||||||
|
entity: relatedModelName,
|
||||||
|
nullable: relationship.nullable,
|
||||||
|
mappedBy: mappedBy,
|
||||||
|
owner: true,
|
||||||
|
onDelete: shouldCascade ? "cascade" : undefined,
|
||||||
|
})(MikroORMEntity.prototype, relationship.name)
|
||||||
|
|
||||||
|
if (relationship.nullable) {
|
||||||
|
Object.defineProperty(MikroORMEntity.prototype, foreignKeyName, {
|
||||||
|
value: null,
|
||||||
|
configurable: true,
|
||||||
|
enumerable: true,
|
||||||
|
writable: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Property({
|
||||||
|
type: "string",
|
||||||
|
columnType: "text",
|
||||||
|
nullable: relationship.nullable,
|
||||||
|
})(MikroORMEntity.prototype, foreignKeyName)
|
||||||
|
|
||||||
|
applyForeignKeyAssignationHooks(foreignKeyName)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Other side is some unsupported data-type
|
||||||
|
*/
|
||||||
|
throw new Error(
|
||||||
|
`Invalid relationship reference for "${mappedBy}" on "${relatedModelName}" entity. Make sure to define a hasOne or hasMany relationship`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines a many to many relationship on the Mikro ORM entity
|
||||||
|
*/
|
||||||
|
export function defineManyToManyRelationship(
|
||||||
|
MikroORMEntity: EntityConstructor<any>,
|
||||||
|
relationship: RelationshipMetadata,
|
||||||
|
relatedEntity: DmlEntity<
|
||||||
|
Record<string, PropertyType<any> | RelationshipType<any>>
|
||||||
|
>,
|
||||||
|
{
|
||||||
|
relatedModelName,
|
||||||
|
pgSchema,
|
||||||
|
}: { relatedModelName: string; pgSchema: string | undefined },
|
||||||
|
{ MANY_TO_MANY_TRACKED_REALTIONS }: Context
|
||||||
|
) {
|
||||||
|
let mappedBy = relationship.mappedBy
|
||||||
|
let inversedBy: undefined | string
|
||||||
|
let pivotEntityName: undefined | string
|
||||||
|
let pivotTableName: undefined | string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validating other side of relationship when mapped by is defined
|
||||||
|
*/
|
||||||
|
if (mappedBy) {
|
||||||
|
const otherSideRelation = relatedEntity.parse().schema[mappedBy]
|
||||||
|
if (!otherSideRelation) {
|
||||||
|
throw new Error(
|
||||||
|
`Missing property "${mappedBy}" on "${relatedModelName}" entity. Make sure to define it as a relationship`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!DmlManyToMany.isManyToMany(otherSideRelation)) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid relationship reference for "${mappedBy}" on "${relatedModelName}" entity. Make sure to define a manyToMany relationship`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the other side has defined a mapped by and if that
|
||||||
|
* mapping is already tracked as the owner.
|
||||||
|
*
|
||||||
|
* - If yes, we will inverse our mapped by
|
||||||
|
* - Otherwise, we will track ourselves as the owner.
|
||||||
|
*/
|
||||||
|
if (
|
||||||
|
otherSideRelation.parse(mappedBy).mappedBy &&
|
||||||
|
MANY_TO_MANY_TRACKED_REALTIONS[`${relatedModelName}.${mappedBy}`]
|
||||||
|
) {
|
||||||
|
inversedBy = mappedBy
|
||||||
|
mappedBy = undefined
|
||||||
|
} else {
|
||||||
|
MANY_TO_MANY_TRACKED_REALTIONS[
|
||||||
|
`${MikroORMEntity.name}.${relationship.name}`
|
||||||
|
] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validating pivot entity when it is defined and computing
|
||||||
|
* its name
|
||||||
|
*/
|
||||||
|
if (relationship.options.pivotEntity) {
|
||||||
|
if (typeof relationship.options.pivotEntity !== "function") {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid pivotEntity reference for "${MikroORMEntity.name}.${relationship.name}". Make sure to define the pivotEntity using a factory function`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const pivotEntity = relationship.options.pivotEntity()
|
||||||
|
if (!DmlEntity.isDmlEntity(pivotEntity)) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid pivotEntity reference for "${MikroORMEntity.name}.${relationship.name}". Make sure to return a DML entity from the pivotEntity callback`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pivotEntityName = parseEntityName(pivotEntity).modelName
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pivotEntityName) {
|
||||||
|
/**
|
||||||
|
* Pivot table name is created as follows (when not explicitly provided)
|
||||||
|
*
|
||||||
|
* - Combining both the entity's names.
|
||||||
|
* - Sorting them by alphabetical order
|
||||||
|
* - Converting them from camelCase to snake_case.
|
||||||
|
* - And finally pluralizing the second entity name.
|
||||||
|
*/
|
||||||
|
pivotTableName =
|
||||||
|
relationship.options.pivotTable ??
|
||||||
|
[MikroORMEntity.name.toLowerCase(), relatedModelName.toLowerCase()]
|
||||||
|
.sort()
|
||||||
|
.map((token, index) => {
|
||||||
|
if (index === 1) {
|
||||||
|
return pluralize(camelToSnakeCase(token))
|
||||||
|
}
|
||||||
|
return camelToSnakeCase(token)
|
||||||
|
})
|
||||||
|
.join("_")
|
||||||
|
}
|
||||||
|
|
||||||
|
ManyToMany({
|
||||||
|
entity: relatedModelName,
|
||||||
|
...(pivotTableName
|
||||||
|
? {
|
||||||
|
pivotTable: pgSchema
|
||||||
|
? `${pgSchema}.${pivotTableName}`
|
||||||
|
: pivotTableName,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
...(pivotEntityName ? { pivotEntity: pivotEntityName } : {}),
|
||||||
|
...(mappedBy ? { mappedBy: mappedBy as any } : {}),
|
||||||
|
...(inversedBy ? { inversedBy: inversedBy as any } : {}),
|
||||||
|
})(MikroORMEntity.prototype, relationship.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines a DML entity schema field as a Mikro ORM relationship
|
||||||
|
*/
|
||||||
|
export function defineRelationship(
|
||||||
|
MikroORMEntity: EntityConstructor<any>,
|
||||||
|
relationship: RelationshipMetadata,
|
||||||
|
cascades: EntityCascades<string[]>,
|
||||||
|
context: Context
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* We expect the relationship.entity to be a function that
|
||||||
|
* lazily returns the related entity
|
||||||
|
*/
|
||||||
|
const relatedEntity =
|
||||||
|
typeof relationship.entity === "function"
|
||||||
|
? (relationship.entity() as unknown)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Since we don't type-check relationships, we should validate
|
||||||
|
* them at runtime
|
||||||
|
*/
|
||||||
|
if (!relatedEntity) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid relationship reference for "${MikroORMEntity.name}.${relationship.name}". Make sure to define the relationship using a factory function`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the return value is a DML entity instance
|
||||||
|
*/
|
||||||
|
if (!DmlEntity.isDmlEntity(relatedEntity)) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid relationship reference for "${MikroORMEntity.name}.${relationship.name}". Make sure to return a DML entity from the relationship callback`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { modelName, tableName, pgSchema } = parseEntityName(relatedEntity)
|
||||||
|
const relatedEntityInfo = {
|
||||||
|
relatedModelName: modelName,
|
||||||
|
relatedTableName: tableName,
|
||||||
|
pgSchema,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defining relationships
|
||||||
|
*/
|
||||||
|
switch (relationship.type) {
|
||||||
|
case "hasOne":
|
||||||
|
defineHasOneRelationship(
|
||||||
|
MikroORMEntity,
|
||||||
|
relationship,
|
||||||
|
relatedEntityInfo,
|
||||||
|
cascades
|
||||||
|
)
|
||||||
|
break
|
||||||
|
case "hasMany":
|
||||||
|
defineHasManyRelationship(
|
||||||
|
MikroORMEntity,
|
||||||
|
relationship,
|
||||||
|
relatedEntityInfo,
|
||||||
|
cascades
|
||||||
|
)
|
||||||
|
break
|
||||||
|
case "belongsTo":
|
||||||
|
defineBelongsToRelationship(
|
||||||
|
MikroORMEntity,
|
||||||
|
relationship,
|
||||||
|
relatedEntity,
|
||||||
|
relatedEntityInfo
|
||||||
|
)
|
||||||
|
break
|
||||||
|
case "manyToMany":
|
||||||
|
defineManyToManyRelationship(
|
||||||
|
MikroORMEntity,
|
||||||
|
relationship,
|
||||||
|
relatedEntity,
|
||||||
|
relatedEntityInfo,
|
||||||
|
context
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { DmlEntity } from "../../entity"
|
||||||
|
import { camelToSnakeCase, toCamelCase, upperCaseFirst } from "../../../common"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses entity name and returns model and table name from
|
||||||
|
* it
|
||||||
|
*/
|
||||||
|
export function parseEntityName(entity: DmlEntity<any>) {
|
||||||
|
const parsedEntity = entity.parse()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Table name is going to be the snake case version of the entity name.
|
||||||
|
* Here we should preserve PG schema (if defined).
|
||||||
|
*
|
||||||
|
* For example: "platform.user" should stay as "platform.user"
|
||||||
|
*/
|
||||||
|
const tableName = camelToSnakeCase(parsedEntity.tableName)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entity name is going to be the camelCase version of the
|
||||||
|
* name defined by the user
|
||||||
|
*/
|
||||||
|
const [pgSchema, ...rest] = tableName.split(".")
|
||||||
|
return {
|
||||||
|
tableName,
|
||||||
|
modelName: upperCaseFirst(toCamelCase(parsedEntity.name)),
|
||||||
|
pgSchema: rest.length ? pgSchema : undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { BaseProperty } from "./base"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ArrayProperty is used to define an array property
|
||||||
|
*/
|
||||||
|
export class ArrayProperty extends BaseProperty<string[]> {
|
||||||
|
protected dataType = {
|
||||||
|
name: "array",
|
||||||
|
} as const
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user