import { type Static, Type } from "core/utils"; import type { Entity, EntityManager } from "../entities"; import { NumberField, TextField } from "../fields"; import type { RepoQuery } from "../server/data-query-impl"; import { EntityRelation, type KyselyJsonFrom, type KyselyQueryBuilder } from "./EntityRelation"; import { EntityRelationAnchor } from "./EntityRelationAnchor"; import { type RelationType, RelationTypes } from "./relation-types"; export type PolymorphicRelationConfig = Static; // @todo: what about cascades? export class PolymorphicRelation extends EntityRelation { static override schema = Type.Composite( [ EntityRelation.schema, Type.Object({ targetCardinality: Type.Optional(Type.Number()) }) ], { additionalProperties: false } ); constructor(source: Entity, target: Entity, config: Partial = {}) { const mappedBy = config.mappedBy || target.name; const inversedBy = config.inversedBy || source.name; // if target can be multiple, allow it. otherwise unlimited const targetCardinality = typeof config.targetCardinality === "number" && config.targetCardinality > 0 ? config.targetCardinality : undefined; const sourceAnchor = new EntityRelationAnchor(source, inversedBy, 1); const targetAnchor = new EntityRelationAnchor(target, mappedBy, targetCardinality); super(sourceAnchor, targetAnchor, config); this.directions = ["source"]; } type(): RelationType { return RelationTypes.Polymorphic; } private queryInfo(entity: Entity) { const other = this.other(entity); const whereLhs = `${other.entity.name}.${this.getReferenceField().name}`; const reference = `${entity.name}.${this.config.mappedBy}`; // this is used for "getReferenceQuery" const reference_other = `${other.entity.name}.${this.config.mappedBy}`; const entityRef = `${entity.name}.${entity.getPrimaryField().name}`; const otherRef = `${other.entity.name}.${this.getEntityIdField().name}`; const groupBy = `${entity.name}.${entity.getPrimaryField().name}`; return { other, whereLhs, reference, reference_other, entityRef, otherRef, groupBy }; } buildJoin(entity: Entity, qb: KyselyQueryBuilder) { const { other, whereLhs, reference, entityRef, otherRef, groupBy } = this.queryInfo(entity); return qb .innerJoin(other.entity.name, (join) => join.onRef(entityRef, "=", otherRef).on(whereLhs, "=", reference) ) .groupBy(groupBy); } override getReferenceQuery(entity: Entity, id: number): Partial { const info = this.queryInfo(entity); return { where: { [this.getReferenceField().name]: info.reference_other, [this.getEntityIdField().name]: id } }; } buildWith(entity: Entity, qb: KyselyQueryBuilder, jsonFrom: KyselyJsonFrom) { const { other, whereLhs, reference, entityRef, otherRef } = this.queryInfo(entity); const limit = other.cardinality === 1 ? 1 : 5; return qb.select((eb) => jsonFrom( eb .selectFrom(other.entity.name) .select(other.entity.getSelect(other.entity.name)) .where(whereLhs, "=", reference) .whereRef(entityRef, "=", otherRef) .limit(limit) ).as(other.reference) ); } override isListableFor(entity: Entity): boolean { // @todo: only make listable if many? check cardinality return this.source.entity.name === entity.name && this.target.cardinality !== 1; } getReferenceField(): TextField { return new TextField("reference", { hidden: true, fillable: ["create"] }); } getEntityIdField(): NumberField { return new NumberField("entity_id", { hidden: true, fillable: ["create"] }); } initialize(em: EntityManager) { const referenceField = this.getReferenceField(); const entityIdField = this.getEntityIdField(); if (!this.target.entity.field(referenceField.name)) { this.target.entity.addField(referenceField); } if (!this.target.entity.field(entityIdField.name)) { this.target.entity.addField(entityIdField); } } }