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

View File

@@ -1291,7 +1291,8 @@ describe("Entity builder", () => {
columnType: "text",
name: "id",
nullable: false,
primary: true,
getter: false,
setter: false,
},
username: {
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", {
id: model.id({ primaryKey: false }),
id: model.id().primaryKey(),
username: model.text(),
email: model.text(),
})
@@ -1391,8 +1392,7 @@ describe("Entity builder", () => {
columnType: "text",
name: "id",
nullable: false,
getter: false,
setter: false,
primary: true,
},
username: {
reference: "scalar",
@@ -1451,7 +1451,7 @@ describe("Entity builder", () => {
test("define prefix for the id", () => {
const user = model.define("user", {
id: model.id({ primaryKey: false, prefix: "us" }),
id: model.id({ prefix: "us" }).primaryKey(),
username: model.text(),
email: model.text(),
})
@@ -1494,8 +1494,7 @@ describe("Entity builder", () => {
columnType: "text",
name: "id",
nullable: false,
getter: false,
setter: false,
primary: true,
},
username: {
reference: "scalar",
@@ -1554,124 +1553,25 @@ describe("Entity builder", () => {
})
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", {
id: model.id(),
id: model.id().primaryKey(),
email: model.text().primaryKey(),
account_id: model.number().primaryKey(),
account_id: model.number(),
})
const entityBuilder = createMikrORMEntity()
const User = entityBuilder(user)
expectTypeOf(new User()).toMatchTypeOf<{
id: string
email: string
account_id: number
}>()
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({
columnType: "text",
name: "id",
nullable: false,
reference: "scalar",
type: "string",
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({
columnType: "text",
name: "email",
@@ -1680,53 +1580,6 @@ describe("Entity builder", () => {
type: "string",
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,
})
})
})

View File

@@ -5,24 +5,6 @@ describe("Id property", () => {
test("create id property type", () => {
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>()
expect(property.parse("id")).toEqual({
fieldName: "id",
@@ -37,4 +19,22 @@ describe("Id property", () => {
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: [],
})
})
})

View File

@@ -17,4 +17,20 @@ describe("Text property", () => {
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: [],
})
})
})

View File

@@ -2,7 +2,6 @@ import type { DMLSchema, RelationshipOptions } from "@medusajs/types"
import { DmlEntity } from "./entity"
import { createBigNumberProperties } from "./helpers/entity-builder/create-big-number-properties"
import { createDefaultProperties } from "./helpers/entity-builder/create-default-properties"
import { inferPrimaryKeyProperties } from "./helpers/entity-builder/infer-primary-key-properties"
import { ArrayProperty } from "./properties/array"
import { BigNumberProperty } from "./properties/big-number"
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"]
export type DefineOptions = string | {
/**
* The data model's name.
*/
name?: string
/**
* The name of the data model's table in the database.
*/
tableName: string
}
export type DefineOptions =
| string
| {
/**
* The data model's name.
*/
name?: string
/**
* The name of the data model's table in the database.
*/
tableName: string
}
export type ManyToManyOptions = RelationshipOptions &
(
| {
/**
* The name of the pivot table
* created in the database for this relationship.
*/
pivotTable?: string
/**
* @ignore
*/
pivotEntity?: never
}
| {
/**
* @ignore
*/
pivotTable?: never
/**
* A function that returns the data model
* representing the pivot table created in the
* database for this relationship.
*/
pivotEntity?: () => DmlEntity<any>
}
)
(
| {
/**
* The name of the pivot table
* created in the database for this relationship.
*/
pivotTable?: string
/**
* @ignore
*/
pivotEntity?: never
}
| {
/**
* @ignore
*/
pivotTable?: never
/**
* A function that returns the data model
* representing the pivot table created in the
* database for this relationship.
*/
pivotEntity?: () => DmlEntity<any>
}
)
/**
* 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.
*
*
* @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.
* The data model's name must be unique.
* @param {Schema} schema - The schema of the data model's properties.
*
*
* @example
* import { model } from "@medusajs/utils"
*
*
* const MyCustom = model.define("my_custom", {
* id: model.id(),
* name: model.text(),
* })
*
*
* export default MyCustom
*/
define<Schema extends DMLSchema>(
@@ -103,7 +104,6 @@ export class EntityBuilder {
schema: Schema
) {
this.#disallowImplicitProperties(schema)
schema = inferPrimaryKeyProperties(schema)
return new DmlEntity(nameOrConfig, {
...schema,
@@ -114,40 +114,39 @@ export class EntityBuilder {
/**
* This method defines an automatically generated string ID property.
*
* By default, this property is considered to be the data models primary key.
*
* @param {ConstructorParameters<typeof IdProperty>[0]} options - The ID's options.
*
*
* You must use the "primaryKey" modifier to mark the property as the
* primary key.
*
* @example
* import { model } from "@medusajs/utils"
*
* const MyCustom = model.define("my_custom", {
* id: model.id(),
*
* const User = model.define("User", {
* id: model.id().primaryKey(),
* // ...
* })
*
* export default MyCustom
*
*
* export default User
*
* @customNamespace Property Types
*/
id(options?: ConstructorParameters<typeof IdProperty>[0]) {
id(options?: { prefix?: string }) {
return new IdProperty(options)
}
/**
* This method defines a string property.
*
*
* @example
* import { model } from "@medusajs/utils"
*
*
* const MyCustom = model.define("my_custom", {
* name: model.text(),
* // ...
* })
*
*
* export default MyCustom
*
*
* @customNamespace Property Types
*/
text() {
@@ -156,17 +155,17 @@ export class EntityBuilder {
/**
* This method defines a boolean property.
*
*
* @example
* import { model } from "@medusajs/utils"
*
*
* const MyCustom = model.define("my_custom", {
* hasAccount: model.boolean(),
* // ...
* })
*
*
* export default MyCustom
*
*
* @customNamespace Property Types
*/
boolean() {
@@ -175,17 +174,17 @@ export class EntityBuilder {
/**
* This method defines a number property.
*
*
* @example
* import { model } from "@medusajs/utils"
*
*
* const MyCustom = model.define("my_custom", {
* age: model.number(),
* // ...
* })
*
*
* export default MyCustom
*
*
* @customNamespace Property Types
*/
number() {
@@ -194,19 +193,19 @@ export class EntityBuilder {
/**
* This method defines a number property that expects large numbers, such as prices.
*
*
* @example
* import { model } from "@medusajs/utils"
*
*
* const MyCustom = model.define("my_custom", {
* price: model.bigNumber(),
* // ...
* })
*
*
* export default MyCustom
*
*
* @customNamespace Property Types
*
*
* @privateRemarks
* This property produces an additional
* column - raw_{{ property_name }}, which stores the configuration
@@ -218,17 +217,17 @@ export class EntityBuilder {
/**
* This method defines an array of strings property.
*
*
* @example
* import { model } from "@medusajs/utils"
*
*
* const MyCustom = model.define("my_custom", {
* names: model.array(),
* // ...
* })
*
*
* export default MyCustom
*
*
* @customNamespace Property Types
*/
array() {
@@ -237,17 +236,17 @@ export class EntityBuilder {
/**
* This method defines a timestamp property.
*
*
* @example
* import { model } from "@medusajs/utils"
*
*
* const MyCustom = model.define("my_custom", {
* date_of_birth: model.dateTime(),
* // ...
* })
*
*
* export default MyCustom
*
*
* @customNamespace Property Types
*/
dateTime() {
@@ -256,17 +255,17 @@ export class EntityBuilder {
/**
* This method defines a property whose value is a stringified JSON object.
*
*
* @example
* import { model } from "@medusajs/utils"
*
*
* const MyCustom = model.define("my_custom", {
* metadata: model.json(),
* // ...
* })
*
*
* export default MyCustom
*
*
* @customNamespace Property Types
*/
json() {
@@ -275,21 +274,21 @@ export class EntityBuilder {
/**
* 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`.
*
*
* @param {Values[]} values - An array of possible values.
*
*
* @example
* import { model } from "@medusajs/utils"
*
*
* const MyCustom = model.define("my_custom", {
* color: model.enum(["black", "white"]),
* // ...
* })
*
*
* export default MyCustom
*
*
* @customNamespace Property Types
*/
enum<const Values extends unknown>(values: Values[]) {
@@ -302,24 +301,24 @@ export class EntityBuilder {
* data model.
*
* 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.
*
*
* @typeParam T - The type of the entity builder passed as a first parameter. By default, it's
* a function returning the related model.
*
*
* @param {T} entityBuilder - A function that returns the data model this model is related to.
* @param {RelationshipOptions} options - The relationship's options.
*
*
* @example
* import { model } from "@medusajs/utils"
*
*
* const User = model.define("user", {
* id: model.id(),
* email: model.hasOne(() => Email),
* })
*
*
* @customNamespace Relationship Methods
*/
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.
*
*
* For example, a product "belongsTo" a store.
*
*
* @typeParam T - The type of the entity builder passed as a first parameter. By default, it's
* a function returning the related model.
*
*
* @param {T} entityBuilder - A function that returns the data model this model is related to.
* @param {RelationshipOptions} options - The relationship's options.
*
*
* @example
* const Product = model.define("product", {
* id: model.id(),
@@ -344,7 +343,7 @@ export class EntityBuilder {
* mappedBy: "products",
* }),
* })
*
*
* @customNamespace Relationship Methods
*/
belongsTo<T>(entityBuilder: T, options?: RelationshipOptions) {
@@ -357,21 +356,21 @@ export class EntityBuilder {
* data model, but the related data model only has one owner.
*
* For example, a store "hasMany" products.
*
*
* @typeParam T - The type of the entity builder passed as a first parameter. By default, it's
* a function returning the related model.
*
*
* @param {T} entityBuilder - A function that returns the data model this model is related to.
* @param {RelationshipOptions} options - The relationship's options.
*
*
* @example
* import { model } from "@medusajs/utils"
*
*
* const Store = model.define("store", {
* id: model.id(),
* products: model.hasMany(() => Product),
* })
*
*
* @customNamespace Relationship Methods
*/
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
* is associated with many orders.
*
*
* @typeParam T - The type of the entity builder passed as a first parameter. By default, it's
* a function returning the related model.
*
*
* @param {T} entityBuilder - A function that returns the data model this model is related to.
* @param {RelationshipOptions} options - The relationship's options.
*
*
* @example
* import { model } from "@medusajs/utils"
*
*
* const Order = model.define("order", {
* id: model.id(),
* products: model.manyToMany(() => Product),
* })
*
*
* const Product = model.define("product", {
* id: model.id(),
* order: model.manyToMany(() => Order),
* })
*
*
* @customNamespace Relationship Methods
*/
manyToMany<T>(
entityBuilder: T,
options?: ManyToManyOptions
) {
manyToMany<T>(entityBuilder: T, options?: ManyToManyOptions) {
return new ManyToMany<T>(entityBuilder, options || {})
}
}

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,
* should be cascaded to.
*
*
* 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
* 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.
*
*
* @example
* import { model } from "@medusajs/utils"
*
*
* const Store = model.define("store", {
* id: model.id(),
* products: model.hasMany(() => Product),
@@ -113,7 +113,7 @@ export class DmlEntity<Schema extends DMLSchema> implements IDmlEntity<Schema> {
* .cascades({
* delete: ["products"],
* })
*
*
* @customNamespace Model Methods
*/
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
* and have conditions.
*
*
* @param indexes - The index's configuration.
*
*
* @example
* An example of a simple index:
*
*
* ```ts
* import { model } from "@medusajs/utils"
*
*
* const MyCustom = model.define("my_custom", {
* id: model.id(),
* name: model.text(),
@@ -160,15 +160,15 @@ export class DmlEntity<Schema extends DMLSchema> implements IDmlEntity<Schema> {
* on: ["name", "age"],
* },
* ])
*
*
* export default MyCustom
* ```
*
*
* To add a condition on the index, use the `where` option:
*
*
* ```ts
* import { model } from "@medusajs/utils"
*
*
* const MyCustom = model.define("my_custom", {
* id: model.id(),
* name: model.text(),
@@ -181,15 +181,15 @@ export class DmlEntity<Schema extends DMLSchema> implements IDmlEntity<Schema> {
* }
* },
* ])
*
*
* export default MyCustom
* ```
*
*
* The condition can also be a negation. For example:
*
*
* ```ts
* import { model } from "@medusajs/utils"
*
*
* const MyCustom = model.define("my_custom", {
* id: model.id(),
* name: model.text(),
@@ -204,12 +204,12 @@ export class DmlEntity<Schema extends DMLSchema> implements IDmlEntity<Schema> {
* }
* },
* ])
*
*
* export default MyCustom
* ```
*
*
* In this example, the index is created when the value of `age` doesn't equal `30`.
*
*
* @customNamespace Model Methods
*/
indexes(indexes: EntityIndex<Schema, string | QueryCondition<Schema>>[]) {

View File

@@ -87,14 +87,14 @@ export function createMikrORMEntity() {
*/
export const toMikroORMEntity = <T>(
entity: T
): T extends DmlEntity<infer Schema> ? Infer<T> : T => {
): T extends DmlEntity<any> ? Infer<T> : T => {
let mikroOrmEntity: T | EntityConstructor<any> = entity
if (DmlEntity.isDmlEntity(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
}
/**

View File

@@ -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
})
}

View File

@@ -11,33 +11,33 @@ export class IdProperty extends BaseProperty<string> {
primaryKey: boolean
prefix?: string
}
} = {
name: "id",
options: { primaryKey: false },
}
constructor(options?: {
/**
* 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
}) {
constructor(options?: { prefix?: string }) {
super()
this.dataType = {
name: "id",
options: { primaryKey: true, ...options },
}
this.dataType.options.prefix = options?.prefix
}
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
}
}

View File

@@ -8,23 +8,30 @@ export class TextProperty extends BaseProperty<string> {
name: "text"
options: {
primaryKey: boolean
prefix?: string
searchable: boolean
}
} = {
name: "text",
options: {
primaryKey: false,
searchable: false,
},
}
/**
* This method indicates that the property is the data model's primary key.
*
*
* @example
* import { model } from "@medusajs/utils"
*
* const MyCustom = model.define("my_custom", {
*
* const Product = model.define("Product", {
* code: model.text().primaryKey(),
* // ...
* })
*
* export default MyCustom
*
*
* export default Product
*
* @customNamespace Property Configuration Methods
*/
primaryKey() {
@@ -35,17 +42,17 @@ export class TextProperty extends BaseProperty<string> {
/**
* This method indicates that a text property is searchable.
*
*
* @example
* import { model } from "@medusajs/utils"
*
*
* const MyCustom = model.define("my_custom", {
* name: model.text().searchable(),
* // ...
* })
*
*
* export default MyCustom
*
*
* @customNamespace Property Configuration Methods
*/
searchable() {
@@ -53,13 +60,4 @@ export class TextProperty extends BaseProperty<string> {
return this
}
constructor(options?: { primaryKey?: boolean; searchable?: boolean }) {
super()
this.dataType = {
name: "text",
options: { primaryKey: false, searchable: false, ...options },
}
}
}