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", () => {
|
||||
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,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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<string, any>) {
|
||||
#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,
|
||||
|
||||
@@ -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 },
|
||||
}
|
||||
}
|
||||
|
||||
primaryKey(decision: boolean) {
|
||||
this.dataType.options.primaryKey = decision
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user