diff --git a/.changeset/hip-suns-sort.md b/.changeset/hip-suns-sort.md new file mode 100644 index 0000000000000..6106bcef551f7 --- /dev/null +++ b/.changeset/hip-suns-sort.md @@ -0,0 +1,6 @@ +--- +"@medusajs/types": patch +"@medusajs/utils": patch +--- + +feature: add support for check constraints in DML diff --git a/packages/core/types/src/dml/index.ts b/packages/core/types/src/dml/index.ts index 7e2cff4f50784..a49d9d33f3158 100644 --- a/packages/core/types/src/dml/index.ts +++ b/packages/core/types/src/dml/index.ts @@ -184,8 +184,7 @@ export type InferHasManyFields = Relation extends () => IDmlEntity< export type InferManyToManyFields = InferHasManyFields /** - * 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 = Prettify< { @@ -204,11 +203,17 @@ export type InferSchemaFields = Prettify< > /** - * Helper to infer the schema type of a DmlEntity + * Infers the schema properties without the relationships */ -export type Infer = T extends IDmlEntity - ? EntityConstructor> - : never +export type InferSchemaProperties = Prettify< + { + [K in keyof Schema as Schema[K] extends { type: infer Type } + ? Type extends RelationshipTypes + ? never + : K + : K]: Schema[K]["$dataType"] + } & InferForeignKeys +> /** * 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 extends IDmlEntity + ? EntityConstructor> + : never + /** * The actions to cascade from a given entity to its * relationship. @@ -251,22 +263,33 @@ export type InferEntityType = T extends IDmlEntity /** * Infer all indexable properties from a DML entity including inferred foreign keys and excluding relationship */ -export type InferIndexableProperties = 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 - : never) +export type InferIndexableProperties = + keyof InferSchemaProperties + +/** + * Returns a list of columns that could be mentioned + * within the checks + */ +export type InferCheckConstraintsProperties = { + [K in keyof InferSchemaProperties]: string +} + +/** + * Options supported when defining a PostgreSQL check + */ +export type CheckConstraint = + | ((columns: InferCheckConstraintsProperties) => string) + | { + name?: string + expression?: + | string + | ((columns: InferCheckConstraintsProperties) => 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>[] + on: InferIndexableProperties[] /** * Conditions to restrict which records are indexed. */ - where?: TWhere + where?: Where } export type SimpleQueryValue = string | number | boolean | null diff --git a/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts b/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts index 17488d9cef51c..b14fed3d32faf 100644 --- a/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts +++ b/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts @@ -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) + }) + }) }) diff --git a/packages/core/utils/src/dml/entity.ts b/packages/core/utils/src/dml/entity.ts index 748b52a1374af..0748da3d6f225 100644 --- a/packages/core/utils/src/dml/entity.ts +++ b/packages/core/utils/src/dml/entity.ts @@ -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" @@ -72,6 +73,7 @@ export class DmlEntity< readonly #tableName: string #cascades: EntityCascades = {} #indexes: EntityIndex[] = [] + #checks: CheckConstraint[] = [] constructor(nameOrConfig: TConfig, schema: Schema) { const { name, tableName } = extractNameAndTableName(nameOrConfig) @@ -100,6 +102,7 @@ export class DmlEntity< schema: DMLSchema cascades: EntityCascades indexes: EntityIndex[] + checks: CheckConstraint[] } { 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[] return this } + + /** + */ + checks(checks: CheckConstraint[]) { + this.#checks = checks + return this + } } diff --git a/packages/core/utils/src/dml/helpers/create-mikro-orm-entity.ts b/packages/core/utils/src/dml/helpers/create-mikro-orm-entity.ts index a599da2546ece..5fca76d11111b 100644 --- a/packages/core/utils/src/dml/helpers/create-mikro-orm-entity.ts +++ b/packages/core/utils/src/dml/helpers/create-mikro-orm-entity.ts @@ -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>(entity: T): Infer { 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 @@ -96,6 +97,7 @@ function createMikrORMEntity() { }) applyEntityIndexes(MikroORMEntity, tableName, entityIndexes) + applyChecks(MikroORMEntity, checks) /** * Converting class to a MikroORM entity diff --git a/packages/core/utils/src/dml/helpers/mikro-orm/apply-checks.ts b/packages/core/utils/src/dml/helpers/mikro-orm/apply-checks.ts new file mode 100644 index 0000000000000..6bbdbb8478c88 --- /dev/null +++ b/packages/core/utils/src/dml/helpers/mikro-orm/apply-checks.ts @@ -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, + entityChecks: CheckConstraint[] = [] +) { + entityChecks.forEach((check) => { + Check( + typeof check === "function" + ? { + expression: check as CheckOptions["expression"], + } + : (check as CheckOptions) + )(MikroORMEntity) + }) +}