Files
bknd/app/src/data/relations/PolymorphicRelation.ts
dswbx 569d021316 fix: update OneToOneRelation and PolymorphicRelation configurations
Enhanced OneToOneRelation to allow source to create target with a mapping field and added a limit. Updated PolymorphicRelation to return a TextField for entity_id instead of NumberField, improving type consistency.
2025-10-01 09:00:44 +02:00

121 lines
4.2 KiB
TypeScript

import type { ExpressionBuilder } from "kysely";
import type { Entity, EntityManager } from "../entities";
import { NumberField, TextField } from "../fields";
import type { RepoQuery } from "../server/query";
import { EntityRelation, type KyselyJsonFrom, type KyselyQueryBuilder } from "./EntityRelation";
import { EntityRelationAnchor } from "./EntityRelationAnchor";
import { type RelationType, RelationTypes } from "./relation-types";
import { s } from "bknd/utils";
import type { PrimaryFieldType } from "bknd";
export type PolymorphicRelationConfig = s.Static<typeof PolymorphicRelation.schema>;
// @todo: what about cascades?
export class PolymorphicRelation extends EntityRelation<typeof PolymorphicRelation.schema> {
static override schema = s.strictObject({
targetCardinality: s.number().optional(),
...EntityRelation.schema.properties,
});
constructor(source: Entity, target: Entity, config: Partial<PolymorphicRelationConfig> = {}) {
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: PrimaryFieldType): Partial<RepoQuery> {
const info = this.queryInfo(entity);
return {
where: {
[this.getReferenceField().name]: info.reference_other,
[this.getEntityIdField().name]: id,
},
};
}
buildWith(entity: Entity) {
const { other, whereLhs, reference, entityRef, otherRef } = this.queryInfo(entity);
return (eb: ExpressionBuilder<any, any>) =>
eb
.selectFrom(other.entity.name)
.where(whereLhs, "=", reference)
.whereRef(entityRef, "=", otherRef)
.$if(other.cardinality === 1, (qb) => qb.limit(1));
}
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(): TextField {
return new TextField("entity_id", { hidden: true, fillable: ["create"] });
}
initialize(em: EntityManager<any>) {
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);
}
}
}