/* eslint-disable @typescript-eslint/restrict-template-expressions */ /* eslint-disable @typescript-eslint/no-unsafe-return */ import internal from "stream"; import { plainToClass } from "class-transformer"; import { Knex } from "knex"; import { TransactionScope } from "./Model.connection"; import { EntityColumnOptions, EntityColumnType } from "./Model.entity"; import { EntityRelationColumnOptions, EntityRelationType } from "./Model.entity.relation"; import { CountOptions, DeleteOptions, FindChainingConditions, FindConditions, FindOneOptions, FindOptions, InsertId, OrderCondition, Paginatable, PaginationOptions, StreamFunctions, UpdateBulkOptions, UpdateOptions, UpsertOptions, } from "./Model.query"; import { Metadata } from "../core/Decorator"; import { TraceableError } from "../core/Error"; import { ClassType } from "../types"; import { hasOwnProperty } from "../util/builtin"; const joinSeparator = "_"; interface RelationalRepositoryInfo { options: EntityRelationColumnOptions; repository: RepositoryInfo; } export interface RepositoryInfo { target: ClassType; tableName: string; columns: Array; fields: { [key: string | symbol]: EntityColumnOptions }; primaryColumns: Array; criteriaColumns: Array; oneToOneRelationColumns: Array; oneToOneRelations: { [key: string | symbol]: RelationalRepositoryInfo }; } export const symRepositoryInfo = Symbol(); interface HasSymRepositoryInfo { [symRepositoryInfo]?: RepositoryInfo; } export class Repository> { private readonly repositoryInfo: RepositoryInfo; private readonly kx: Knex; constructor(scopeOrKnex: TransactionScope | Knex, entity: ClassType) { this.repositoryInfo = Repository.getRepositoryInfo(entity); this.kx = scopeOrKnex instanceof TransactionScope ? scopeOrKnex.kx : scopeOrKnex; } static getRepositoryInfo(entity: ClassType & HasSymRepositoryInfo): RepositoryInfo { if (entity[symRepositoryInfo]) return entity[symRepositoryInfo]; const info: RepositoryInfo = {} as any; // To preventing infinite recursive initialization. entity[symRepositoryInfo] = info; const metadata = Metadata.getStorage().entities.find(e => e.target === entity); const columns = Metadata.getStorage().entityColumns.filter(e => e.target === entity); const relations = Metadata.getStorage().entityRelations.filter(e => e.target === entity); if (!metadata) { throw new Error(`Invalid Repository: No Decorated Entity (${entity.name})`); } else if (!columns.length) { throw new Error(`Invalid Repository: No Decorated Columns (${entity.name})`); } (info.target = metadata.target), (info.tableName = metadata.options?.name), (info.columns = columns.map(e => e.propertyKey)), (info.fields = columns.reduce>((p, e) => { p[e.propertyKey] = e.options; return p; }, {})), (info.primaryColumns = columns.filter(e => e.type === EntityColumnType.Primary).map(e => e.propertyKey)), (info.criteriaColumns = columns.filter(e => e.type === EntityColumnType.Primary).map(e => e.propertyKey)), (info.oneToOneRelationColumns = relations.filter(e => e.type === EntityRelationType.OneToOne).map(e => e.propertyKey)), (info.oneToOneRelations = relations .filter(e => e.type === EntityRelationType.OneToOne) .reduce }>>((output, relation) => { output[relation.propertyKey] = { options: { ...relation.options, name: Array.isArray(relation.options.name) ? relation.options.name : [relation.options.name], }, repository: Repository.getRepositoryInfo(relation.options.target), }; return output; }, {})); return info; } async save(entity: T, trx?: TransactionScope): Promise { try { const kx = trx?.kx || this.kx; const [res] = await kx .insert( this.repositoryInfo.columns.reduce>((output, columnName) => { const [key, val] = (() => { const column = this.repositoryInfo.fields[columnName]; if ("name" in column) { const key = column.name; const val = ((v): any => { if (typeof v === "function") return kx.raw(v(key)); else if (v === undefined && column && "default" in column && column.default) { return kx.raw(column.default(key)); } else return v; })(entity[columnName]); return [key, val]; } else { if (hasOwnProperty(entity, columnName)) { throw new Error(`Invalid Usage: Save with raw column not allowed. (${column.raw(this.repositoryInfo.tableName)})`); } else { return [null, null]; } } })(); if (key === null) return output; output[key] = val; return output; }, {}) ) .into(this.repositoryInfo.tableName); if (this.repositoryInfo.primaryColumns.length === 1) { const id = entity[this.repositoryInfo.primaryColumns[0]]; if (id && typeof id !== "function") { return entity[this.repositoryInfo.primaryColumns[0]]; } } const [[lid]] = await kx.raw("SELECT LAST_INSERT_ID() AS seq"); return res || lid.seq; } catch (err) { throw TraceableError(err as Error); } } async saveBulk(entities: Array, trx?: TransactionScope): Promise { try { const kx = trx?.kx || this.kx; if (this.repositoryInfo.primaryColumns.length !== 1) { throw new Error("Not Supprted: SaveBulk with multiple primary columns. "); } else if (entities.find(e => !e[this.repositoryInfo.primaryColumns[0]])) { throw new Error("Not Supprted: SaveBulk with empty primary column value. "); } const insertedIds: InsertId[] = []; const newEntities = entities.map(entity => { return this.repositoryInfo.columns.reduce>((output, column) => { const field = this.repositoryInfo.fields[column]; const isPrimaryColumn = this.repositoryInfo.primaryColumns[0] === column; if (!field) { throw new Error(`Invalid Usage: Save with column(${String(column)}) not allowed. `); } const [key, val] = (() => { if (!field) { return [null, null]; } else if ("name" in field) { const key = field.name; const val = ((v): any => { if (typeof v === "function") return kx.raw(v(key)); else if (v === undefined && field.default) { return kx.raw(field.default(key)); } else return v; })(entity[column]); return [key, val]; } else { if (hasOwnProperty(entity, column)) { throw new Error(`Invalid Usage: Save with raw column not allowed. (${field.raw(this.repositoryInfo.tableName)})`); } else { return [null, null]; } } })(); if (key === null) return output; if (isPrimaryColumn) { insertedIds.push(val); } output[key] = val; return output; }, {}); }); await kx.insert(newEntities).into(this.repositoryInfo.tableName); return insertedIds; } catch (err) { throw TraceableError(err as Error); } } async update(entity: T, options?: UpdateOptions, trx?: TransactionScope): Promise { try { let kx = (trx?.kx || this.kx).from(this.repositoryInfo.tableName); const conditions: FindConditions = Object.assign({}, options?.where || {}); this.repositoryInfo.primaryColumns.forEach(e => { (conditions as any)[e] = entity[e]; }); kx = this.where(kx, conditions); kx = kx.update( (options?.update || this.repositoryInfo.columns).reduce>((output, columnName) => { const column = this.repositoryInfo.fields[columnName]; if ("name" in column) { output[column.name] = entity[columnName]; } else { throw new Error(`Invalid Usage: Update with raw column not allowed. (${column.raw(this.repositoryInfo.tableName)})`); } return output; }, {}) ); if (options?.debug) { // eslint-disable-next-line no-console console.log(">", kx.toSQL()); } const affected = await kx; return Number(affected); } catch (err) { throw TraceableError(err as Error); } } async updateBulk(entities: Array, options: UpdateBulkOptions, trx?: TransactionScope): Promise { try { const kx = trx?.kx || this.kx; const primaryColumn = this.repositoryInfo.primaryColumns[0]; const primaryField = this.repositoryInfo.fields[primaryColumn]; if (this.repositoryInfo.primaryColumns.length !== 1) { throw new Error("Not Supprted: updateBulk with multiple primary columns. "); } else if (entities.find(e => !e[primaryColumn])) { throw new Error("Not Supprted: updateBulk with empty primary column value. "); } else if (!options.update.length) { return 0; } const primaryFieldName = primaryField && "name" in primaryField ? primaryField.name : null; const updateFieldNames = (options.update as string[]) .map(e => { const field = this.repositoryInfo.fields[e]; return field && "name" in field ? field.name : null; }) .filter(e => e); if (!primaryFieldName) { throw new Error(`Invalid Usage: UpdateBulk with primary column(${String(primaryColumn)}) not allowed.`); } else if (updateFieldNames.length !== options.update.length) { throw new Error(`Invalid Usage: UpdateBulk with column(${options.update.join(", ")}) not allowed.`); } const entityIds = entities.map(e => e[primaryColumn]); const findWhere: FindConditions = {}; (findWhere as any)[primaryColumn] = entityIds; const ids = (await this.select({ where: findWhere, forUpdate: true, select: [primaryColumn as keyof T] }, trx)).map( e => e[primaryColumn] ); const filteredEntities = entities.filter(e => ids.includes(e[primaryColumn])); const updatedEntities = filteredEntities.map(entity => { return this.repositoryInfo.columns.reduce>((p, column) => { const field = this.repositoryInfo.fields[column]; if (!field) { throw new Error(`Invalid Usage: UpdateBulk with raw column(${String(column)}) not allowed.`); } else if ("name" in field) { if (entity[column] === undefined) { throw new Error(`Invalid Usage: UpdateBulk with raw column(${String(column)}) is undefined.`); } p[field.name] = entity[column]; } else { throw new Error(`Invalid Usage: UpdateBulk with raw column not allowed. (${field.raw(this.repositoryInfo.tableName)})`); } return p; }, {}); }); await kx.insert(updatedEntities).into(this.repositoryInfo.tableName).onConflict(primaryFieldName).merge(updateFieldNames); return updatedEntities.length; } catch (err) { throw TraceableError(err as Error); } } async upsert(entity: T, options: UpsertOptions, trx?: TransactionScope): Promise { try { const kx = trx?.kx || this.kx; const primaryColumn = this.repositoryInfo.primaryColumns[0]; const newEntity = this.repositoryInfo.columns.reduce((p, column) => { const field = this.repositoryInfo.fields[column]; const [key, val] = (() => { if (!field) { return [null, null]; } else if ("name" in field) { const key = field.name; const val = ((v): any => { if (typeof v === "function") return kx.raw(v(key)); else if (v === undefined && field.default) { return kx.raw(field.default(key)); } else return v; })(entity[column]); return [key, val]; } else { if (hasOwnProperty(entity, column)) { throw new Error(`Invalid Usage: Save with raw column not allowed. (${field.raw(this.repositoryInfo.tableName)})`); } else { return [null, null]; } } })(); if (key === null) return p; p[key] = val; return p; }, {} as Record); const updateFieldNames = (() => { const getFieldName = (fieldKey: string): string | null => { const field = this.repositoryInfo.fields[fieldKey]; return field && "name" in field ? field.name : null; }; return (options.update as string[]).map(getFieldName).filter((name): name is string => name !== null); })(); const kxQuery = kx.insert(newEntity).into(this.repositoryInfo.tableName).onConflict().merge(updateFieldNames); const [res] = await kxQuery; if (options?.debug) { // eslint-disable-next-line no-console console.log(">", kxQuery.toSQL()); } if (this.repositoryInfo.primaryColumns.length === 1) { const id = entity[primaryColumn]; if (id && typeof id !== "function") { return entity[primaryColumn]; } } const [[lid]] = await kx.raw("SELECT LAST_INSERT_ID() AS seq"); return res || lid.seq; } catch (err) { throw TraceableError(err as Error); } } async delete(entity: T, options?: DeleteOptions, trx?: TransactionScope): Promise { try { let kx = (trx?.kx || this.kx).from(this.repositoryInfo.tableName); const conditions: FindConditions = Object.assign({}, options?.where || {}); this.repositoryInfo.primaryColumns.forEach(e => { (conditions as any)[e] = entity[e]; }); kx = this.where(kx, conditions); kx = kx.del(); if (options?.debug) { // eslint-disable-next-line no-console console.log(">", kx.toSQL()); } const affected = await kx; return Number(affected); } catch (err) { throw TraceableError(err as Error); } } async findOne(options?: FindOneOptions, trx?: TransactionScope): Promise { try { const [res] = await this.select({ ...options, limit: 1 }, trx); return res || null; } catch (err) { throw TraceableError(err as Error); } } async find(options?: FindOptions, trx?: TransactionScope): Promise { try { const res = await this.select(options, trx); return res; } catch (err) { throw TraceableError(err as Error); } } async pagination(options?: PaginationOptions, trx?: TransactionScope): Promise> { try { const kx = trx?.kx || this.kx; const page = Math.max(1, options && options.page ? options.page : 1); const rpp = Math.max(1, options && options.rpp ? options.rpp : 30); const limit = BigInt(rpp); const offset: bigint = (BigInt(page) - BigInt(1)) * limit; const count = (await this.where(kx.from(this.repositoryInfo.tableName), options?.where || {}).count("* as cnt"))[0].cnt; const items = await this.find({ ...options, limit, offset }, trx); return { page, rpp, count, items, }; } catch (err) { throw TraceableError(err as Error); } } streaming(options: FindOptions, trx?: TransactionScope): internal.PassThrough & AsyncIterable { try { return this.prepareQuery(options, trx).stream(); } catch (err) { throw TraceableError(err as Error); } } async streamAsync(options: FindOptions, streamFn: StreamFunctions, trx?: TransactionScope): Promise { try { const kx = this.prepareQuery(options, trx); kx.stream(stream => { stream.on("data", row => streamFn.onData(this.mapping(row))); if (streamFn.onStreamEnd) { stream.on("end", () => streamFn.onStreamEnd?.()); } }); } catch (err) { throw TraceableError(err as Error); } } private prepareQuery(options?: FindOptions, trx?: TransactionScope): Knex.QueryBuilder { try { const knex = trx?.kx || this.kx; const joinAliases: { [key: string]: string } = {}; let kx = knex.from(this.repositoryInfo.tableName); const selectColumns: any[] = options?.select || this.repositoryInfo.columns; const select = selectColumns .filter(x => this.repositoryInfo.columns.indexOf(x) !== -1) .map(alias => { const column = this.repositoryInfo.fields[alias]; if (hasOwnProperty(column, "name")) { return `${this.repositoryInfo.tableName}.${column.name} as ${alias}`; } else { kx.select(knex.raw(`${column.raw(this.repositoryInfo.tableName)} as ${alias}`)); } }) .filter(x => x) .map(e => e!); kx = kx.select(select); if (typeof options?.distinct === "boolean" && options.distinct) { kx.distinct(select); } if (options?.forUpdate) { kx = kx.forUpdate(); } // Join kx = this.join(kx, options?.select, joinAliases); // Query if (options?.where) { kx = this.where(kx, options.where); } const joinAliasesProp = Object.keys(joinAliases).reduce>((output, joinAliasKey) => { output[`.${joinAliasKey.split(joinSeparator).join(".")}`] = joinAliases[joinAliasKey]; return output; }, {}); // Sort if (options?.order) { const orderBy = Array.isArray(options.order) ? options.order : [options.order]; for (let i = 0; i < orderBy.length; i++) { kx = this.order(kx, orderBy[i], joinAliasesProp); } } // Pagination if (options?.offset) kx = kx.offset(String(options.offset) as any); if (options?.limit) kx = kx.limit(String(options.limit) as any); if (options?.debug) { // eslint-disable-next-line no-console console.log(">", kx.toSQL()); } return kx; } catch (err) { throw TraceableError(err as Error); } } private async select(options?: FindOptions, trx?: TransactionScope): Promise { try { const rows = await this.prepareQuery(options, trx); if (!rows || !rows.length) return []; return rows.map((row: any) => this.mapping(row)); } catch (err) { throw TraceableError(err as Error); } } async count(options: CountOptions, trx?: TransactionScope): Promise { try { let kx = (trx?.kx || this.kx).count("* AS cnt").from(this.repositoryInfo.tableName); kx = this.join(kx, [], {}); // Query if (options.where) { kx = this.where(kx, options.where); } if (options.debug) { // eslint-disable-next-line no-console console.log(">", kx.toSQL()); } const res = await kx; if (!res || !res.length) return BigInt(0); return BigInt(res[0].cnt || 0); } catch (err) { throw TraceableError(err as Error); } } private joinWith( kx: any, rec: number, fromTable: string, propertyKey: string | symbol, to: RelationalRepositoryInfo, aliases?: { [key: string | symbol]: string } ) { const kxx = kx; const fromColumns = to.options.name; const toFields = to.repository.fields; const toColumns = to.repository.primaryColumns; const toTableNameAlias = `${to.repository.tableName}_${rec}`; const toTable = `${to.repository.tableName} AS ${toTableNameAlias}`; const joinTableColumns = to.repository.columns.map(col => { const column = to.repository.fields[col]; if (hasOwnProperty(column, "name")) { return `${toTableNameAlias}.${column.name} as ${String(propertyKey)}${joinSeparator}${String(col)}`; } else { throw new Error(`Invalid Usage: Join with raw column not allowed. (${column.raw(toTableNameAlias)})`); } }); // Assign alias if (aliases) aliases[propertyKey] = toTableNameAlias; if (fromColumns.length !== toColumns.length) { throw new Error( `Invalid Relation: Joining columns are not matched (${(fromColumns as string[]).join(",")} -> ${toColumns.join(",")})` ); } kxx.leftOuterJoin(toTable, function (this: any) { for (let i = 0; i < fromColumns.length; i++) { const column = toFields[toColumns[i]]; if ("name" in column) { this.on(`${fromTable}.${fromColumns[i]}`, `${toTableNameAlias}.${column.name}`); } else { throw new Error(`Invalid Usage: Join with raw column not allowed. (${column.raw(fromTable)})`); } } }); kxx.select(joinTableColumns); let idx = 0; to.repository.oneToOneRelationColumns.forEach(relationColumn => { this.joinWith( kxx, rec * 10 + idx++, toTableNameAlias, `${String(propertyKey)}${joinSeparator}${String(relationColumn)}`, to.repository.oneToOneRelations[relationColumn], aliases ); }); return kxx; } private join(kx: any, selectColumns?: Array, aliases?: { [key: string]: string }): any { const kxx = kx; let idx = 0; this.repositoryInfo.oneToOneRelationColumns.forEach(relationColumn => { if (!selectColumns || selectColumns.indexOf(relationColumn as keyof T) !== -1) { this.joinWith( kxx, ++idx, this.repositoryInfo.tableName, relationColumn, this.repositoryInfo.oneToOneRelations[relationColumn], aliases ); } }); return kxx; } private where(kx: any, where?: FindConditions | FindChainingConditions, orWhere?: boolean): any { /* eslint-disable no-prototype-builtins, @typescript-eslint/no-this-alias */ let kxx = kx; if (!where) return kxx; Object.keys(where).forEach(ke => { if (ke === "$AND" || ke === "$OR") { const that = this; if (Array.isArray(where?.[ke])) { if (ke === "$AND") { (where[ke] as any).forEach((chWhere: any) => { kxx = kx.andWhere(function (this: any) { that.where(this, chWhere, false); }); }); } else if (ke === "$OR") { (where[ke] as any).forEach((chWhere: any) => { kxx = kx.orWhere(function (this: any) { that.where(this, chWhere, true); }); }); } } else { if (ke === "$AND") { kx.andWhere(function (this: any) { that.where(this, where?.[ke] as any, false); }); } else if (ke === "$OR") { kx.orWhere(function (this: any) { that.where(this, where?.[ke] as any, true); }); } } } else { const column = this.repositoryInfo.fields[ke]; const isConditional = (v: any) => typeof v === "object" && (v["$AND"] || v["$OR"]); const isManipulatable = (v: any) => typeof v === "object" && (v.hasOwnProperty(">=") || v.hasOwnProperty(">") || v.hasOwnProperty("<=") || v.hasOwnProperty("<") || v.hasOwnProperty("!=") || v.hasOwnProperty("LIKE") || v.hasOwnProperty("LIKE%") || v.hasOwnProperty("%LIKE") || v.hasOwnProperty("%LIKE%") || v.hasOwnProperty("NOT_IN") || typeof v["IS_NULL"] === "boolean" || typeof v["IS_NOT_NULL"] === "boolean"); const raw = !!hasOwnProperty(column, "raw"); const k = hasOwnProperty(column, "name") ? `${this.repositoryInfo.tableName}.${column.name}` : column.raw(this.repositoryInfo.tableName); const v = (where as any)?.[ke]; if (Array.isArray(v)) { if (!raw) { kxx.whereIn(k, v); } else { if (v.length > 0) kxx.whereRaw(`${k} IN (?)`, [v]); else kxx.whereRaw("false"); } } else if (isManipulatable(v) || isConditional(v)) { const that = this; kxx[orWhere ? "orWhere" : "andWhere"](function (this: any) { Object.keys(v).forEach(cond => { if (cond === "$AND" || cond === "$OR") { // eslint-disable-next-line prefer-arrow-callback this[cond === "$OR" ? "orWhere" : "andWhere"](function (this: any) { v[cond].forEach((vv: any) => { that.where(this, { [ke]: vv }, cond === "$OR"); }); }); } else if (cond === "IS_NULL" || cond === "IS_NOT_NULL") { if (v["IS_NULL"] === true || v["IS_NOT_NULL"] === false) { this[orWhere ? "orWhereNull" : "whereNull"](k); } else if (v["IS_NULL"] === false || v["IS_NOT_NULL"] === true) { this[orWhere ? "orWhereNotNull" : "whereNotNull"](k); } } else if (cond === "LIKE" || cond === "%LIKE" || cond === "LIKE%" || cond === "%LIKE%") { if (cond === "LIKE") this[orWhere ? "orWhere" : "where"](k, "LIKE", v[cond]); else if (cond === "%LIKE") this[orWhere ? "orWhere" : "where"](k, "LIKE", `%${v[cond]}`); else if (cond === "LIKE%") this[orWhere ? "orWhere" : "where"](k, "LIKE", `${v[cond]}%`); else if (cond === "%LIKE%") this[orWhere ? "orWhere" : "where"](k, "LIKE", `%${v[cond]}%`); } else if (cond === "NOT_IN") { this[orWhere ? "orWhereNotIn" : "whereNotIn"](k, v[cond]); } else if (typeof v[cond] === "function") { const res = v[cond](k); if (typeof res === "object") { this.whereRaw(`${k} ${cond} ${res.operand}`, res.bindings); } else { this.whereRaw(`${k} ${cond} ${res}`); } } else { this[orWhere ? "orWhereRaw" : "whereRaw"](`${k} ${cond} ?`, [v[cond]]); } }); }); } else if (typeof v === "function") { const result = v(k); if (result.hasOwnProperty("query")) { kxx.whereRaw(result.query, result.bindings); } else { kxx[orWhere ? "orWhere" : "where"](this.kx.raw(result)); } } else { if (!raw) { kxx[orWhere ? "orWhere" : "where"](k, v); } else { kxx[orWhere ? "orWhere" : "andWhere"](function (this: any) { this.whereRaw(`${k} = ?`, [v]); }); } } /* else { const k = column.raw(this.repositoryInfo.tableName); const v = where[ke]; if (Array.isArray(v)) { kxx[orWhere ? "orWhere" : "andWhere"](function () { if (v.length > 0) this.whereRaw(`${k} IN (?)`, [v]); else this.whereRaw("false"); }); } else if (isConditional(v)) { throw new Error(`Invalid Usage: Query with raw column not supported. (${column.raw(this.repositoryInfo.tableName)})`); } else { // const that = this; kxx[orWhere ? "orWhere" : "andWhere"](function () { this.whereRaw(`${k} = ?`, [v]); }); } } */ } }); return kxx; } private order(kx: any, orderCond: OrderCondition, joinAliases: { [key: string]: string }): any { const order = typeof orderCond === "function" ? [orderCond] : orderCond; let kxx = kx; Object.keys(order).forEach(ke => { const column = this.repositoryInfo.fields[ke]; const oto = !column && this.repositoryInfo.oneToOneRelations[ke]; const v = (order as any)[ke]; if (oto) { throw new Error("Invalid Usage: Sorting by joining column must be used delegated function.)"); } else if (!column && typeof v === "function") { kxx = kx.orderByRaw(v(joinAliases)); } else if (hasOwnProperty(column, "name")) { const k = `${this.repositoryInfo.tableName}.${column.name}`; if (typeof v === "function") { kxx = kx.orderByRaw(v(k)); } else { kxx = kx.orderBy(k, v); } } else { const k = column.raw(this.repositoryInfo.tableName); kxx = kx.orderByRaw(`${k} ${v}`); } }); return kxx; } private mapping(row: any, repositoryInfo?: RepositoryInfo, prefix?: string): T { const x = plainToClass( (repositoryInfo || this.repositoryInfo).target, Object.keys(row) .filter(e => !prefix || e.startsWith(`${prefix}${joinSeparator}`)) .reduce>((output, c) => { const col = !prefix ? c : c.substring(prefix.length + 1); if (!col.includes(joinSeparator)) { output[col] = row[c]; } else { const [join] = col.split(joinSeparator); if (!output[join]) { output[join] = this.mapping( row, (repositoryInfo || this.repositoryInfo).oneToOneRelations[join].repository, !prefix ? join : `${prefix}_${join}` ); } } return output; }, {}) ); return x; } }