Mark keys as primary with explicit method call (#7900)

This commit is contained in:
Harminder Virk
2024-07-02 13:27:21 +05:30
committed by GitHub
parent b25c6ab54f
commit 074e4a888e
13 changed files with 239 additions and 438 deletions
+16 -4
View File
@@ -1,10 +1,21 @@
/**
* Symbol to identify a DML entity from an object
*/
export const IsDmlEntity = Symbol.for("isDmlEntity") export const IsDmlEntity = Symbol.for("isDmlEntity")
/**
* Representation of DML schema. It must be a key-value pair
* with string based keys and properties/relationships
* as the value.
*/
export type DMLSchema = Record< export type DMLSchema = Record<
string, string,
PropertyType<any> | RelationshipType<any> PropertyType<any> | RelationshipType<any>
> >
/**
* Representation of a DML entity
*/
export interface IDmlEntity<Schema extends DMLSchema> { export interface IDmlEntity<Schema extends DMLSchema> {
[IsDmlEntity]: true [IsDmlEntity]: true
schema: Schema schema: Schema
@@ -66,9 +77,9 @@ export type PropertyType<T> = {
*/ */
export type RelationshipOptions = { export type RelationshipOptions = {
/** /**
* The name of the relationship as defined * The name of the relationship as defined in the other
* in the other data model. This is only required * data model. This is only required by the `belongsTo`
* by the `belongsTo` relationship method. * relationship method.
*/ */
mappedBy?: string mappedBy?: string
} & Record<string, any> } & Record<string, any>
@@ -107,7 +118,8 @@ export interface EntityConstructor<Props> extends Function {
} }
/** /**
* From a IDmlEntity, infer the foreign keys name and type for belongsTo relation meaning hasOne and ManyToOne * From a IDmlEntity, infer the foreign keys name and type for
* "belongsTo" relation meaning "hasOne" and "ManyToOne"
*/ */
export type InferForeignKeys<T> = T extends IDmlEntity<infer Schema> export type InferForeignKeys<T> = T extends IDmlEntity<infer Schema>
? { ? {
@@ -1291,7 +1291,8 @@ describe("Entity builder", () => {
columnType: "text", columnType: "text",
name: "id", name: "id",
nullable: false, nullable: false,
primary: true, getter: false,
setter: false,
}, },
username: { username: {
reference: "scalar", reference: "scalar",
@@ -1346,9 +1347,9 @@ describe("Entity builder", () => {
}) })
}) })
test("mark id as non-primary", () => { test("mark id as primary", () => {
const user = model.define("user", { const user = model.define("user", {
id: model.id({ primaryKey: false }), id: model.id().primaryKey(),
username: model.text(), username: model.text(),
email: model.text(), email: model.text(),
}) })
@@ -1391,8 +1392,7 @@ describe("Entity builder", () => {
columnType: "text", columnType: "text",
name: "id", name: "id",
nullable: false, nullable: false,
getter: false, primary: true,
setter: false,
}, },
username: { username: {
reference: "scalar", reference: "scalar",
@@ -1451,7 +1451,7 @@ describe("Entity builder", () => {
test("define prefix for the id", () => { test("define prefix for the id", () => {
const user = model.define("user", { const user = model.define("user", {
id: model.id({ primaryKey: false, prefix: "us" }), id: model.id({ prefix: "us" }).primaryKey(),
username: model.text(), username: model.text(),
email: model.text(), email: model.text(),
}) })
@@ -1494,8 +1494,7 @@ describe("Entity builder", () => {
columnType: "text", columnType: "text",
name: "id", name: "id",
nullable: false, nullable: false,
getter: false, primary: true,
setter: false,
}, },
username: { username: {
reference: "scalar", reference: "scalar",
@@ -1554,124 +1553,25 @@ describe("Entity builder", () => {
}) })
describe("Entity builder | primaryKey", () => { describe("Entity builder | primaryKey", () => {
test("should create both id fields and primaryKey fields", () => { test("should infer primaryKeys from a model", () => {
const user = model.define("user", { const user = model.define("user", {
id: model.id(), id: model.id().primaryKey(),
email: model.text().primaryKey(), email: model.text().primaryKey(),
account_id: model.number().primaryKey(), account_id: model.number(),
}) })
const entityBuilder = createMikrORMEntity() const entityBuilder = createMikrORMEntity()
const User = entityBuilder(user) const User = entityBuilder(user)
expectTypeOf(new User()).toMatchTypeOf<{
id: string
email: string
account_id: number
}>()
const metaData = MetadataStorage.getMetadataFromDecorator(User) const metaData = MetadataStorage.getMetadataFromDecorator(User)
expect(metaData.properties).toEqual({
id: {
reference: "scalar",
type: "string",
columnType: "text",
name: "id",
nullable: false,
getter: false,
setter: false,
},
email: {
columnType: "text",
name: "email",
nullable: false,
primary: true,
reference: "scalar",
type: "string",
},
account_id: {
columnType: "integer",
name: "account_id",
nullable: false,
primary: true,
reference: "scalar",
type: "number",
},
created_at: {
reference: "scalar",
type: "date",
columnType: "timestamptz",
name: "created_at",
defaultRaw: "now()",
onCreate: expect.any(Function),
nullable: false,
getter: false,
setter: false,
},
updated_at: {
reference: "scalar",
type: "date",
columnType: "timestamptz",
name: "updated_at",
defaultRaw: "now()",
onCreate: expect.any(Function),
onUpdate: expect.any(Function),
nullable: false,
getter: false,
setter: false,
},
deleted_at: {
reference: "scalar",
type: "date",
columnType: "timestamptz",
name: "deleted_at",
nullable: true,
getter: false,
setter: false,
},
})
})
test("should infer primaryKeys from a model", () => {
let user = model.define("user", {
id: model.id(),
email: model.text(),
account_id: model.number(),
})
const entityBuilder = createMikrORMEntity()
let User = entityBuilder(user)
let metaData = MetadataStorage.getMetadataFromDecorator(User)
expect(metaData.properties.id).toEqual({ expect(metaData.properties.id).toEqual({
columnType: "text", columnType: "text",
name: "id", name: "id",
nullable: false, nullable: false,
reference: "scalar",
type: "string",
primary: true, primary: true,
reference: "scalar",
type: "string",
}) })
user = model.define("user", {
id: model.id(),
email: model.text().primaryKey(),
account_id: model.number(),
})
User = entityBuilder(user)
metaData = MetadataStorage.getMetadataFromDecorator(User)
expect(metaData.properties.id).toEqual({
columnType: "text",
name: "id",
nullable: false,
reference: "scalar",
type: "string",
getter: false,
setter: false,
})
expect(metaData.properties.email).toEqual({ expect(metaData.properties.email).toEqual({
columnType: "text", columnType: "text",
name: "email", name: "email",
@@ -1680,53 +1580,6 @@ describe("Entity builder", () => {
type: "string", type: "string",
primary: true, primary: true,
}) })
expect(metaData.properties.account_id).toEqual({
columnType: "integer",
name: "account_id",
nullable: false,
reference: "scalar",
type: "number",
getter: false,
setter: false,
})
user = model.define("user", {
id: model.id(),
email: model.text().primaryKey(),
account_id: model.number().primaryKey(),
})
User = entityBuilder(user)
metaData = MetadataStorage.getMetadataFromDecorator(User)
expect(metaData.properties.id).toEqual({
columnType: "text",
name: "id",
nullable: false,
reference: "scalar",
type: "string",
getter: false,
setter: false,
})
expect(metaData.properties.email).toEqual({
columnType: "text",
name: "email",
nullable: false,
reference: "scalar",
type: "string",
primary: true,
})
expect(metaData.properties.account_id).toEqual({
columnType: "integer",
name: "account_id",
nullable: false,
reference: "scalar",
type: "number",
primary: true,
})
}) })
}) })
@@ -5,24 +5,6 @@ describe("Id property", () => {
test("create id property type", () => { test("create id property type", () => {
const property = new IdProperty() const property = new IdProperty()
expectTypeOf(property["$dataType"]).toEqualTypeOf<string>()
expect(property.parse("id")).toEqual({
fieldName: "id",
dataType: {
name: "id",
options: {
primaryKey: true,
},
},
nullable: false,
indexes: [],
relationships: [],
})
})
test("create id property type with marking it as a primary key", () => {
const property = new IdProperty({ primaryKey: false })
expectTypeOf(property["$dataType"]).toEqualTypeOf<string>() expectTypeOf(property["$dataType"]).toEqualTypeOf<string>()
expect(property.parse("id")).toEqual({ expect(property.parse("id")).toEqual({
fieldName: "id", fieldName: "id",
@@ -37,4 +19,22 @@ describe("Id property", () => {
relationships: [], relationships: [],
}) })
}) })
test("create id property type with marking it as a primary key", () => {
const property = new IdProperty().primaryKey()
expectTypeOf(property["$dataType"]).toEqualTypeOf<string>()
expect(property.parse("id")).toEqual({
fieldName: "id",
dataType: {
name: "id",
options: {
primaryKey: true,
},
},
nullable: false,
indexes: [],
relationships: [],
})
})
}) })
@@ -17,4 +17,20 @@ describe("Text property", () => {
relationships: [], relationships: [],
}) })
}) })
test("mark text property as primary key", () => {
const property = new TextProperty().primaryKey()
expectTypeOf(property["$dataType"]).toEqualTypeOf<string>()
expect(property.parse("username")).toEqual({
fieldName: "username",
dataType: {
name: "text",
options: { primaryKey: true, searchable: false },
},
nullable: false,
indexes: [],
relationships: [],
})
})
}) })
+112 -116
View File
@@ -2,7 +2,6 @@ import type { DMLSchema, RelationshipOptions } from "@medusajs/types"
import { DmlEntity } from "./entity" import { DmlEntity } from "./entity"
import { createBigNumberProperties } from "./helpers/entity-builder/create-big-number-properties" import { createBigNumberProperties } from "./helpers/entity-builder/create-big-number-properties"
import { createDefaultProperties } from "./helpers/entity-builder/create-default-properties" import { createDefaultProperties } from "./helpers/entity-builder/create-default-properties"
import { inferPrimaryKeyProperties } from "./helpers/entity-builder/infer-primary-key-properties"
import { ArrayProperty } from "./properties/array" import { ArrayProperty } from "./properties/array"
import { BigNumberProperty } from "./properties/big-number" import { BigNumberProperty } from "./properties/big-number"
import { BooleanProperty } from "./properties/boolean" import { BooleanProperty } from "./properties/boolean"
@@ -22,43 +21,45 @@ import { ManyToMany } from "./relations/many-to-many"
*/ */
const IMPLICIT_PROPERTIES = ["created_at", "updated_at", "deleted_at"] const IMPLICIT_PROPERTIES = ["created_at", "updated_at", "deleted_at"]
export type DefineOptions = string | { export type DefineOptions =
/** | string
* The data model's name. | {
*/ /**
name?: string * The data model's name.
/** */
* The name of the data model's table in the database. name?: string
*/ /**
tableName: string * The name of the data model's table in the database.
} */
tableName: string
}
export type ManyToManyOptions = RelationshipOptions & export type ManyToManyOptions = RelationshipOptions &
( (
| { | {
/** /**
* The name of the pivot table * The name of the pivot table
* created in the database for this relationship. * created in the database for this relationship.
*/ */
pivotTable?: string pivotTable?: string
/** /**
* @ignore * @ignore
*/ */
pivotEntity?: never pivotEntity?: never
} }
| { | {
/** /**
* @ignore * @ignore
*/ */
pivotTable?: never pivotTable?: never
/** /**
* A function that returns the data model * A function that returns the data model
* representing the pivot table created in the * representing the pivot table created in the
* database for this relationship. * database for this relationship.
*/ */
pivotEntity?: () => DmlEntity<any> pivotEntity?: () => DmlEntity<any>
} }
) )
/** /**
* Entity builder exposes the API to create an entity and define its * Entity builder exposes the API to create an entity and define its
@@ -81,21 +82,21 @@ export class EntityBuilder {
/** /**
* This method defines a data model. * This method defines a data model.
* *
* @typeParam Schema - The type of the accepted schema in the second parameter of the method. * @typeParam Schema - The type of the accepted schema in the second parameter of the method.
* *
* @param {DefineOptions} nameOrConfig - Either the data model's name, or configurations to name the data model. * @param {DefineOptions} nameOrConfig - Either the data model's name, or configurations to name the data model.
* The data model's name must be unique. * The data model's name must be unique.
* @param {Schema} schema - The schema of the data model's properties. * @param {Schema} schema - The schema of the data model's properties.
* *
* @example * @example
* import { model } from "@medusajs/utils" * import { model } from "@medusajs/utils"
* *
* const MyCustom = model.define("my_custom", { * const MyCustom = model.define("my_custom", {
* id: model.id(), * id: model.id(),
* name: model.text(), * name: model.text(),
* }) * })
* *
* export default MyCustom * export default MyCustom
*/ */
define<Schema extends DMLSchema>( define<Schema extends DMLSchema>(
@@ -103,7 +104,6 @@ export class EntityBuilder {
schema: Schema schema: Schema
) { ) {
this.#disallowImplicitProperties(schema) this.#disallowImplicitProperties(schema)
schema = inferPrimaryKeyProperties(schema)
return new DmlEntity(nameOrConfig, { return new DmlEntity(nameOrConfig, {
...schema, ...schema,
@@ -114,40 +114,39 @@ export class EntityBuilder {
/** /**
* This method defines an automatically generated string ID property. * This method defines an automatically generated string ID property.
* *
* By default, this property is considered to be the data models primary key. * You must use the "primaryKey" modifier to mark the property as the
* * primary key.
* @param {ConstructorParameters<typeof IdProperty>[0]} options - The ID's options. *
*
* @example * @example
* import { model } from "@medusajs/utils" * import { model } from "@medusajs/utils"
* *
* const MyCustom = model.define("my_custom", { * const User = model.define("User", {
* id: model.id(), * id: model.id().primaryKey(),
* // ... * // ...
* }) * })
* *
* export default MyCustom * export default User
* *
* @customNamespace Property Types * @customNamespace Property Types
*/ */
id(options?: ConstructorParameters<typeof IdProperty>[0]) { id(options?: { prefix?: string }) {
return new IdProperty(options) return new IdProperty(options)
} }
/** /**
* This method defines a string property. * This method defines a string property.
* *
* @example * @example
* import { model } from "@medusajs/utils" * import { model } from "@medusajs/utils"
* *
* const MyCustom = model.define("my_custom", { * const MyCustom = model.define("my_custom", {
* name: model.text(), * name: model.text(),
* // ... * // ...
* }) * })
* *
* export default MyCustom * export default MyCustom
* *
* @customNamespace Property Types * @customNamespace Property Types
*/ */
text() { text() {
@@ -156,17 +155,17 @@ export class EntityBuilder {
/** /**
* This method defines a boolean property. * This method defines a boolean property.
* *
* @example * @example
* import { model } from "@medusajs/utils" * import { model } from "@medusajs/utils"
* *
* const MyCustom = model.define("my_custom", { * const MyCustom = model.define("my_custom", {
* hasAccount: model.boolean(), * hasAccount: model.boolean(),
* // ... * // ...
* }) * })
* *
* export default MyCustom * export default MyCustom
* *
* @customNamespace Property Types * @customNamespace Property Types
*/ */
boolean() { boolean() {
@@ -175,17 +174,17 @@ export class EntityBuilder {
/** /**
* This method defines a number property. * This method defines a number property.
* *
* @example * @example
* import { model } from "@medusajs/utils" * import { model } from "@medusajs/utils"
* *
* const MyCustom = model.define("my_custom", { * const MyCustom = model.define("my_custom", {
* age: model.number(), * age: model.number(),
* // ... * // ...
* }) * })
* *
* export default MyCustom * export default MyCustom
* *
* @customNamespace Property Types * @customNamespace Property Types
*/ */
number() { number() {
@@ -194,19 +193,19 @@ export class EntityBuilder {
/** /**
* This method defines a number property that expects large numbers, such as prices. * This method defines a number property that expects large numbers, such as prices.
* *
* @example * @example
* import { model } from "@medusajs/utils" * import { model } from "@medusajs/utils"
* *
* const MyCustom = model.define("my_custom", { * const MyCustom = model.define("my_custom", {
* price: model.bigNumber(), * price: model.bigNumber(),
* // ... * // ...
* }) * })
* *
* export default MyCustom * export default MyCustom
* *
* @customNamespace Property Types * @customNamespace Property Types
* *
* @privateRemarks * @privateRemarks
* This property produces an additional * This property produces an additional
* column - raw_{{ property_name }}, which stores the configuration * column - raw_{{ property_name }}, which stores the configuration
@@ -218,17 +217,17 @@ export class EntityBuilder {
/** /**
* This method defines an array of strings property. * This method defines an array of strings property.
* *
* @example * @example
* import { model } from "@medusajs/utils" * import { model } from "@medusajs/utils"
* *
* const MyCustom = model.define("my_custom", { * const MyCustom = model.define("my_custom", {
* names: model.array(), * names: model.array(),
* // ... * // ...
* }) * })
* *
* export default MyCustom * export default MyCustom
* *
* @customNamespace Property Types * @customNamespace Property Types
*/ */
array() { array() {
@@ -237,17 +236,17 @@ export class EntityBuilder {
/** /**
* This method defines a timestamp property. * This method defines a timestamp property.
* *
* @example * @example
* import { model } from "@medusajs/utils" * import { model } from "@medusajs/utils"
* *
* const MyCustom = model.define("my_custom", { * const MyCustom = model.define("my_custom", {
* date_of_birth: model.dateTime(), * date_of_birth: model.dateTime(),
* // ... * // ...
* }) * })
* *
* export default MyCustom * export default MyCustom
* *
* @customNamespace Property Types * @customNamespace Property Types
*/ */
dateTime() { dateTime() {
@@ -256,17 +255,17 @@ export class EntityBuilder {
/** /**
* This method defines a property whose value is a stringified JSON object. * This method defines a property whose value is a stringified JSON object.
* *
* @example * @example
* import { model } from "@medusajs/utils" * import { model } from "@medusajs/utils"
* *
* const MyCustom = model.define("my_custom", { * const MyCustom = model.define("my_custom", {
* metadata: model.json(), * metadata: model.json(),
* // ... * // ...
* }) * })
* *
* export default MyCustom * export default MyCustom
* *
* @customNamespace Property Types * @customNamespace Property Types
*/ */
json() { json() {
@@ -275,21 +274,21 @@ export class EntityBuilder {
/** /**
* This method defines a property whose value can only be one of the specified values. * This method defines a property whose value can only be one of the specified values.
* *
* @typeParam Values - The type of possible values. By default, it's `string`. * @typeParam Values - The type of possible values. By default, it's `string`.
* *
* @param {Values[]} values - An array of possible values. * @param {Values[]} values - An array of possible values.
* *
* @example * @example
* import { model } from "@medusajs/utils" * import { model } from "@medusajs/utils"
* *
* const MyCustom = model.define("my_custom", { * const MyCustom = model.define("my_custom", {
* color: model.enum(["black", "white"]), * color: model.enum(["black", "white"]),
* // ... * // ...
* }) * })
* *
* export default MyCustom * export default MyCustom
* *
* @customNamespace Property Types * @customNamespace Property Types
*/ */
enum<const Values extends unknown>(values: Values[]) { enum<const Values extends unknown>(values: Values[]) {
@@ -302,24 +301,24 @@ export class EntityBuilder {
* data model. * data model.
* *
* For example: A user "hasOne" email. * For example: A user "hasOne" email.
* *
* Use the {@link belongsTo} method to define the inverse of this relationship in * Use the {@link belongsTo} method to define the inverse of this relationship in
* the other data model. * the other data model.
* *
* @typeParam T - The type of the entity builder passed as a first parameter. By default, it's * @typeParam T - The type of the entity builder passed as a first parameter. By default, it's
* a function returning the related model. * a function returning the related model.
* *
* @param {T} entityBuilder - A function that returns the data model this model is related to. * @param {T} entityBuilder - A function that returns the data model this model is related to.
* @param {RelationshipOptions} options - The relationship's options. * @param {RelationshipOptions} options - The relationship's options.
* *
* @example * @example
* import { model } from "@medusajs/utils" * import { model } from "@medusajs/utils"
* *
* const User = model.define("user", { * const User = model.define("user", {
* id: model.id(), * id: model.id(),
* email: model.hasOne(() => Email), * email: model.hasOne(() => Email),
* }) * })
* *
* @customNamespace Relationship Methods * @customNamespace Relationship Methods
*/ */
hasOne<T>(entityBuilder: T, options?: RelationshipOptions) { hasOne<T>(entityBuilder: T, options?: RelationshipOptions) {
@@ -328,15 +327,15 @@ export class EntityBuilder {
/** /**
* This method defines the inverse of the {@link hasOne} or {@link hasMany} relationship. * This method defines the inverse of the {@link hasOne} or {@link hasMany} relationship.
* *
* For example, a product "belongsTo" a store. * For example, a product "belongsTo" a store.
* *
* @typeParam T - The type of the entity builder passed as a first parameter. By default, it's * @typeParam T - The type of the entity builder passed as a first parameter. By default, it's
* a function returning the related model. * a function returning the related model.
* *
* @param {T} entityBuilder - A function that returns the data model this model is related to. * @param {T} entityBuilder - A function that returns the data model this model is related to.
* @param {RelationshipOptions} options - The relationship's options. * @param {RelationshipOptions} options - The relationship's options.
* *
* @example * @example
* const Product = model.define("product", { * const Product = model.define("product", {
* id: model.id(), * id: model.id(),
@@ -344,7 +343,7 @@ export class EntityBuilder {
* mappedBy: "products", * mappedBy: "products",
* }), * }),
* }) * })
* *
* @customNamespace Relationship Methods * @customNamespace Relationship Methods
*/ */
belongsTo<T>(entityBuilder: T, options?: RelationshipOptions) { belongsTo<T>(entityBuilder: T, options?: RelationshipOptions) {
@@ -357,21 +356,21 @@ export class EntityBuilder {
* data model, but the related data model only has one owner. * data model, but the related data model only has one owner.
* *
* For example, a store "hasMany" products. * For example, a store "hasMany" products.
* *
* @typeParam T - The type of the entity builder passed as a first parameter. By default, it's * @typeParam T - The type of the entity builder passed as a first parameter. By default, it's
* a function returning the related model. * a function returning the related model.
* *
* @param {T} entityBuilder - A function that returns the data model this model is related to. * @param {T} entityBuilder - A function that returns the data model this model is related to.
* @param {RelationshipOptions} options - The relationship's options. * @param {RelationshipOptions} options - The relationship's options.
* *
* @example * @example
* import { model } from "@medusajs/utils" * import { model } from "@medusajs/utils"
* *
* const Store = model.define("store", { * const Store = model.define("store", {
* id: model.id(), * id: model.id(),
* products: model.hasMany(() => Product), * products: model.hasMany(() => Product),
* }) * })
* *
* @customNamespace Relationship Methods * @customNamespace Relationship Methods
*/ */
hasMany<T>(entityBuilder: T, options?: RelationshipOptions) { hasMany<T>(entityBuilder: T, options?: RelationshipOptions) {
@@ -384,32 +383,29 @@ export class EntityBuilder {
* *
* For example, an order is associated with many products, and a product * For example, an order is associated with many products, and a product
* is associated with many orders. * is associated with many orders.
* *
* @typeParam T - The type of the entity builder passed as a first parameter. By default, it's * @typeParam T - The type of the entity builder passed as a first parameter. By default, it's
* a function returning the related model. * a function returning the related model.
* *
* @param {T} entityBuilder - A function that returns the data model this model is related to. * @param {T} entityBuilder - A function that returns the data model this model is related to.
* @param {RelationshipOptions} options - The relationship's options. * @param {RelationshipOptions} options - The relationship's options.
* *
* @example * @example
* import { model } from "@medusajs/utils" * import { model } from "@medusajs/utils"
* *
* const Order = model.define("order", { * const Order = model.define("order", {
* id: model.id(), * id: model.id(),
* products: model.manyToMany(() => Product), * products: model.manyToMany(() => Product),
* }) * })
* *
* const Product = model.define("product", { * const Product = model.define("product", {
* id: model.id(), * id: model.id(),
* order: model.manyToMany(() => Order), * order: model.manyToMany(() => Order),
* }) * })
* *
* @customNamespace Relationship Methods * @customNamespace Relationship Methods
*/ */
manyToMany<T>( manyToMany<T>(entityBuilder: T, options?: ManyToManyOptions) {
entityBuilder: T,
options?: ManyToManyOptions
) {
return new ManyToMany<T>(entityBuilder, options || {}) return new ManyToMany<T>(entityBuilder, options || {})
} }
} }
+21 -21
View File
@@ -96,16 +96,16 @@ export class DmlEntity<Schema extends DMLSchema> implements IDmlEntity<Schema> {
/** /**
* This method configures which related data models an operation, such as deletion, * This method configures which related data models an operation, such as deletion,
* should be cascaded to. * should be cascaded to.
* *
* For example, if a store is deleted, its product should also be deleted. * For example, if a store is deleted, its product should also be deleted.
* *
* @param options - The cascades configurations. They object's keys are the names of * @param options - The cascades configurations. They object's keys are the names of
* the actions, such as `deleted`, and the value is an array of relations that the * the actions, such as `deleted`, and the value is an array of relations that the
* action should be cascaded to. * action should be cascaded to.
* *
* @example * @example
* import { model } from "@medusajs/utils" * import { model } from "@medusajs/utils"
* *
* const Store = model.define("store", { * const Store = model.define("store", {
* id: model.id(), * id: model.id(),
* products: model.hasMany(() => Product), * products: model.hasMany(() => Product),
@@ -113,7 +113,7 @@ export class DmlEntity<Schema extends DMLSchema> implements IDmlEntity<Schema> {
* .cascades({ * .cascades({
* delete: ["products"], * delete: ["products"],
* }) * })
* *
* @customNamespace Model Methods * @customNamespace Model Methods
*/ */
cascades( cascades(
@@ -142,15 +142,15 @@ export class DmlEntity<Schema extends DMLSchema> implements IDmlEntity<Schema> {
/** /**
* This method defines indices on the data model. An index can be on multiple columns * This method defines indices on the data model. An index can be on multiple columns
* and have conditions. * and have conditions.
* *
* @param indexes - The index's configuration. * @param indexes - The index's configuration.
* *
* @example * @example
* An example of a simple index: * An example of a simple index:
* *
* ```ts * ```ts
* import { model } from "@medusajs/utils" * import { model } from "@medusajs/utils"
* *
* const MyCustom = model.define("my_custom", { * const MyCustom = model.define("my_custom", {
* id: model.id(), * id: model.id(),
* name: model.text(), * name: model.text(),
@@ -160,15 +160,15 @@ export class DmlEntity<Schema extends DMLSchema> implements IDmlEntity<Schema> {
* on: ["name", "age"], * on: ["name", "age"],
* }, * },
* ]) * ])
* *
* export default MyCustom * export default MyCustom
* ``` * ```
* *
* To add a condition on the index, use the `where` option: * To add a condition on the index, use the `where` option:
* *
* ```ts * ```ts
* import { model } from "@medusajs/utils" * import { model } from "@medusajs/utils"
* *
* const MyCustom = model.define("my_custom", { * const MyCustom = model.define("my_custom", {
* id: model.id(), * id: model.id(),
* name: model.text(), * name: model.text(),
@@ -181,15 +181,15 @@ export class DmlEntity<Schema extends DMLSchema> implements IDmlEntity<Schema> {
* } * }
* }, * },
* ]) * ])
* *
* export default MyCustom * export default MyCustom
* ``` * ```
* *
* The condition can also be a negation. For example: * The condition can also be a negation. For example:
* *
* ```ts * ```ts
* import { model } from "@medusajs/utils" * import { model } from "@medusajs/utils"
* *
* const MyCustom = model.define("my_custom", { * const MyCustom = model.define("my_custom", {
* id: model.id(), * id: model.id(),
* name: model.text(), * name: model.text(),
@@ -204,12 +204,12 @@ export class DmlEntity<Schema extends DMLSchema> implements IDmlEntity<Schema> {
* } * }
* }, * },
* ]) * ])
* *
* export default MyCustom * export default MyCustom
* ``` * ```
* *
* In this example, the index is created when the value of `age` doesn't equal `30`. * In this example, the index is created when the value of `age` doesn't equal `30`.
* *
* @customNamespace Model Methods * @customNamespace Model Methods
*/ */
indexes(indexes: EntityIndex<Schema, string | QueryCondition<Schema>>[]) { indexes(indexes: EntityIndex<Schema, string | QueryCondition<Schema>>[]) {
@@ -87,14 +87,14 @@ export function createMikrORMEntity() {
*/ */
export const toMikroORMEntity = <T>( export const toMikroORMEntity = <T>(
entity: T entity: T
): T extends DmlEntity<infer Schema> ? Infer<T> : T => { ): T extends DmlEntity<any> ? Infer<T> : T => {
let mikroOrmEntity: T | EntityConstructor<any> = entity let mikroOrmEntity: T | EntityConstructor<any> = entity
if (DmlEntity.isDmlEntity(entity)) { if (DmlEntity.isDmlEntity(entity)) {
mikroOrmEntity = createMikrORMEntity()(entity) mikroOrmEntity = createMikrORMEntity()(entity)
} }
return mikroOrmEntity as T extends DmlEntity<infer Schema> ? Infer<T> : T return mikroOrmEntity as T extends DmlEntity<any> ? Infer<T> : T
} }
/** /**
@@ -1,74 +0,0 @@
import { DMLSchema } from "@medusajs/types"
import { IdProperty } from "../../properties/id"
/*
The id() property is an core opinionated property that will act as a primaryKey
by default and come with built-in logic when converted to a mikroorm entity. If no other
primaryKey() properties are found within the schema, we continue treating the id() property
as a primaryKey. When other fields are set as explicit primaryKey fields, we convert the
id() property to no longer be a primaryKey.
Example:
Model 1:
id: model.id() -> primary key
code: model.text()
Model 2:
id: model.id()
code: model.text().primaryKey() -> primary key
Model 3:
id: model.id()
code: model.text().primaryKey() -> composite primary key
name: model.text().primaryKey() -> composite primary key
*/
export function inferPrimaryKeyProperties<TSchema extends DMLSchema>(
schema: TSchema
) {
// If explicit primaryKey fields are not found, no inferrence is required. Return early.
if (!getExplicitPrimaryKeyFields(schema).length) {
return schema
}
// If explicit primaryKey fields are found, set any id() properties to no longer be
// set to primaryKey.
for (const [field, property] of Object.entries(schema)) {
const parsed = property.parse(field)
const isRelationshipType = "type" in parsed
if (isRelationshipType) {
continue
}
if (parsed.dataType.name === "id") {
;(property as IdProperty).primaryKey(false)
}
}
return schema
}
/*
Gets all explicit primary key fields from a schema, except id properties.
eg: model.define('test', {
id: model.id(), -> implicit primaryKey field,
text: model.text(),
textPrimary: model.text().primaryKey(), -> explicit primaryKey field
numberPrimary: model.number().primaryKey(), -> explicit primaryKey field
belongsTo: model.belongsTo(() => belongsToAnother),
})
*/
function getExplicitPrimaryKeyFields(schema: DMLSchema) {
return Object.entries(schema).filter(([field, property]) => {
const parsed = property.parse(field)
const isRelationshipType = "type" in parsed
// Return early if its a relationship property or an id property
if (isRelationshipType || parsed.dataType.name === "id") {
return false
}
return !!parsed.dataType.options?.primaryKey
})
}
+22 -22
View File
@@ -11,33 +11,33 @@ export class IdProperty extends BaseProperty<string> {
primaryKey: boolean primaryKey: boolean
prefix?: string prefix?: string
} }
} = {
name: "id",
options: { primaryKey: false },
} }
constructor(options?: { constructor(options?: { prefix?: string }) {
/**
* Whether the ID is the data model's primary key.
*
* @defaultValue true
*/
primaryKey?: boolean
/**
* By default, Medusa shortens the data model's name and uses it as the
* prefix of all IDs. For example, `cm_123`.
*
* Use this option to specify the prefix to use instead.
*/
prefix?: string
}) {
super() super()
this.dataType = { this.dataType.options.prefix = options?.prefix
name: "id",
options: { primaryKey: true, ...options },
}
} }
primaryKey(decision: boolean) { /**
this.dataType.options.primaryKey = decision * This method indicates that the property is the data model's primary key.
*
* @example
* import { model } from "@medusajs/utils"
*
* const Product = model.define("Product", {
* id: model.id().primaryKey(),
* // ...
* })
*
* export default Product
*
* @customNamespace Property Configuration Methods
*/
primaryKey() {
this.dataType.options.primaryKey = true
return this return this
} }
} }
+17 -19
View File
@@ -8,23 +8,30 @@ export class TextProperty extends BaseProperty<string> {
name: "text" name: "text"
options: { options: {
primaryKey: boolean primaryKey: boolean
prefix?: string
searchable: boolean searchable: boolean
} }
} = {
name: "text",
options: {
primaryKey: false,
searchable: false,
},
} }
/** /**
* This method indicates that the property is the data model's primary key. * This method indicates that the property is the data model's primary key.
* *
* @example * @example
* import { model } from "@medusajs/utils" * import { model } from "@medusajs/utils"
* *
* const MyCustom = model.define("my_custom", { * const Product = model.define("Product", {
* code: model.text().primaryKey(), * code: model.text().primaryKey(),
* // ... * // ...
* }) * })
* *
* export default MyCustom * export default Product
* *
* @customNamespace Property Configuration Methods * @customNamespace Property Configuration Methods
*/ */
primaryKey() { primaryKey() {
@@ -35,17 +42,17 @@ export class TextProperty extends BaseProperty<string> {
/** /**
* This method indicates that a text property is searchable. * This method indicates that a text property is searchable.
* *
* @example * @example
* import { model } from "@medusajs/utils" * import { model } from "@medusajs/utils"
* *
* const MyCustom = model.define("my_custom", { * const MyCustom = model.define("my_custom", {
* name: model.text().searchable(), * name: model.text().searchable(),
* // ... * // ...
* }) * })
* *
* export default MyCustom * export default MyCustom
* *
* @customNamespace Property Configuration Methods * @customNamespace Property Configuration Methods
*/ */
searchable() { searchable() {
@@ -53,13 +60,4 @@ export class TextProperty extends BaseProperty<string> {
return this return this
} }
constructor(options?: { primaryKey?: boolean; searchable?: boolean }) {
super()
this.dataType = {
name: "text",
options: { primaryKey: false, searchable: false, ...options },
}
}
} }
@@ -2,7 +2,7 @@ import { model } from "@medusajs/utils"
import { Notification } from "./notification" import { Notification } from "./notification"
export const NotificationProvider = model.define("notificationProvider", { export const NotificationProvider = model.define("notificationProvider", {
id: model.id({ prefix: "notpro" }), id: model.id({ prefix: "notpro" }).primaryKey(),
handle: model.text(), handle: model.text(),
name: model.text(), name: model.text(),
is_enabled: model.boolean().default(true), is_enabled: model.boolean().default(true),
@@ -3,7 +3,7 @@ import { NotificationProvider } from "./notification-provider"
// We probably want to have a TTL for each entry, so we don't bloat the DB (and also for GDPR reasons if TTL < 30 days). // We probably want to have a TTL for each entry, so we don't bloat the DB (and also for GDPR reasons if TTL < 30 days).
export const Notification = model.define("notification", { export const Notification = model.define("notification", {
id: model.id({ prefix: "noti" }), id: model.id({ prefix: "noti" }).primaryKey(),
// This can be an email, phone number, or username, depending on the channel. // This can be an email, phone number, or username, depending on the channel.
to: model.text(), to: model.text(),
channel: model.text(), channel: model.text(),
+1 -1
View File
@@ -2,7 +2,7 @@ import { model } from "@medusajs/utils"
import RegionCountry from "./country" import RegionCountry from "./country"
const Region = model.define("region", { const Region = model.define("region", {
id: model.id({ prefix: "reg" }), id: model.id({ prefix: "reg" }).primaryKey(),
name: model.text().searchable(), name: model.text().searchable(),
currency_code: model.text().searchable(), currency_code: model.text().searchable(),
automatic_taxes: model.boolean().default(true), automatic_taxes: model.boolean().default(true),