From 32c2c997b75524f91c83bd3e8cc53cbd70d1d831 Mon Sep 17 00:00:00 2001 From: Jeongho Nam Date: Sun, 6 Mar 2022 22:20:34 +0900 Subject: [PATCH] Close #56 and Close #57 --- package.json | 2 +- src/builders/AppJoinBuilder.ts | 33 ++-- .../internal/app_join_belongs_many_to_one.ts | 58 ++++--- .../internal/app_join_belongs_one_to_one.ts | 60 ++++--- .../internal/app_join_has_many_to_many.ts | 155 ++++++++++++++---- .../internal/app_join_has_one_to_many.ts | 60 ++++--- .../internal/app_join_has_one_to_one.ts | 58 ++++--- .../internal/get_app_join_function.ts | 51 ++++++ src/functional/appJoin.ts | 73 +++++++++ src/functional/index.ts | 1 + src/test/features/test_app_join.ts | 100 +++++++++++ src/test/features/test_json_select_builder.ts | 19 +-- src/typings/Creator.ts | 2 +- 13 files changed, 526 insertions(+), 146 deletions(-) create mode 100644 src/builders/internal/get_app_join_function.ts create mode 100644 src/functional/appJoin.ts create mode 100644 src/test/features/test_app_join.ts diff --git a/package.json b/package.json index 9c2b222..d0a3371 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "safe-typeorm", - "version": "1.0.5", + "version": "1.0.6", "description": "Safe Relationship Decorators for the TypeORM", "main": "lib/index.js", "typings": "lib/index.d.ts", diff --git a/src/builders/AppJoinBuilder.ts b/src/builders/AppJoinBuilder.ts index b8af229..c836c44 100644 --- a/src/builders/AppJoinBuilder.ts +++ b/src/builders/AppJoinBuilder.ts @@ -6,11 +6,7 @@ import { SpecialFields } from "../typings/SpecialFields"; import { ReflectAdaptor } from "../decorators/base/ReflectAdaptor"; import { IAppJoinChildTuple } from "./internal/IAppJoinChildTuple"; -import { app_join_belongs_many_to_one } from "./internal/app_join_belongs_many_to_one"; -import { app_join_belongs_one_to_one } from "./internal/app_join_belongs_one_to_one"; -import { app_join_has_many_to_many } from "./internal/app_join_has_many_to_many"; -import { app_join_has_one_to_many } from "./internal/app_join_has_one_to_many"; -import { app_join_has_one_to_one } from "./internal/app_join_has_one_to_one"; +import { get_app_join_function } from "./internal/get_app_join_function"; /** * Application level join builder. @@ -74,8 +70,6 @@ export class AppJoinBuilder */ public constructor(mine: Creator) { - new Array(3) - this.mine_ = mine; this.children_ = new Map(); } @@ -209,21 +203,16 @@ export class AppJoinBuilder { for (const [field, child] of this.children_.entries()) { - // JOIN WITH RELATED ENTITIES - let output: Relationship.TargetType[]; - if (child.metadata.type === "Belongs.ManyToOne" || child.metadata.type === "Belongs.External.ManyToOne") - output = await app_join_belongs_many_to_one(this.mine_, child as any, data, field); - else if (child.metadata.type === "Belongs.OneToOne" || child.metadata.type === "Belongs.External.OneToOne") - output = await app_join_belongs_one_to_one(child as any, data, field); - else if (child.metadata.type === "Has.OneToOne" || child.metadata.type === "Has.External.OneToOne") - output = await app_join_has_one_to_one(this.mine_, child as any, data, field); - else if (child.metadata.type === "Has.OneToMany" || child.metadata.type === "Has.External.OneToMany") - output = await app_join_has_one_to_many(this.mine_, child as any, data, field); - else if (child.metadata.type === "Has.ManyToMany") - output = await app_join_has_many_to_many(this.mine_, child as any, data, field); - else - continue; - + // CALL APP JOIN FUNCTION + const func: Function = get_app_join_function(child.metadata.type); + const output: Relationship.TargetType[] = await func + ( + this.mine_, + child.metadata, + data, + field + ); + // HIERARCHICAL CALL if (output.length !== 0) await child.builder._Execute(output); diff --git a/src/builders/internal/app_join_belongs_many_to_one.ts b/src/builders/internal/app_join_belongs_many_to_one.ts index 3d4f296..c415329 100644 --- a/src/builders/internal/app_join_belongs_many_to_one.ts +++ b/src/builders/internal/app_join_belongs_many_to_one.ts @@ -1,54 +1,68 @@ import { Belongs } from "../../decorators/Belongs"; import { Creator } from "../../typings/Creator"; +import { SpecialFields } from "../../typings/SpecialFields"; import { ITableInfo } from "../../functional/internal/ITableInfo"; -import { IAppJoinChildTuple } from "./IAppJoinChildTuple"; import { get_records_by_where_in } from "./get_records_by_where_in"; /** * @internal */ -export async function app_join_belongs_many_to_one +export async function app_join_belongs_many_to_one< + Mine extends object, + Target extends object, + Field extends SpecialFields | Belongs.External.ManyToOne>> ( - mine: Creator, - child: IAppJoinChildTuple | Belongs.OneToOne.IMetadata>, - data: any[], - field: any, - ): Promise + mine: Creator, + metadata: Belongs.ManyToOne.IMetadata | Belongs.External.ManyToOne.IMetadata, + myData: Mine[], + field: Field, + targetData?: Target[] + ): Promise { // NO DATA - if (data.length === 0) + if (myData.length === 0) return []; // NO REFERENCE - const idList: any[] = data - .map(elem => elem[field].id) + const idList: any[] = myData + .map(elem => (elem[field] as Belongs.ManyToOne).id) .filter(id => id !== null); if (idList.length === 0) return []; // THE TARGET INFO - const target: Creator = child.metadata.target(); + const target: Creator = metadata.target(); const table: ITableInfo = ITableInfo.get(target); // LOAD TARGET DATA - const output: any[] = await get_records_by_where_in(target, table.primaryColumn, idList); + const output: Target[] + = targetData + || await get_records_by_where_in(target, table.primaryColumn, idList); // LINK RELATIONSHIPS - const dict: Map = new Map(output.map(elem => [ elem[table.primaryColumn], elem ])); - for (const elem of data) + const dict: Map = new Map(output.map(elem => [ (elem as any)[table.primaryColumn], elem ])); + for (const elem of myData) { - const id: any | null = elem[field].id; - if (id === null) + const accessor = elem[field] as Belongs.ManyToOne; + if (accessor.id === null) continue; - const reference: any = dict.get(id)!; - await elem[field].set(reference); + const reference: any = dict.get(accessor.id)!; + await accessor.set(reference); } // RECURSIVE - if (target === mine) - await app_join_belongs_many_to_one(mine, child, output, field); - + if (target === mine && targetData === undefined) + { + const surplus: Target[] = await app_join_belongs_many_to_one + ( + mine, + metadata, + output as Mine[], + field + ); + output.push(...surplus); + } return output; -} +} \ No newline at end of file diff --git a/src/builders/internal/app_join_belongs_one_to_one.ts b/src/builders/internal/app_join_belongs_one_to_one.ts index 1193e5a..fabf912 100644 --- a/src/builders/internal/app_join_belongs_one_to_one.ts +++ b/src/builders/internal/app_join_belongs_one_to_one.ts @@ -1,51 +1,73 @@ import { Belongs } from "../../decorators/Belongs"; import { Creator } from "../../typings/Creator"; +import { SpecialFields } from "../../typings/SpecialFields"; import { ITableInfo } from "../../functional/internal/ITableInfo"; -import { IAppJoinChildTuple } from "./IAppJoinChildTuple"; import { get_records_by_where_in } from "./get_records_by_where_in"; /** * @internal */ -export async function app_join_belongs_one_to_one +export async function app_join_belongs_one_to_one< + Mine extends object, + Target extends object, + Field extends SpecialFields | Belongs.External.OneToOne>> ( - child: IAppJoinChildTuple | Belongs.OneToOne.IMetadata>, - data: any[], - field: any, - ): Promise + mine: Creator, + metadata: Belongs.OneToOne.IMetadata | Belongs.External.OneToOne.IMetadata, + myData: Mine[], + field: Field, + targetData?: Target[] + ): Promise { // NO DATA - if (data.length === 0) + if (myData.length === 0) return []; // NO REFERENCE - const idList: any[] = data - .map(elem => elem[field].id) + const idList: any[] = myData + .map(elem => (elem[field] as Belongs.ManyToOne).id) .filter(id => id !== null); if (idList.length === 0) return []; // THE TARGET INFO - const target: Creator = child.metadata.target(); + const target: Creator = metadata.target(); const table: ITableInfo = ITableInfo.get(target); // LOAD TARGET DATA - const output: any[] = await get_records_by_where_in(target, table.primaryColumn, idList); + const output: Target[] + = targetData + || await get_records_by_where_in(target, table.primaryColumn, idList); // LINK RELATIONSHIPS - const dict: Map = new Map(output.map(elem => [ elem[table.primaryColumn], elem ])); - for (const elem of data) + const dict: Map = new Map(output.map(elem => [ (elem as any)[table.primaryColumn], elem ])); + for (const elem of myData) { - const id: any | null = elem[field].id; - if (id === null) + const accessor = elem[field] as Belongs.ManyToOne; + if (accessor.id === null) continue; - const reference: any = dict.get(id)!; - await elem[field].set(reference); + const reference: any | undefined = dict.get(accessor.id); + if (reference === undefined) + continue; + + await accessor.set(reference); + if (metadata.inverse !== null) + await reference[metadata.inverse].set(elem); + } - if (child.metadata.inverse !== null) - await reference[child.metadata.inverse].set(elem); + // RECURSIVE + if (target === mine && targetData === undefined) + { + const surplus: Target[] = await app_join_belongs_one_to_one + ( + mine, + metadata, + output as Mine[], + field + ); + output.push(...surplus); } return output; } diff --git a/src/builders/internal/app_join_has_many_to_many.ts b/src/builders/internal/app_join_has_many_to_many.ts index ece4082..7909a81 100644 --- a/src/builders/internal/app_join_has_many_to_many.ts +++ b/src/builders/internal/app_join_has_many_to_many.ts @@ -9,74 +9,165 @@ import { findRepository } from "../../functional/findRepository"; import { AppJoinBuilder } from "../AppJoinBuilder"; import { JoinQueryBuilder } from "../JoinQueryBuilder"; - -import { IAppJoinChildTuple } from "./IAppJoinChildTuple"; +import { get_records_by_where_in } from "./get_records_by_where_in"; +import { Belongs } from "../../decorators/Belongs"; /** * @internal */ -export async function app_join_has_many_to_many +export async function app_join_has_many_to_many< + Mine extends object, + Target extends object, + Router extends object, + Field extends Has.ManyToMany.IMetadata> ( - mine: Creator, - child: IAppJoinChildTuple>, - data: any[], - field: any - ): Promise + mine: Creator, + metadata: Has.ManyToMany.IMetadata, + myData: Mine[], + field: Field, + targetData?: Target[], + routerData?: Router[], + ): Promise { - if (data.length === 0) + // NO DATA OR MANUAL JOIN + if (myData.length === 0) return []; + else if (targetData !== undefined) + return app_join_has_many_to_many_manual + ( + mine, + metadata, + myData, + field, + targetData, + routerData + ); // MY TABLE & DATA const myTable: ITableInfo = ITableInfo.get(mine); - const myDict: Map> = new Map(data.map(elem => [ - elem[myTable.primaryColumn], + const myDict: Map[]>> = new Map(myData.map(elem => [ + (elem as any)[myTable.primaryColumn], new Pair(elem, []) ])); - const myIdList: any[] = data.map(rec => rec[myTable.primaryColumn]); + const myIdList: any[] = myData.map(rec => (rec as any)[myTable.primaryColumn]); // LOAD TARGET DATA - const router: Creator = child.metadata.router(); - const stmt: orm.SelectQueryBuilder = + const router: Creator = metadata.router(); + const stmt: orm.SelectQueryBuilder = findRepository(router) .createQueryBuilder(); - new JoinQueryBuilder(stmt, router).innerJoinAndSelect(child.metadata.target_inverse); + new JoinQueryBuilder(stmt, router) + .innerJoinAndSelect(metadata.target_inverse); - const routeList: any[] = []; + const routeList: Router[] = []; while (myIdList.length !== 0) { const some: any[] = myIdList.splice(0, AppJoinBuilder.MAX_VARIABLE_COUNT); - routeList.push(...await stmt + const records: Router[] = await stmt .clone() - .andWhere(...getWhereArguments(router, child.metadata.my_inverse as "id", "IN", some)) - .getMany() - ); + .andWhere(...getWhereArguments(router, metadata.my_inverse as "id", "IN", some)) + .getMany(); + routeList.push(...records); } - const output: any[] = []; + const output: Target[] = []; // LINK RELATIONSHIPS for (const router of routeList) { - const entry: any = myDict.get(router[child.metadata.my_inverse].id)!; - const target: any = await router[child.metadata.target_inverse].get(); + const entry: Pair[]> | undefined = myDict.get((router as any)[metadata.my_inverse].id); + if (entry === undefined) + continue; - const tuple: Has.ManyToMany.ITuple = { - router: router, - target: target, + const target: Target = await (router as any)[metadata.target_inverse].get(); + const tuple: Has.ManyToMany.ITuple = { + router, + target, }; entry.second.push(tuple); output.push(target); } for (const entry of myDict.values()) { - if (child.metadata.comparator) - entry.second = entry.second.sort(child.metadata.comparator); - await entry.first[field].set(entry.second.map(elem => elem.target)); + if (metadata.comparator) + entry.second = entry.second.sort(metadata.comparator); + await (entry.first as any)[field].set(entry.second.map(elem => elem.target)); } // RECURSIVE - if (child.metadata.target() === mine) - await app_join_has_many_to_many(mine, child, output, field); - + if (metadata.target() === mine) + { + const surplus: Target[] = await app_join_has_many_to_many + ( + mine, + metadata, + output as Mine[], + field + ); + output.push(...surplus); + } return output; +} + +async function app_join_has_many_to_many_manual< + Mine extends object, + Target extends object, + Router extends object, + Field extends Has.ManyToMany.IMetadata> + ( + mine: Creator, + metadata: Has.ManyToMany.IMetadata, + myData: Mine[], + field: Field, + targetData: Target[], + routerData?: Router[], + ): Promise +{ + // MY TABLE & DATA + const myTable: ITableInfo = ITableInfo.get(mine); + const myDict: Map> = new Map(myData.map(elem => [ + (elem as any)[myTable.primaryColumn], + new Pair(elem, []) + ])); + const myIdList: any[] = myData.map(rec => (rec as any)[myTable.primaryColumn]); + + // TARGET TABLE & DATA + const targetTable: ITableInfo = ITableInfo.get(metadata.target()); + const targetDict: Map = new Map(targetData.map(elem => [ + (elem as any)[targetTable.primaryColumn], + elem + ])); + + // LOAD ROUTER DATA + if (routerData === undefined) + routerData = await get_records_by_where_in + ( + metadata.router(), + metadata.my_inverse, + myIdList + ); + + // LINK RELATIONSHIPS + for (const router of routerData) + { + const myInverse = (router as any)[metadata.my_inverse] as Belongs.ManyToOne; + const myTuple: Pair | undefined = myDict.get(myInverse.id); + if (myTuple === undefined) + continue; + + const targetInverse = (router as any)[metadata.target_inverse] as Belongs.ManyToOne; + const target: Target | undefined = targetDict.get(targetInverse.id); + if (target === undefined) + continue; + + myTuple.second.push(target); + } + for (const entry of myDict.values()) + { + const accessor: Has.ManyToMany = (entry.first as any)[field]; + await accessor.set(entry.second); + } + + // RETURNS + return targetData; } \ No newline at end of file diff --git a/src/builders/internal/app_join_has_one_to_many.ts b/src/builders/internal/app_join_has_one_to_many.ts index fff103f..66827e5 100644 --- a/src/builders/internal/app_join_has_one_to_many.ts +++ b/src/builders/internal/app_join_has_one_to_many.ts @@ -2,54 +2,74 @@ import { Pair } from "tstl/utility/Pair"; import { Creator } from "../../typings/Creator"; import { Has } from "../../decorators/Has"; +import { SpecialFields } from "../../typings/SpecialFields"; import { ITableInfo } from "../../functional/internal/ITableInfo"; -import { IAppJoinChildTuple } from "./IAppJoinChildTuple"; import { get_records_by_where_in } from "./get_records_by_where_in"; +import { Belongs } from "../../decorators"; /** * @internal */ -export async function app_join_has_one_to_many +export async function app_join_has_one_to_many< + Mine extends object, + Target extends object, + Field extends SpecialFields | Has.External.OneToMany>> ( - mine: Creator, - child: IAppJoinChildTuple>, - data: any[], - field: any + mine: Creator, + metadata: Has.OneToMany.IMetadata | Has.External.OneToMany.IMetadata, + myData: Mine[], + field: Field, + targetData?: Target[] ): Promise { - if (data.length === 0) + if (myData.length === 0) return []; // INFO - const target: Creator = child.metadata.target(); + const target: Creator = metadata.target(); const myTable: ITableInfo = ITableInfo.get(mine); // LOAD TARGET DATA - const myDict: Map> = new Map(data.map(elem => [ - elem[myTable.primaryColumn], + const myDict: Map> = new Map(myData.map(elem => [ + (elem as any)[myTable.primaryColumn], new Pair(elem, []) ])) - const myIdList: any[] = data.map(rec => rec[myTable.primaryColumn]); - const output: any[] = await get_records_by_where_in(target, child.metadata.inverse, myIdList); + const myIdList: any[] = myData.map(rec => (rec as any)[myTable.primaryColumn]); + const output: Target[] + = targetData + || await get_records_by_where_in(target, metadata.inverse, myIdList); // LINK RELATIONSHIPS for (const targetRecord of output) { - const tuple: Pair = myDict.get(targetRecord[child.metadata.inverse].id)!; + const inverse: Belongs.ManyToOne = (targetRecord as any)[metadata.inverse]; + const tuple: Pair | undefined = myDict.get(inverse.id); + + if (tuple === undefined) + continue; + tuple.second.push(targetRecord); - await targetRecord[child.metadata.inverse].set(tuple.first); + await inverse.set(tuple.first); } for (const tuple of myDict.values()) { - if (child.metadata.comparator) - tuple.second = tuple.second.sort(child.metadata.comparator); - await tuple.first[field].set(tuple.second); + if (metadata.comparator) + tuple.second = tuple.second.sort(metadata.comparator); + await (tuple.first[field] as Has.OneToMany).set(tuple.second); } // RECURSIVE - if (target === mine) - await app_join_has_one_to_many(mine, child, output, field); - + if (target === mine && targetData === undefined) + { + const surplus: Target[] = await app_join_has_one_to_many + ( + mine, + metadata, + output as Mine[], + field + ); + output.push(...surplus); + } return output; } \ No newline at end of file diff --git a/src/builders/internal/app_join_has_one_to_one.ts b/src/builders/internal/app_join_has_one_to_one.ts index 33acb78..ce9f92e 100644 --- a/src/builders/internal/app_join_has_one_to_one.ts +++ b/src/builders/internal/app_join_has_one_to_one.ts @@ -4,49 +4,69 @@ import { Creator } from "../../typings/Creator"; import { Has } from "../../decorators/Has"; import { ITableInfo } from "../../functional/internal/ITableInfo"; -import { IAppJoinChildTuple } from "./IAppJoinChildTuple"; import { get_records_by_where_in } from "./get_records_by_where_in"; +import { SpecialFields } from "../../typings/SpecialFields"; +import { Belongs } from "../../decorators"; /** * @internal */ -export async function app_join_has_one_to_one +export async function app_join_has_one_to_one< + Mine extends object, + Target extends object, + Field extends SpecialFields | Has.External.OneToOne.IMetadata>> ( - mine: Creator, - child: IAppJoinChildTuple>, - data: any[], - field: any, - ): Promise + mine: Creator, + metadata: Has.OneToOne.IMetadata | Has.External.OneToOne.IMetadata, + myData: Mine[], + field: Field, + targetData?: Target[] + ): Promise { - if (data.length === 0) + if (myData.length === 0) return []; // MY TABLE & DATA const myTable: ITableInfo = ITableInfo.get(mine); - const myDict: Map> = new Map(data.map(record => + const myDict: Map> = new Map(myData.map(record => [ - record[myTable.primaryColumn], + (record as any)[myTable.primaryColumn], new Pair(record, null) ])); - const myIdList: any[] = data.map(rec => rec[myTable.primaryColumn]); + const myIdList: any[] = myData.map(rec => (rec as any)[myTable.primaryColumn]); // LOAD TARGET DATA - const target: Creator = child.metadata.target(); - const output: any[] = await get_records_by_where_in(target, child.metadata.inverse, myIdList); + const target: Creator = metadata.target(); + const output: Target[] + = targetData + || await get_records_by_where_in(target, metadata.inverse, myIdList); // LINK RELATIONSHIPS for (const targetRecord of output) { - const tuple: any = myDict.get(targetRecord[child.metadata.inverse].id)!; + const inverse: Belongs.OneToOne = (targetRecord as any)[metadata.inverse]; + const tuple: Pair | undefined = myDict.get(inverse.id); + + if (tuple === undefined) + continue; + tuple.second = targetRecord; - await targetRecord[child.metadata.inverse].set(tuple.first); + await inverse.set(tuple.first); } for (const tuple of myDict.values()) - await tuple.first[field].set(tuple.second); + await (tuple.first[field] as Has.OneToOne).set(tuple.second); // RECURSIVE - if (target === mine) - await app_join_has_one_to_one(mine, child, output, field); - + if (target === mine && targetData === undefined) + { + const surplus: Target[] = await app_join_has_one_to_one + ( + mine, + metadata, + output as Mine[], + field + ); + output.push(...surplus); + } return output; } \ No newline at end of file diff --git a/src/builders/internal/get_app_join_function.ts b/src/builders/internal/get_app_join_function.ts new file mode 100644 index 0000000..8901ccc --- /dev/null +++ b/src/builders/internal/get_app_join_function.ts @@ -0,0 +1,51 @@ +import { Singleton } from "tstl/thread/Singleton"; +import { Has } from "../../decorators"; +import { Belongs } from "../../decorators/Belongs"; + +import { app_join_belongs_many_to_one } from "./app_join_belongs_many_to_one"; +import { app_join_belongs_one_to_one } from "./app_join_belongs_one_to_one"; +import { app_join_has_many_to_many } from "./app_join_has_many_to_many"; +import { app_join_has_one_to_many } from "./app_join_has_one_to_many"; +import { app_join_has_one_to_one } from "./app_join_has_one_to_one"; + +/** + * @internal + */ +export function get_app_join_function(type: Type) +{ + return singleton.get()[type]; +} + +/** + * @internal + */ +type Type + = Deduct> + | Deduct> + | Deduct> + | Deduct> + | Deduct> + | Deduct> + | Deduct> + | Deduct> + | Deduct>; + +/** + * @internal + */ +type Deduct = T extends { type: infer Type } ? Type : never; + +/** + * @internal + */ +const singleton = new Singleton(() => ({ + "Belongs.ManyToOne": app_join_belongs_many_to_one, + "Belongs.External.ManyToOne": app_join_belongs_many_to_one, + "Belongs.OneToOne": app_join_belongs_one_to_one, + "Belongs.External.OneToOne": app_join_belongs_one_to_one, + "Has.OneToOne": app_join_has_one_to_one, + "Has.External.OneToOne": app_join_has_one_to_one, + "Has.OneToMany": app_join_has_one_to_many, + "Has.External.OneToMany": app_join_has_one_to_many, + "Has.ManyToMany": app_join_has_many_to_many +})); \ No newline at end of file diff --git a/src/functional/appJoin.ts b/src/functional/appJoin.ts new file mode 100644 index 0000000..09748c3 --- /dev/null +++ b/src/functional/appJoin.ts @@ -0,0 +1,73 @@ +import { OutOfRange } from "tstl/exception/OutOfRange"; + +import { Has } from "../decorators/Has"; +import { ReflectAdaptor } from "../decorators/base/ReflectAdaptor"; + +import { Creator } from "../typings/Creator"; +import { Relationship } from "../typings/Relationship"; +import { SpecialFields } from "../typings/SpecialFields"; + +import { get_app_join_function } from "../builders/internal/get_app_join_function"; + +export function appJoin< + Mine extends object, + Target extends object, + Accessor extends SpecialFields>> + ( + mine: Mine[], + accessor: Accessor + ): Promise; + +export function appJoin< + Mine extends object, + Target extends object, + Accessor extends SpecialFields>> + ( + mine: Mine[], + accessor: Accessor, + target: Target[] + ): Promise; + +export function appJoin< + Mine extends object, + Target extends object, + Router extends object, + Accessor extends SpecialFields>> + ( + mine: Mine[], + accessor: Accessor, + target: Target[], + routers: Router[] + ): Promise; + +export async function appJoin< + Mine extends object, + Target extends object, + Accessor extends SpecialFields>> + ( + mine: Mine[], + accessor: Accessor, + ...args: any[] + ): Promise +{ + // NO DATA + if (mine.length === 0) + return []; + + // GET METADATA + const creator: Creator = mine[0].constructor as Creator; + const metadata: ReflectAdaptor.Metadata> | undefined = ReflectAdaptor.get(creator.prototype, accessor); + if (metadata === undefined) + throw new OutOfRange(`Error on safe.appJoin(): unable to find the matched relationship accessor "${accessor}" from the "${creator.name}" class.`); + + // CALL THE APP JOIN FUNCTION + const func: Function = get_app_join_function(metadata.type); + return func + ( + creator, + metadata, + mine, + accessor, + ...args + ); +} \ No newline at end of file diff --git a/src/functional/index.ts b/src/functional/index.ts index 12ecc8d..2d9ff6b 100644 --- a/src/functional/index.ts +++ b/src/functional/index.ts @@ -1,3 +1,4 @@ +export * from "./appJoin"; export * from "./createAppJoinBuilder"; export * from "./createJoinQueryBuilder"; export * from "./createJsonSelectBuilder"; diff --git a/src/test/features/test_app_join.ts b/src/test/features/test_app_join.ts new file mode 100644 index 0000000..2a0ef11 --- /dev/null +++ b/src/test/features/test_app_join.ts @@ -0,0 +1,100 @@ +import safe from "../.."; +import { Relationship } from "../../typings"; +import { generate_random_clean_groups } from "../internal/generators/generate_random_clean_groups"; +import { must_not_query_anything } from "../internal/procedures/must_not_query_anything"; +import { AttachmentFile } from "../models/bbs/AttachmentFile"; +import { BbsArticle } from "../models/bbs/BbsArticle"; +import { BbsArticleContent } from "../models/bbs/BbsArticleContent"; +import { BbsArticleTag } from "../models/bbs/BbsArticleTag"; +import { BbsComment } from "../models/bbs/BbsComment"; +import { BbsGroup } from "../models/bbs/BbsGroup"; + +function reload + (records: Mine[]): Promise +{ + return safe + .findRepository(records[0].constructor as safe.typings.Creator) + .createQueryBuilder() + .andWhereInIds(records.map(record => record.id)) + .getMany(); +} + +async function test< + Mine extends safe.Model + & { id: string } + & { [key in Field]: Relationship }, + Target extends object, + Field extends safe.typings.SpecialFields>> + ( + records: Mine[], + field: Field, + targets: Target[], + ): Promise +{ + if (records.length === 0) + return; + + const reloaded: Mine[] = await reload(records); + await safe.appJoin(reloaded, field); + await must_not_query_anything + ( + `appJoin(${records[0].constructor.name}, "${field}")`, + async () => + { + for (const elem of reloaded) + await elem[field].get(); + } + ); + + const retry: Mine[] = await reload(records); + await safe.appJoin(retry, field, targets); + await must_not_query_anything + ( + `appJoin(${records[0].constructor.name}, "${field}", targets)`, + async () => + { + for (const elem of retry) + await elem[field].get(); + } + ); +} + +export async function test_app_join(): Promise +{ + const groupList: BbsGroup[] = await generate_random_clean_groups(); + const articleList: BbsArticle[] = []; + const tagList: BbsArticleTag[] = []; + const contentList: BbsArticleContent[] = []; + const commentList: BbsComment[] = []; + const contentFileList: AttachmentFile[] = []; + const commentFileList: AttachmentFile[] = []; + + for (const group of groupList) + for (const article of await group.articles.get()) + { + articleList.push(article); + for (const content of await article.contents.get()) + { + contentList.push(content); + contentFileList.push(...await content.files.get()); + } + for (const comment of await article.comments.get()) + { + commentList.push(comment); + commentFileList.push(...await comment.files.get()); + } + tagList.push(...await article.tags.get()); + } + + await test(groupList, "articles", articleList); + await test(articleList, "contents", contentList); + await test(articleList, "comments", commentList); + await test(articleList, "tags", tagList); + await test(contentList, "files", contentFileList); + await test(commentList, "files", commentFileList); + + await test(tagList, "article", articleList); + await test(contentList, "article", articleList); + await test(commentList, "article", articleList); + await test(articleList, "group", groupList); +} \ No newline at end of file diff --git a/src/test/features/test_json_select_builder.ts b/src/test/features/test_json_select_builder.ts index 860203c..f62079f 100644 --- a/src/test/features/test_json_select_builder.ts +++ b/src/test/features/test_json_select_builder.ts @@ -15,22 +15,21 @@ import { IBbsGroup } from "../structures/IBbsGroup"; export async function test_json_select_builder(): Promise { - const builder = BbsGroup.createJsonSelectBuilder - ({ - articles: BbsArticle.createJsonSelectBuilder - ({ + const builder = new safe.JsonSelectBuilder(BbsGroup, + { + articles: new safe.JsonSelectBuilder(BbsArticle, + { group: safe.DEFAULT, - category: BbsCategory.createJsonSelectBuilder - ({ + category: new safe.JsonSelectBuilder(BbsCategory, + { parent: "recursive" as const, }), - tags: BbsArticleTag.createJsonSelectBuilder - ( + tags: new safe.JsonSelectBuilder(BbsArticleTag, {}, tag => tag.value // OUTPUT CONVERSION BY MAPPING ), - contents: BbsArticleContent.createJsonSelectBuilder - ({ + contents: new safe.JsonSelectBuilder(BbsArticleContent, + { files: "join" as const }), }) diff --git a/src/typings/Creator.ts b/src/typings/Creator.ts index 854645a..8702ed7 100644 --- a/src/typings/Creator.ts +++ b/src/typings/Creator.ts @@ -4,7 +4,7 @@ * @template T The target class type. * @author Jeongho Nam - https://github.com/samchon */ -export type Creator = +export type Creator = { new(...args: any[]): T; };