feature: add support for check constraints in DML (#10391)
This commit is contained in:
1 parent
ac79585232
commit
3e98364bd1
6 files changed
+194
-31
No files matched your search
@@ -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>
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in new issue
Block a user