feat(utils): infer primaryKeys from a DML model (#7839)

what:

- depending on other properties in a DML model, we infer primaryKeys between id properties and primaryKey-able properties. 

```
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
```
This commit is contained in:
Riqwan Thamir
2024-06-26 09:26:09 +00:00
committed by GitHub
parent 922fff4051
commit 4945c79818
4 changed files with 179 additions and 22 deletions
@@ -1518,7 +1518,7 @@ describe("Entity builder", () => {
describe("Entity builder | primaryKey", () => { describe("Entity builder | primaryKey", () => {
test("should create both id fields and primaryKey fields", () => { test("should create both id fields and primaryKey fields", () => {
const user = model.define("user", { const user = model.define("user", {
id: model.id({ primaryKey: false }), id: model.id(),
email: model.text().primaryKey(), email: model.text().primaryKey(),
account_id: model.number().primaryKey(), account_id: model.number().primaryKey(),
}) })
@@ -1533,25 +1533,6 @@ describe("Entity builder", () => {
}>() }>()
const metaData = MetadataStorage.getMetadataFromDecorator(User) 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({ expect(metaData.properties).toEqual({
id: { id: {
@@ -1612,8 +1593,102 @@ describe("Entity builder", () => {
setter: false, 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,
})
}) })
}) })
@@ -6,6 +6,7 @@ import type {
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 { BigNumberProperty } from "./properties/big-number" import { BigNumberProperty } from "./properties/big-number"
import { BooleanProperty } from "./properties/boolean" import { BooleanProperty } from "./properties/boolean"
import { DateTimeProperty } from "./properties/date-time" import { DateTimeProperty } from "./properties/date-time"
@@ -37,7 +38,7 @@ type DefineOptions = string | { name?: string; tableName: string }
* schema using the shorthand methods. * schema using the shorthand methods.
*/ */
export class EntityBuilder { export class EntityBuilder {
#disallowImplicitProperties(schema: Record<string, any>) { #disallowImplicitProperties(schema: DMLSchema) {
const implicitProperties = Object.keys(schema).filter((fieldName) => const implicitProperties = Object.keys(schema).filter((fieldName) =>
IMPLICIT_PROPERTIES.includes(fieldName) IMPLICIT_PROPERTIES.includes(fieldName)
) )
@@ -60,6 +61,7 @@ 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,
@@ -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<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
})
}
@@ -20,4 +20,10 @@ export class IdProperty extends BaseProperty<string> {
options: { primaryKey: true, ...options }, options: { primaryKey: true, ...options },
} }
} }
primaryKey(decision: boolean) {
this.dataType.options.primaryKey = decision
return this
}
} }