feature: add support for check constraints in DML (#10391)
This commit is contained in:
6
.changeset/hip-suns-sort.md
Normal file
6
.changeset/hip-suns-sort.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@medusajs/types": patch
|
||||
"@medusajs/utils": patch
|
||||
---
|
||||
|
||||
feature: add support for check constraints in DML
|
||||
@@ -184,8 +184,7 @@ export type InferHasManyFields<Relation> = Relation extends () => IDmlEntity<
|
||||
export type InferManyToManyFields<Relation> = InferHasManyFields<Relation>
|
||||
|
||||
/**
|
||||
* Inferring the types of the schema fields from the DML
|
||||
* entity
|
||||
* Infers the types of the schema fields from the DML entity
|
||||
*/
|
||||
export type InferSchemaFields<Schema extends DMLSchema> = Prettify<
|
||||
{
|
||||
@@ -204,11 +203,17 @@ export type InferSchemaFields<Schema extends DMLSchema> = Prettify<
|
||||
>
|
||||
|
||||
/**
|
||||
* Helper to infer the schema type of a DmlEntity
|
||||
* Infers the schema properties without the relationships
|
||||
*/
|
||||
export type Infer<T> = T extends IDmlEntity<infer Schema, any>
|
||||
? EntityConstructor<InferSchemaFields<Schema>>
|
||||
: never
|
||||
export type InferSchemaProperties<Schema extends DMLSchema> = Prettify<
|
||||
{
|
||||
[K in keyof Schema as Schema[K] extends { type: infer Type }
|
||||
? Type extends RelationshipTypes
|
||||
? never
|
||||
: K
|
||||
: K]: Schema[K]["$dataType"]
|
||||
} & InferForeignKeys<Schema>
|
||||
>
|
||||
|
||||
/**
|
||||
* Extracts names of relationships from a schema
|
||||
@@ -224,6 +229,13 @@ export type ExtractEntityRelations<
|
||||
: never
|
||||
}[keyof Schema & string][]
|
||||
|
||||
/**
|
||||
* Helper to infer the schema type of a DmlEntity
|
||||
*/
|
||||
export type Infer<T> = T extends IDmlEntity<infer Schema, any>
|
||||
? EntityConstructor<InferSchemaFields<Schema>>
|
||||
: never
|
||||
|
||||
/**
|
||||
* The actions to cascade from a given entity to its
|
||||
* relationship.
|
||||
@@ -251,22 +263,33 @@ export type InferEntityType<T> = T extends IDmlEntity<any, any>
|
||||
/**
|
||||
* Infer all indexable properties from a DML entity including inferred foreign keys and excluding relationship
|
||||
*/
|
||||
export type InferIndexableProperties<T> = keyof (T extends IDmlEntity<
|
||||
infer Schema,
|
||||
any
|
||||
>
|
||||
? {
|
||||
[K in keyof Schema as Schema[K] extends { type: infer Type }
|
||||
? Type extends RelationshipTypes
|
||||
? never
|
||||
: K
|
||||
: K]: string
|
||||
} & InferForeignKeys<Schema>
|
||||
: never)
|
||||
export type InferIndexableProperties<Schema extends DMLSchema> =
|
||||
keyof InferSchemaProperties<Schema>
|
||||
|
||||
/**
|
||||
* Returns a list of columns that could be mentioned
|
||||
* within the checks
|
||||
*/
|
||||
export type InferCheckConstraintsProperties<Schema extends DMLSchema> = {
|
||||
[K in keyof InferSchemaProperties<Schema>]: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Options supported when defining a PostgreSQL check
|
||||
*/
|
||||
export type CheckConstraint<Schema extends DMLSchema> =
|
||||
| ((columns: InferCheckConstraintsProperties<Schema>) => string)
|
||||
| {
|
||||
name?: string
|
||||
expression?:
|
||||
| string
|
||||
| ((columns: InferCheckConstraintsProperties<Schema>) => string)
|
||||
property?: string
|
||||
}
|
||||
|
||||
export type EntityIndex<
|
||||
TSchema extends DMLSchema = DMLSchema,
|
||||
TWhere = string
|
||||
Schema extends DMLSchema = DMLSchema,
|
||||
Where = string
|
||||
> = {
|
||||
/**
|
||||
* The name of the index. If not provided,
|
||||
@@ -281,11 +304,11 @@ export type EntityIndex<
|
||||
/**
|
||||
* The list of properties to create the index on.
|
||||
*/
|
||||
on: InferIndexableProperties<IDmlEntity<TSchema, any>>[]
|
||||
on: InferIndexableProperties<Schema>[]
|
||||
/**
|
||||
* Conditions to restrict which records are indexed.
|
||||
*/
|
||||
where?: TWhere
|
||||
where?: Where
|
||||
}
|
||||
|
||||
export type SimpleQueryValue = string | number | boolean | null
|
||||
|
||||
@@ -6890,4 +6890,104 @@ describe("Entity builder", () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Entity builder | checks", () => {
|
||||
test("should define checks for an entity", () => {
|
||||
const group = model
|
||||
.define("group", {
|
||||
id: model.number(),
|
||||
name: model.text(),
|
||||
})
|
||||
.checks([
|
||||
(columns) => {
|
||||
expectTypeOf(columns).toEqualTypeOf<{
|
||||
id: string
|
||||
name: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
deleted_at: string
|
||||
}>()
|
||||
return `${columns.id} > 1`
|
||||
},
|
||||
])
|
||||
|
||||
const Group = toMikroORMEntity(group)
|
||||
const metaData = MetadataStorage.getMetadataFromDecorator(Group)
|
||||
|
||||
expect(metaData.checks).toHaveLength(1)
|
||||
expect(metaData.checks[0].expression.toString()).toMatchInlineSnapshot(`
|
||||
"(columns)=>{
|
||||
(0, _expecttype.expectTypeOf)(columns).toEqualTypeOf();
|
||||
return \`\${columns.id} > 1\`;
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test("should define checks as an object", () => {
|
||||
const group = model
|
||||
.define("group", {
|
||||
id: model.number(),
|
||||
name: model.text(),
|
||||
})
|
||||
.checks([
|
||||
{
|
||||
name: "my_custom_check",
|
||||
expression: (columns) => {
|
||||
expectTypeOf(columns).toEqualTypeOf<{
|
||||
id: string
|
||||
name: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
deleted_at: string
|
||||
}>()
|
||||
return `${columns.id} > 1`
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const Group = toMikroORMEntity(group)
|
||||
const metaData = MetadataStorage.getMetadataFromDecorator(Group)
|
||||
|
||||
expect(metaData.checks).toHaveLength(1)
|
||||
expect(metaData.checks[0].name).toEqual("my_custom_check")
|
||||
expect(metaData.checks[0].expression.toString()).toMatchInlineSnapshot(`
|
||||
"(columns)=>{
|
||||
(0, _expecttype.expectTypeOf)(columns).toEqualTypeOf();
|
||||
return \`\${columns.id} > 1\`;
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test("should infer foreign keys inside the checks callback", () => {
|
||||
const group = model
|
||||
.define("group", {
|
||||
id: model.number(),
|
||||
name: model.text(),
|
||||
parent_group: model.belongsTo(() => group, {
|
||||
mappedBy: "groups",
|
||||
}),
|
||||
groups: model.hasMany(() => group, {
|
||||
mappedBy: "parent_group",
|
||||
}),
|
||||
})
|
||||
.checks([
|
||||
(columns) => {
|
||||
expectTypeOf(columns).toEqualTypeOf<{
|
||||
id: string
|
||||
name: string
|
||||
parent_group_id: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
deleted_at: string
|
||||
}>()
|
||||
return `${columns.id} > 1`
|
||||
},
|
||||
])
|
||||
|
||||
const Group = toMikroORMEntity(group)
|
||||
const metaData = MetadataStorage.getMetadataFromDecorator(Group)
|
||||
|
||||
expect(metaData.checks).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import {
|
||||
DMLSchema,
|
||||
EntityCascades,
|
||||
EntityIndex,
|
||||
ExtractEntityRelations,
|
||||
IDmlEntity,
|
||||
IDmlEntityConfig,
|
||||
InferDmlEntityNameFromConfig,
|
||||
DMLSchema,
|
||||
EntityIndex,
|
||||
CheckConstraint,
|
||||
EntityCascades,
|
||||
QueryCondition,
|
||||
IDmlEntityConfig,
|
||||
ExtractEntityRelations,
|
||||
InferDmlEntityNameFromConfig,
|
||||
} from "@medusajs/types"
|
||||
import { isObject, isString, toCamelCase, upperCaseFirst } from "../common"
|
||||
import { transformIndexWhere } from "./helpers/entity-builder/build-indexes"
|
||||
@@ -72,6 +73,7 @@ export class DmlEntity<
|
||||
readonly #tableName: string
|
||||
#cascades: EntityCascades<string[]> = {}
|
||||
#indexes: EntityIndex<Schema>[] = []
|
||||
#checks: CheckConstraint<Schema>[] = []
|
||||
|
||||
constructor(nameOrConfig: TConfig, schema: Schema) {
|
||||
const { name, tableName } = extractNameAndTableName(nameOrConfig)
|
||||
@@ -100,6 +102,7 @@ export class DmlEntity<
|
||||
schema: DMLSchema
|
||||
cascades: EntityCascades<string[]>
|
||||
indexes: EntityIndex<Schema>[]
|
||||
checks: CheckConstraint<Schema>[]
|
||||
} {
|
||||
return {
|
||||
name: this.name,
|
||||
@@ -107,6 +110,7 @@ export class DmlEntity<
|
||||
schema: this.schema,
|
||||
cascades: this.#cascades,
|
||||
indexes: this.#indexes,
|
||||
checks: this.#checks,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,4 +242,11 @@ export class DmlEntity<
|
||||
this.#indexes = indexes as EntityIndex<Schema>[]
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
checks(checks: CheckConstraint<Schema>[]) {
|
||||
this.#checks = checks
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,11 +11,12 @@ import { Entity, Filter } from "@mikro-orm/core"
|
||||
import { DmlEntity } from "../entity"
|
||||
import { IdProperty } from "../properties/id"
|
||||
import { DuplicateIdPropertyError } from "../errors"
|
||||
import { applyChecks } from "./mikro-orm/apply-checks"
|
||||
import { mikroOrmSoftDeletableFilterOptions } from "../../dal"
|
||||
import { applySearchable } from "./entity-builder/apply-searchable"
|
||||
import { defineProperty } from "./entity-builder/define-property"
|
||||
import { defineRelationship } from "./entity-builder/define-relationship"
|
||||
import { applySearchable } from "./entity-builder/apply-searchable"
|
||||
import { parseEntityName } from "./entity-builder/parse-entity-name"
|
||||
import { defineRelationship } from "./entity-builder/define-relationship"
|
||||
import { applyEntityIndexes, applyIndexes } from "./mikro-orm/apply-indexes"
|
||||
|
||||
/**
|
||||
@@ -47,7 +48,7 @@ function createMikrORMEntity() {
|
||||
function createEntity<T extends DmlEntity<any, any>>(entity: T): Infer<T> {
|
||||
class MikroORMEntity {}
|
||||
|
||||
const { schema, cascades, indexes: entityIndexes = [] } = entity.parse()
|
||||
const { schema, cascades, indexes: entityIndexes, checks } = entity.parse()
|
||||
const { modelName, tableName } = parseEntityName(entity)
|
||||
if (ENTITIES[modelName]) {
|
||||
return ENTITIES[modelName] as Infer<T>
|
||||
@@ -96,6 +97,7 @@ function createMikrORMEntity() {
|
||||
})
|
||||
|
||||
applyEntityIndexes(MikroORMEntity, tableName, entityIndexes)
|
||||
applyChecks(MikroORMEntity, checks)
|
||||
|
||||
/**
|
||||
* Converting class to a MikroORM entity
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Check, CheckOptions } from "@mikro-orm/core"
|
||||
import { CheckConstraint, EntityConstructor } from "@medusajs/types"
|
||||
|
||||
/**
|
||||
* Defines PostgreSQL constraints using the MikrORM's "@Check"
|
||||
* decorator
|
||||
*/
|
||||
export function applyChecks(
|
||||
MikroORMEntity: EntityConstructor<any>,
|
||||
entityChecks: CheckConstraint<any>[] = []
|
||||
) {
|
||||
entityChecks.forEach((check) => {
|
||||
Check(
|
||||
typeof check === "function"
|
||||
? {
|
||||
expression: check as CheckOptions["expression"],
|
||||
}
|
||||
: (check as CheckOptions)
|
||||
)(MikroORMEntity)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user