feature: add support for check constraints in DML (#10391)

This commit is contained in:
Harminder Virk authored and GitHub committed 2024-12-02 17:59:50 +05:30
1 parent ac79585232
commit 3e98364bd1
6 files changed
+194 -31

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
---
"@medusajs/types": patch
"@medusajs/utils": patch
---
feature: add support for check constraints in DML
+45 -22
View File
@@ -184,8 +184,7 @@ export type InferHasManyFields<Relation> = Relation extends () => IDmlEntity<
export type InferManyToManyFields<Relation> = InferHasManyFields<Relation> export type InferManyToManyFields<Relation> = InferHasManyFields<Relation>
/** /**
* Inferring the types of the schema fields from the DML * Infers the types of the schema fields from the DML entity
* entity
*/ */
export type InferSchemaFields<Schema extends DMLSchema> = Prettify< 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> export type InferSchemaProperties<Schema extends DMLSchema> = Prettify<
? EntityConstructor<InferSchemaFields<Schema>> {
: never [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 * Extracts names of relationships from a schema
@@ -224,6 +229,13 @@ export type ExtractEntityRelations<
: never : never
}[keyof Schema & string][] }[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 * The actions to cascade from a given entity to its
* relationship. * 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 * Infer all indexable properties from a DML entity including inferred foreign keys and excluding relationship
*/ */
export type InferIndexableProperties<T> = keyof (T extends IDmlEntity< export type InferIndexableProperties<Schema extends DMLSchema> =
infer Schema, keyof InferSchemaProperties<Schema>
any
> /**
? { * Returns a list of columns that could be mentioned
[K in keyof Schema as Schema[K] extends { type: infer Type } * within the checks
? Type extends RelationshipTypes */
? never export type InferCheckConstraintsProperties<Schema extends DMLSchema> = {
: K [K in keyof InferSchemaProperties<Schema>]: string
: K]: string }
} & InferForeignKeys<Schema>
: never) /**
* 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< export type EntityIndex<
TSchema extends DMLSchema = DMLSchema, Schema extends DMLSchema = DMLSchema,
TWhere = string Where = string
> = { > = {
/** /**
* The name of the index. If not provided, * The name of the index. If not provided,
@@ -281,11 +304,11 @@ export type EntityIndex<
/** /**
* The list of properties to create the index on. * The list of properties to create the index on.
*/ */
on: InferIndexableProperties<IDmlEntity<TSchema, any>>[] on: InferIndexableProperties<Schema>[]
/** /**
* Conditions to restrict which records are indexed. * Conditions to restrict which records are indexed.
*/ */
where?: TWhere where?: Where
} }
export type SimpleQueryValue = string | number | boolean | null 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)
})
})
}) })
+17 -6
View File
@@ -1,12 +1,13 @@
import { import {
DMLSchema,
EntityCascades,
EntityIndex,
ExtractEntityRelations,
IDmlEntity, IDmlEntity,
IDmlEntityConfig, DMLSchema,
InferDmlEntityNameFromConfig, EntityIndex,
CheckConstraint,
EntityCascades,
QueryCondition, QueryCondition,
IDmlEntityConfig,
ExtractEntityRelations,
InferDmlEntityNameFromConfig,
} from "@medusajs/types" } from "@medusajs/types"
import { isObject, isString, toCamelCase, upperCaseFirst } from "../common" import { isObject, isString, toCamelCase, upperCaseFirst } from "../common"
import { transformIndexWhere } from "./helpers/entity-builder/build-indexes" import { transformIndexWhere } from "./helpers/entity-builder/build-indexes"
@@ -72,6 +73,7 @@ export class DmlEntity<
readonly #tableName: string readonly #tableName: string
#cascades: EntityCascades<string[]> = {} #cascades: EntityCascades<string[]> = {}
#indexes: EntityIndex<Schema>[] = [] #indexes: EntityIndex<Schema>[] = []
#checks: CheckConstraint<Schema>[] = []
constructor(nameOrConfig: TConfig, schema: Schema) { constructor(nameOrConfig: TConfig, schema: Schema) {
const { name, tableName } = extractNameAndTableName(nameOrConfig) const { name, tableName } = extractNameAndTableName(nameOrConfig)
@@ -100,6 +102,7 @@ export class DmlEntity<
schema: DMLSchema schema: DMLSchema
cascades: EntityCascades<string[]> cascades: EntityCascades<string[]>
indexes: EntityIndex<Schema>[] indexes: EntityIndex<Schema>[]
checks: CheckConstraint<Schema>[]
} { } {
return { return {
name: this.name, name: this.name,
@@ -107,6 +110,7 @@ export class DmlEntity<
schema: this.schema, schema: this.schema,
cascades: this.#cascades, cascades: this.#cascades,
indexes: this.#indexes, indexes: this.#indexes,
checks: this.#checks,
} }
} }
@@ -238,4 +242,11 @@ export class DmlEntity<
this.#indexes = indexes as EntityIndex<Schema>[] this.#indexes = indexes as EntityIndex<Schema>[]
return this 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 { DmlEntity } from "../entity"
import { IdProperty } from "../properties/id" import { IdProperty } from "../properties/id"
import { DuplicateIdPropertyError } from "../errors" import { DuplicateIdPropertyError } from "../errors"
import { applyChecks } from "./mikro-orm/apply-checks"
import { mikroOrmSoftDeletableFilterOptions } from "../../dal" import { mikroOrmSoftDeletableFilterOptions } from "../../dal"
import { applySearchable } from "./entity-builder/apply-searchable"
import { defineProperty } from "./entity-builder/define-property" 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 { parseEntityName } from "./entity-builder/parse-entity-name"
import { defineRelationship } from "./entity-builder/define-relationship"
import { applyEntityIndexes, applyIndexes } from "./mikro-orm/apply-indexes" 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> { function createEntity<T extends DmlEntity<any, any>>(entity: T): Infer<T> {
class MikroORMEntity {} class MikroORMEntity {}
const { schema, cascades, indexes: entityIndexes = [] } = entity.parse() const { schema, cascades, indexes: entityIndexes, checks } = entity.parse()
const { modelName, tableName } = parseEntityName(entity) const { modelName, tableName } = parseEntityName(entity)
if (ENTITIES[modelName]) { if (ENTITIES[modelName]) {
return ENTITIES[modelName] as Infer<T> return ENTITIES[modelName] as Infer<T>
@@ -96,6 +97,7 @@ function createMikrORMEntity() {
}) })
applyEntityIndexes(MikroORMEntity, tableName, entityIndexes) applyEntityIndexes(MikroORMEntity, tableName, entityIndexes)
applyChecks(MikroORMEntity, checks)
/** /**
* Converting class to a MikroORM entity * 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)
})
}