Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: add support for check constraints in DML #10391

Merged
merged 3 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/hip-suns-sort.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@medusajs/types": patch
"@medusajs/utils": patch
---

feature: add support for check constraints in DML
67 changes: 45 additions & 22 deletions packages/core/types/src/dml/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<
{
Expand All @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down
100 changes: 100 additions & 0 deletions packages/core/utils/src/dml/__tests__/entity-builder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})
})
19 changes: 15 additions & 4 deletions packages/core/utils/src/dml/entity.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import {
IDmlEntity,
DMLSchema,
EntityCascades,
EntityIndex,
ExtractEntityRelations,
IDmlEntity,
CheckConstraint,
EntityCascades,
QueryCondition,
IDmlEntityConfig,
ExtractEntityRelations,
InferDmlEntityNameFromConfig,
QueryCondition,
} from "@medusajs/types"
import { isObject, isString, toCamelCase, upperCaseFirst } from "../common"
import { transformIndexWhere } from "./helpers/entity-builder/build-indexes"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -100,13 +102,15 @@ export class DmlEntity<
schema: DMLSchema
cascades: EntityCascades<string[]>
indexes: EntityIndex<Schema>[]
checks: CheckConstraint<Schema>[]
} {
return {
name: this.name,
tableName: this.#tableName,
schema: this.schema,
cascades: this.#cascades,
indexes: this.#indexes,
checks: this.#checks,
}
}

Expand Down Expand Up @@ -238,4 +242,11 @@ export class DmlEntity<
this.#indexes = indexes as EntityIndex<Schema>[]
return this
}

/**
*/
checks(checks: CheckConstraint<Schema>[]) {
this.#checks = checks
return this
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"

/**
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -96,6 +97,7 @@ function createMikrORMEntity() {
})

applyEntityIndexes(MikroORMEntity, tableName, entityIndexes)
applyChecks(MikroORMEntity, checks)

/**
* Converting class to a MikroORM entity
Expand Down
21 changes: 21 additions & 0 deletions packages/core/utils/src/dml/helpers/mikro-orm/apply-checks.ts
Original file line number Diff line number Diff line change
@@ -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)
})
}
Loading