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:
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user