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({
columnType: "text",
name: "id",
nullable: false,
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({ expect(metaData.properties.id).toEqual({
columnType: "text", columnType: "text",
name: "id", name: "id",
nullable: false, nullable: false,
reference: "scalar", reference: "scalar",
type: "string", type: "string",
getter: false, primary: true,
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: [],
})
})
}) })
+13 -17
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,7 +21,9 @@ 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. * The data model's name.
*/ */
@@ -31,10 +32,10 @@ export type DefineOptions = string | {
* The name of the data model's table in the database. * The name of the data model's table in the database.
*/ */
tableName: string tableName: string
} }
export type ManyToManyOptions = RelationshipOptions & export type ManyToManyOptions = RelationshipOptions &
( (
| { | {
/** /**
* The name of the pivot table * The name of the pivot table
@@ -58,7 +59,7 @@ export type ManyToManyOptions = RelationshipOptions &
*/ */
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
@@ -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,
@@ -115,23 +115,22 @@ 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)
} }
@@ -406,10 +405,7 @@ export class EntityBuilder {
* *
* @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 || {})
} }
} }
@@ -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
})
}
+23 -23
View File
@@ -11,33 +11,33 @@ export class IdProperty extends BaseProperty<string> {
primaryKey: boolean primaryKey: boolean
prefix?: string prefix?: string
} }
} } = {
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
}) {
super()
this.dataType = {
name: "id", name: "id",
options: { primaryKey: true, ...options }, options: { primaryKey: false },
}
} }
primaryKey(decision: boolean) { constructor(options?: { prefix?: string }) {
this.dataType.options.primaryKey = decision super()
this.dataType.options.prefix = options?.prefix
}
/**
* 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
} }
} }
+9 -11
View File
@@ -8,8 +8,15 @@ 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,
},
} }
/** /**
@@ -18,12 +25,12 @@ export class TextProperty extends BaseProperty<string> {
* @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
*/ */
@@ -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),