mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 12:37:20 +00:00
updated repo query schema, repo and withbuilder validation, and reworked relation with building
This commit is contained in:
@@ -103,17 +103,10 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
||||
validated.select = options.select;
|
||||
}
|
||||
|
||||
if (options.with && options.with.length > 0) {
|
||||
for (const entry of options.with) {
|
||||
const related = this.em.relationOf(entity.name, entry);
|
||||
if (!related) {
|
||||
throw new InvalidSearchParamsException(
|
||||
`WITH: "${entry}" is not a relation of "${entity.name}"`
|
||||
);
|
||||
}
|
||||
|
||||
validated.with.push(entry);
|
||||
}
|
||||
if (options.with) {
|
||||
const depth = WithBuilder.validateWiths(this.em, entity.name, options.with);
|
||||
// @todo: determine allowed depth
|
||||
validated.with = options.with;
|
||||
}
|
||||
|
||||
if (options.join && options.join.length > 0) {
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { isObject } from "core/utils";
|
||||
import type { KyselyJsonFrom, RepoQuery } from "data";
|
||||
import { InvalidSearchParamsException } from "data/errors";
|
||||
import type { RepoWithSchema } from "data/server/data-query-impl";
|
||||
import type { Entity, EntityManager, RepositoryQB } from "../../entities";
|
||||
|
||||
export class WithBuilder {
|
||||
private static buildClause(
|
||||
/*private static buildClause(
|
||||
em: EntityManager<any>,
|
||||
qb: RepositoryQB,
|
||||
entity: Entity,
|
||||
withString: string
|
||||
ref: string,
|
||||
config?: RepoQuery
|
||||
) {
|
||||
const relation = em.relationOf(entity.name, withString);
|
||||
if (!relation) {
|
||||
@@ -15,7 +20,6 @@ export class WithBuilder {
|
||||
const cardinality = relation.ref(withString).cardinality;
|
||||
//console.log("with--builder", { entity: entity.name, withString, cardinality });
|
||||
|
||||
const fns = em.connection.fn;
|
||||
const jsonFrom = cardinality === 1 ? fns.jsonObjectFrom : fns.jsonArrayFrom;
|
||||
|
||||
if (!jsonFrom) {
|
||||
@@ -27,16 +31,69 @@ export class WithBuilder {
|
||||
} catch (e) {
|
||||
throw new Error(`Could not build "with" relation "${withString}": ${(e as any).message}`);
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
static addClause(em: EntityManager<any>, qb: RepositoryQB, entity: Entity, withs: string[]) {
|
||||
if (withs.length === 0) return qb;
|
||||
static addClause(
|
||||
em: EntityManager<any>,
|
||||
qb: RepositoryQB,
|
||||
entity: Entity,
|
||||
withs: RepoQuery["with"]
|
||||
) {
|
||||
if (!withs || !isObject(withs)) {
|
||||
console.warn(`'withs' undefined or invalid, given: ${JSON.stringify(withs)}`);
|
||||
return qb;
|
||||
}
|
||||
|
||||
const fns = em.connection.fn;
|
||||
let newQb = qb;
|
||||
for (const entry of withs) {
|
||||
newQb = WithBuilder.buildClause(em, newQb, entity, entry);
|
||||
|
||||
for (const [ref, query] of Object.entries(withs)) {
|
||||
const relation = em.relationOf(entity.name, ref);
|
||||
if (!relation) {
|
||||
throw new Error(`Relation "${entity.name}<>${ref}" not found`);
|
||||
}
|
||||
const cardinality = relation.ref(ref).cardinality;
|
||||
const jsonFrom: KyselyJsonFrom =
|
||||
cardinality === 1 ? fns.jsonObjectFrom : fns.jsonArrayFrom;
|
||||
if (!jsonFrom) {
|
||||
throw new Error("Connection does not support jsonObjectFrom/jsonArrayFrom");
|
||||
}
|
||||
|
||||
const alias = relation.other(entity).reference;
|
||||
newQb = newQb.select((eb) => {
|
||||
return jsonFrom(relation.buildWith(entity, ref)(eb)).as(alias);
|
||||
});
|
||||
//newQb = relation.buildWith(entity, qb, jsonFrom, ref);
|
||||
}
|
||||
|
||||
return newQb;
|
||||
}
|
||||
|
||||
static validateWiths(em: EntityManager<any>, entity: string, withs: RepoQuery["with"]) {
|
||||
let depth = 0;
|
||||
if (!withs || !isObject(withs)) {
|
||||
console.warn(`'withs' undefined or invalid, given: ${JSON.stringify(withs)}`);
|
||||
return depth;
|
||||
}
|
||||
|
||||
const child_depths: number[] = [];
|
||||
for (const [ref, query] of Object.entries(withs)) {
|
||||
const related = em.relationOf(entity, ref);
|
||||
if (!related) {
|
||||
throw new InvalidSearchParamsException(
|
||||
`WITH: "${ref}" is not a relation of "${entity}"`
|
||||
);
|
||||
}
|
||||
depth++;
|
||||
|
||||
if ("with" in query) {
|
||||
child_depths.push(WithBuilder.validateWiths(em, ref, query.with as any));
|
||||
}
|
||||
}
|
||||
if (child_depths.length > 0) {
|
||||
depth += Math.max(...child_depths);
|
||||
}
|
||||
|
||||
return depth;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type Static, Type, parse } from "core/utils";
|
||||
import type { SelectQueryBuilder } from "kysely";
|
||||
import type { ExpressionBuilder, SelectQueryBuilder } from "kysely";
|
||||
import type { Entity, EntityData, EntityManager } from "../entities";
|
||||
import {
|
||||
type EntityRelationAnchor,
|
||||
@@ -67,10 +67,8 @@ export abstract class EntityRelation<
|
||||
*/
|
||||
abstract buildWith(
|
||||
entity: Entity,
|
||||
qb: KyselyQueryBuilder,
|
||||
jsonFrom: KyselyJsonFrom,
|
||||
reference: string
|
||||
): KyselyQueryBuilder;
|
||||
): (eb: ExpressionBuilder<any, any>) => KyselyQueryBuilder;
|
||||
|
||||
abstract buildJoin(
|
||||
entity: Entity,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { type Static, Type } from "core/utils";
|
||||
import type { ExpressionBuilder } from "kysely";
|
||||
import { Entity, type EntityManager } from "../entities";
|
||||
import { type Field, PrimaryField, VirtualField } from "../fields";
|
||||
import type { RepoQuery } from "../server/data-query-impl";
|
||||
@@ -123,7 +124,7 @@ export class ManyToManyRelation extends EntityRelation<typeof ManyToManyRelation
|
||||
.groupBy(groupBy);
|
||||
}
|
||||
|
||||
buildWith(entity: Entity, qb: KyselyQueryBuilder, jsonFrom: KyselyJsonFrom) {
|
||||
buildWith(entity: Entity) {
|
||||
if (!this.em) {
|
||||
throw new Error("EntityManager not set, can't build");
|
||||
}
|
||||
@@ -138,7 +139,29 @@ export class ManyToManyRelation extends EntityRelation<typeof ManyToManyRelation
|
||||
(f) => !(f instanceof RelationField || f instanceof PrimaryField)
|
||||
);
|
||||
|
||||
return qb.select((eb) => {
|
||||
return (eb: ExpressionBuilder<any, any>) =>
|
||||
eb
|
||||
.selectFrom(other.entity.name)
|
||||
.select((eb2) => {
|
||||
const select: any[] = other.entity.getSelect(other.entity.name);
|
||||
if (additionalFields.length > 0) {
|
||||
const conn = this.connectionEntity.name;
|
||||
select.push(
|
||||
jsonBuildObject(
|
||||
Object.fromEntries(
|
||||
additionalFields.map((f) => [f.name, eb2.ref(`${conn}.${f.name}`)])
|
||||
)
|
||||
).as(this.connectionTableMappedName)
|
||||
);
|
||||
}
|
||||
|
||||
return select;
|
||||
})
|
||||
.whereRef(entityRef, "=", otherRef)
|
||||
.innerJoin(...join)
|
||||
.limit(limit);
|
||||
|
||||
/*return qb.select((eb) => {
|
||||
const select: any[] = other.entity.getSelect(other.entity.name);
|
||||
// @todo: also add to find by references
|
||||
if (additionalFields.length > 0) {
|
||||
@@ -160,7 +183,7 @@ export class ManyToManyRelation extends EntityRelation<typeof ManyToManyRelation
|
||||
.innerJoin(...join)
|
||||
.limit(limit)
|
||||
).as(other.reference);
|
||||
});
|
||||
});*/
|
||||
}
|
||||
|
||||
initialize(em: EntityManager<any>) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { PrimaryFieldType } from "core";
|
||||
import { snakeToPascalWithSpaces } from "core/utils";
|
||||
import { type Static, Type } from "core/utils";
|
||||
import type { ExpressionBuilder } from "kysely";
|
||||
import type { Entity, EntityManager } from "../entities";
|
||||
import type { RepoQuery } from "../server/data-query-impl";
|
||||
import { EntityRelation, type KyselyJsonFrom, type KyselyQueryBuilder } from "./EntityRelation";
|
||||
@@ -155,15 +156,22 @@ export class ManyToOneRelation extends EntityRelation<typeof ManyToOneRelation.s
|
||||
return qb.innerJoin(self.entity.name, entityRef, otherRef).groupBy(groupBy);
|
||||
}
|
||||
|
||||
buildWith(entity: Entity, qb: KyselyQueryBuilder, jsonFrom: KyselyJsonFrom, reference: string) {
|
||||
buildWith(entity: Entity, reference: string) {
|
||||
const { self, entityRef, otherRef, relationRef } = this.queryInfo(entity, reference);
|
||||
const limit =
|
||||
self.cardinality === 1
|
||||
? 1
|
||||
: this.config.with_limit ?? ManyToOneRelation.DEFAULTS.with_limit;
|
||||
: (this.config.with_limit ?? ManyToOneRelation.DEFAULTS.with_limit);
|
||||
//console.log("buildWith", entity.name, reference, { limit });
|
||||
|
||||
return qb.select((eb) =>
|
||||
return (eb: ExpressionBuilder<any, any>) =>
|
||||
eb
|
||||
.selectFrom(`${self.entity.name} as ${relationRef}`)
|
||||
.select(self.entity.getSelect(relationRef))
|
||||
.whereRef(entityRef, "=", otherRef)
|
||||
.limit(limit);
|
||||
|
||||
/*return qb.select((eb) =>
|
||||
jsonFrom(
|
||||
eb
|
||||
.selectFrom(`${self.entity.name} as ${relationRef}`)
|
||||
@@ -171,7 +179,7 @@ export class ManyToOneRelation extends EntityRelation<typeof ManyToOneRelation.s
|
||||
.whereRef(entityRef, "=", otherRef)
|
||||
.limit(limit)
|
||||
).as(relationRef)
|
||||
);
|
||||
);*/
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { type Static, Type } from "core/utils";
|
||||
import type { ExpressionBuilder } from "kysely";
|
||||
import type { Entity, EntityManager } from "../entities";
|
||||
import { NumberField, TextField } from "../fields";
|
||||
import type { RepoQuery } from "../server/data-query-impl";
|
||||
@@ -87,11 +88,19 @@ export class PolymorphicRelation extends EntityRelation<typeof PolymorphicRelati
|
||||
};
|
||||
}
|
||||
|
||||
buildWith(entity: Entity, qb: KyselyQueryBuilder, jsonFrom: KyselyJsonFrom) {
|
||||
buildWith(entity: Entity) {
|
||||
const { other, whereLhs, reference, entityRef, otherRef } = this.queryInfo(entity);
|
||||
const limit = other.cardinality === 1 ? 1 : 5;
|
||||
|
||||
return qb.select((eb) =>
|
||||
return (eb: ExpressionBuilder<any, any>) =>
|
||||
eb
|
||||
.selectFrom(other.entity.name)
|
||||
.select(other.entity.getSelect(other.entity.name))
|
||||
.where(whereLhs, "=", reference)
|
||||
.whereRef(entityRef, "=", otherRef)
|
||||
.limit(limit);
|
||||
|
||||
/*return qb.select((eb) =>
|
||||
jsonFrom(
|
||||
eb
|
||||
.selectFrom(other.entity.name)
|
||||
@@ -100,7 +109,7 @@ export class PolymorphicRelation extends EntityRelation<typeof PolymorphicRelati
|
||||
.whereRef(entityRef, "=", otherRef)
|
||||
.limit(limit)
|
||||
).as(other.reference)
|
||||
);
|
||||
);*/
|
||||
}
|
||||
|
||||
override isListableFor(entity: Entity): boolean {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { TThis } from "@sinclair/typebox";
|
||||
import {
|
||||
type SchemaOptions,
|
||||
type Static,
|
||||
@@ -64,19 +65,60 @@ export const whereSchema = Type.Transform(
|
||||
})
|
||||
.Encode(JSON.stringify);
|
||||
|
||||
export const querySchema = Type.Object(
|
||||
{
|
||||
limit: Type.Optional(limit),
|
||||
offset: Type.Optional(offset),
|
||||
sort: Type.Optional(sort),
|
||||
select: Type.Optional(stringArray),
|
||||
with: Type.Optional(stringArray),
|
||||
join: Type.Optional(stringArray),
|
||||
where: Type.Optional(whereSchema)
|
||||
},
|
||||
{
|
||||
additionalProperties: false
|
||||
export type ShallowRepoQuery = {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
sort?: string | { by: string; dir: "asc" | "desc" };
|
||||
select?: string[];
|
||||
with?: string[] | Record<string, ShallowRepoQuery>;
|
||||
join?: string[];
|
||||
where?: any;
|
||||
};
|
||||
export type RepoWithSchema = Record<
|
||||
string,
|
||||
Omit<ShallowRepoQuery, "with"> & {
|
||||
with?: unknown;
|
||||
}
|
||||
>;
|
||||
export const withSchema = <TSelf extends TThis>(Self: TSelf) =>
|
||||
Type.Transform(Type.Union([stringArray, Type.Record(Type.String(), Self)]))
|
||||
.Decode((value) => {
|
||||
let _value = value;
|
||||
if (Array.isArray(value)) {
|
||||
if (!value.every((v) => typeof v === "string")) {
|
||||
throw new Error("Invalid 'with' schema");
|
||||
}
|
||||
|
||||
_value = value.reduce((acc, v) => {
|
||||
acc[v] = {};
|
||||
return acc;
|
||||
}, {} as RepoWithSchema);
|
||||
}
|
||||
|
||||
return _value as RepoWithSchema;
|
||||
})
|
||||
.Encode((value) => value);
|
||||
|
||||
export const querySchema = Type.Recursive(
|
||||
(Self) =>
|
||||
Type.Partial(
|
||||
Type.Object(
|
||||
{
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
sort: sort,
|
||||
select: stringArray,
|
||||
with: withSchema(Self),
|
||||
join: stringArray,
|
||||
where: whereSchema
|
||||
},
|
||||
{
|
||||
// @todo: determine if unknown is allowed, it's ignore anyway
|
||||
additionalProperties: false
|
||||
}
|
||||
)
|
||||
),
|
||||
{ $id: "query-schema" }
|
||||
);
|
||||
|
||||
export type RepoQueryIn = Static<typeof querySchema>;
|
||||
|
||||
Reference in New Issue
Block a user