diff --git a/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts b/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts index 5978d847e6..6a760d192b 100644 --- a/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts +++ b/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts @@ -1518,7 +1518,7 @@ describe("Entity builder", () => { describe("Entity builder | primaryKey", () => { test("should create both id fields and primaryKey fields", () => { const user = model.define("user", { - id: model.id({ primaryKey: false }), + id: model.id(), email: model.text().primaryKey(), account_id: model.number().primaryKey(), }) @@ -1533,25 +1533,6 @@ describe("Entity builder", () => { }>() const metaData = MetadataStorage.getMetadataFromDecorator(User) - const userInstance = new User() - userInstance["generateId"]() - - expect(metaData.className).toEqual("User") - expect(metaData.path).toEqual("User") - - expect(metaData.hooks).toEqual({ - beforeCreate: ["generateId"], - onInit: ["generateId"], - }) - - expect(metaData.filters).toEqual({ - softDeletable: { - name: "softDeletable", - cond: expect.any(Function), - default: true, - args: false, - }, - }) expect(metaData.properties).toEqual({ id: { @@ -1612,8 +1593,102 @@ describe("Entity builder", () => { setter: false, }, }) + }) - expect(userInstance.id).toBeDefined() + 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({ + 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", + 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, + }) }) }) diff --git a/packages/core/utils/src/dml/entity-builder.ts b/packages/core/utils/src/dml/entity-builder.ts index 471e91b499..e54a76b51b 100644 --- a/packages/core/utils/src/dml/entity-builder.ts +++ b/packages/core/utils/src/dml/entity-builder.ts @@ -6,6 +6,7 @@ import type { 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 { BigNumberProperty } from "./properties/big-number" import { BooleanProperty } from "./properties/boolean" import { DateTimeProperty } from "./properties/date-time" @@ -37,7 +38,7 @@ type DefineOptions = string | { name?: string; tableName: string } * schema using the shorthand methods. */ export class EntityBuilder { - #disallowImplicitProperties(schema: Record) { + #disallowImplicitProperties(schema: DMLSchema) { const implicitProperties = Object.keys(schema).filter((fieldName) => IMPLICIT_PROPERTIES.includes(fieldName) ) @@ -60,6 +61,7 @@ export class EntityBuilder { schema: Schema ) { this.#disallowImplicitProperties(schema) + schema = inferPrimaryKeyProperties(schema) return new DmlEntity(nameOrConfig, { ...schema, diff --git a/packages/core/utils/src/dml/helpers/entity-builder/infer-primary-key-properties.ts b/packages/core/utils/src/dml/helpers/entity-builder/infer-primary-key-properties.ts new file mode 100644 index 0000000000..a8d2718b70 --- /dev/null +++ b/packages/core/utils/src/dml/helpers/entity-builder/infer-primary-key-properties.ts @@ -0,0 +1,74 @@ +import { DMLSchema } from "../../entity-builder" +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( + 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 + }) +} diff --git a/packages/core/utils/src/dml/properties/id.ts b/packages/core/utils/src/dml/properties/id.ts index 86b4ca8c68..549d56df8e 100644 --- a/packages/core/utils/src/dml/properties/id.ts +++ b/packages/core/utils/src/dml/properties/id.ts @@ -20,4 +20,10 @@ export class IdProperty extends BaseProperty { options: { primaryKey: true, ...options }, } } + + primaryKey(decision: boolean) { + this.dataType.options.primaryKey = decision + + return this + } }