refactor(typeorm): drop the string-array find-option patch; convert in app code (#9793)

Removes patches/typeorm+1.0.0.patch and replaces its runtime shim + type-widening with equivalent conversion (parseFindOptionsRelations/Select, LegacyFind*Options) applied at the TypeORM data-access boundary. MikroORM path unchanged. Parser hardened against prototype pollution + empty segments. Verified: core tsc clean, full worker prod build green vs stock typeorm@1.0.0, 22 unit tests pass, worker runs BullMQ/scheduler at runtime. All code-quality/security/AI-review checks green; CircleCI build-monorepo-root passed (build-api/build-web are approval-gated).
This commit is contained in:
Ruslan Konviser
2026-07-09 17:18:46 +02:00
committed by GitHub
parent 4b406c9d6e
commit e7523d1ad7
47 changed files with 539 additions and 238 deletions
@@ -14,6 +14,7 @@ import { isNotNullOrUndefined } from '@gauzy/utils';
import { isBetterSqlite3, isSqlite } from '@gauzy/config';
import { TenantAwareCrudService } from './../core/crud';
import { RequestContext } from '../core/context';
import { parseFindOptionsRelations } from '../core/utils';
import {
activityLogUpdatedFieldsAndValues,
generateActivityLogDescription,
@@ -120,7 +121,7 @@ export class ActivityLogService extends TenantAwareCrudService<ActivityLog> {
// Ensure that filters are properly defined
const queryOptions: FindManyOptions<ActivityLog> = {
where,
...(relations && { relations }),
...(relations && { relations: parseFindOptionsRelations(relations) }),
take: take,
skip: take * (skip - 1) // Calculate offset (skip) based on validated skip value
};
@@ -2250,7 +2250,6 @@ export class AuthService extends SocialAuthService {
break;
}
case MultiORMEnum.TypeORM: {
// TODO(typeorm-v1): `relations` no longer accepts a string array. This value references a variable whose shape can't be determined statically — if it holds `string[]`, wrap it: `Object.fromEntries(<expr>?.map(r => [r, true]) ?? [])` (dot-paths need extra nesting handling). If it already holds the v1 object shape, no change needed.
user = await this.typeOrmUserRepository.findOne({ where, relations });
break;
}
@@ -2277,7 +2276,6 @@ export class AuthService extends SocialAuthService {
break;
}
case MultiORMEnum.TypeORM: {
// TODO(typeorm-v1): `relations` no longer accepts a string array. This value references a variable whose shape can't be determined statically — if it holds `string[]`, wrap it: `Object.fromEntries(<expr>?.map(r => [r, true]) ?? [])` (dot-paths need extra nesting handling). If it already holds the v1 object shape, no change needed.
employee = await this.typeOrmEmployeeRepository.findOne({ where: employeeWhere, relations: employeeRelations });
break;
}
@@ -2389,7 +2387,6 @@ export class AuthService extends SocialAuthService {
break;
}
case MultiORMEnum.TypeORM: {
// TODO(typeorm-v1): `relations` no longer accepts a string array. This value references a variable whose shape can't be determined statically — if it holds `string[]`, wrap it: `Object.fromEntries(<expr>?.map(r => [r, true]) ?? [])` (dot-paths need extra nesting handling). If it already holds the v1 object shape, no change needed.
user = await this.typeOrmUserRepository.findOne({ where, relations });
break;
}
@@ -18,6 +18,7 @@ import {
} from '@gauzy/contracts';
import { BaseQueryDTO, TenantAwareCrudService } from '../core/crud';
import { RequestContext } from '../core/context';
import { parseFindOptionsRelations } from '../core/utils';
import { EmployeeService } from '../employee/employee.service';
import { ActivityLogService } from '../activity-log/activity-log.service';
import { RoleService } from '../role/role.service';
@@ -164,7 +165,9 @@ export class BroadcastService extends TenantAwareCrudService<Broadcast> {
// Extract filters from where clause
const whereClause = filters.where ?? {};
const { entity, entityId, category, visibilityMode, isArchived = false } = whereClause;
const relations = Array.isArray(filters.relations) ? (filters.relations as string[]) : [];
// Pass the raw value through the converter, which handles both the legacy string[] form and the
// v1 object form (and undefined) — so object-form relations aren't silently dropped.
const relations = filters.relations;
// Extract pagination options from filters
const { take, skip } = filters;
@@ -184,7 +187,7 @@ export class BroadcastService extends TenantAwareCrudService<Broadcast> {
// Retrieve broadcasts matching base criteria with pagination
const queryOptions: FindManyOptions<Broadcast> = {
where,
relations,
relations: parseFindOptionsRelations(relations),
order: { publishedAt: 'DESC' },
...(take !== undefined && { take }),
...(skip !== undefined && { skip })
@@ -340,7 +343,7 @@ export class BroadcastService extends TenantAwareCrudService<Broadcast> {
organizationId: organizationId || RequestContext.currentOrganizationId()
})
},
relations: [relationName]
relations: parseFindOptionsRelations([relationName])
});
if (!entityWithMembers) {
@@ -522,7 +525,7 @@ export class BroadcastService extends TenantAwareCrudService<Broadcast> {
const entityWithMembers = await repository.findOne({
where: { id: entityId, tenantId, organizationId },
relations: [relationName]
relations: parseFindOptionsRelations([relationName])
});
if (!entityWithMembers) return [];
+43 -22
View File
@@ -22,7 +22,16 @@ import { BaseEntity, SoftDeletableBaseEntity } from '../entities/internal';
import { multiORMCreateQueryBuilder } from '../../core/orm/query-builder/query-builder.factory';
import { IQueryBuilder } from '../../core/orm/query-builder/iquery-builder';
import { MikroOrmBaseEntityRepository } from '../../core/repository/mikro-orm-base-entity.repository';
import { MultiORM, MultiORMEnum, concatIdToWhere, getORMType, parseTypeORMFindToMikroOrm } from './../../core/utils';
import {
MultiORM,
MultiORMEnum,
concatIdToWhere,
getORMType,
parseFindOptionsRelations,
parseFindOptionsSelect,
parseTypeORMFindOptions,
parseTypeORMFindToMikroOrm
} from './../../core/utils';
import { parseTypeORMFindCountOptions } from './utils';
import {
ICountByOptions,
@@ -143,7 +152,9 @@ export abstract class CrudService<T extends BaseEntity> implements ICrudService<
items = items.map((entity: T) => this.serialize(entity)) as T[];
break;
case MultiORMEnum.TypeORM:
[items, total] = await this.typeOrmRepository.findAndCount(options as FindManyOptions<T>);
[items, total] = await this.typeOrmRepository.findAndCount(
parseTypeORMFindOptions(options as FindManyOptions<T>)
);
break;
default:
throw new Error(`Not implemented for ${this.ormType}`);
@@ -165,7 +176,7 @@ export abstract class CrudService<T extends BaseEntity> implements ICrudService<
const items = await this.mikroOrmRepository.find(where, mikroOptions);
return items.map((entity: T) => this.serialize(entity)) as T[];
case MultiORMEnum.TypeORM:
return await this.typeOrmRepository.find(options as FindManyOptions<T>);
return await this.typeOrmRepository.find(parseTypeORMFindOptions(options as FindManyOptions<T>));
default:
throw new Error(`Not implemented for ${this.ormType}`);
}
@@ -179,7 +190,7 @@ export abstract class CrudService<T extends BaseEntity> implements ICrudService<
* @param options
* @returns
*/
public async paginate(options?: FindManyOptions<T>): Promise<IPagination<T>> {
public async paginate(options?: IFindManyOptions<T>): Promise<IPagination<T>> {
try {
let total: number;
let items: T[];
@@ -191,19 +202,23 @@ export abstract class CrudService<T extends BaseEntity> implements ICrudService<
items = items.map((entity) => this.serialize(entity)) as T[];
break;
case MultiORMEnum.TypeORM:
// Normalize legacy string-array `relations`/`select` to object form before hitting TypeORM.
const typeOrmOptions = parseTypeORMFindOptions(options as FindManyOptions<T>);
[items, total] = await this.typeOrmRepository.findAndCount({
skip: options && options.skip ? options.take * (options.skip - 1) : 0,
take: options && options.take ? options.take : 10,
/**
* Specifies what relations should be loaded.
*
* @deprecated
*/
...(options && options.select ? { select: options.select } : {}),
...(options && options.relations ? { relations: options.relations } : {}),
...(options && options.where ? { where: options.where } : {}),
...(options && options.order ? { order: options.order } : {}),
...(options && options.withDeleted ? { withDeleted: options.withDeleted } : {})
skip:
typeOrmOptions && typeOrmOptions.skip
? typeOrmOptions.take * (typeOrmOptions.skip - 1)
: 0,
take: typeOrmOptions && typeOrmOptions.take ? typeOrmOptions.take : 10,
...(typeOrmOptions && typeOrmOptions.select ? { select: typeOrmOptions.select } : {}),
...(typeOrmOptions && typeOrmOptions.relations
? { relations: typeOrmOptions.relations }
: {}),
...(typeOrmOptions && typeOrmOptions.where ? { where: typeOrmOptions.where } : {}),
...(typeOrmOptions && typeOrmOptions.order ? { order: typeOrmOptions.order } : {}),
...(typeOrmOptions && typeOrmOptions.withDeleted
? { withDeleted: typeOrmOptions.withDeleted }
: {})
});
break;
default:
@@ -249,8 +264,10 @@ export abstract class CrudService<T extends BaseEntity> implements ICrudService<
id,
...(options && options.where ? options.where : {})
},
...(options && options.select ? { select: options.select } : {}),
...(options && options.relations ? { relations: options.relations } : []),
...(options && options.select ? { select: parseFindOptionsSelect(options.select) } : {}),
...(options && options.relations
? { relations: parseFindOptionsRelations(options.relations) }
: []),
...(options && options.order ? { order: options.order } : {})
} as FindOneOptions<T>);
break;
@@ -285,7 +302,9 @@ export abstract class CrudService<T extends BaseEntity> implements ICrudService<
record = (await this.mikroOrmRepository.findOneOrFail(where, mikroOptions)) as any;
break;
case MultiORMEnum.TypeORM:
record = await this.typeOrmRepository.findOneOrFail(options as FindOneOptions<T>);
record = await this.typeOrmRepository.findOneOrFail(
parseTypeORMFindOptions(options as FindOneOptions<T>)
);
break;
default:
throw new Error(`Not implemented for ${this.ormType}`);
@@ -363,8 +382,10 @@ export abstract class CrudService<T extends BaseEntity> implements ICrudService<
id,
...(options && options.where ? options.where : {})
},
...(options && options.select ? { select: options.select } : {}),
...(options && options.relations ? { relations: options.relations } : []),
...(options && options.select ? { select: parseFindOptionsSelect(options.select) } : {}),
...(options && options.relations
? { relations: parseFindOptionsRelations(options.relations) }
: []),
...(options && options.order ? { order: options.order } : {}),
...(options && options.withDeleted ? { withDeleted: options.withDeleted } : {})
} as FindOneOptions<T>);
@@ -395,7 +416,7 @@ export abstract class CrudService<T extends BaseEntity> implements ICrudService<
record = (await this.mikroOrmRepository.findOne(where, mikroOptions)) as any;
break;
case MultiORMEnum.TypeORM:
record = await this.typeOrmRepository.findOne(options as FindOneOptions<T>);
record = await this.typeOrmRepository.findOne(parseTypeORMFindOptions(options as FindOneOptions<T>));
break;
default:
throw new Error(`Not implemented for ${this.ormType}`);
@@ -2,9 +2,10 @@
// MIT License, see https://github.com/xmlking/ngx-starter-kit/blob/develop/LICENSE
// Copyright (c) 2018 Sumanth Chinthagunta
import { DeepPartial, DeleteResult, FindManyOptions, FindOneOptions, FindOptionsWhere, UpdateResult } from 'typeorm';
import { DeepPartial, DeleteResult, FindOptionsWhere, UpdateResult } from 'typeorm';
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
import { IPagination, ID } from '@gauzy/contracts';
import { LegacyFindManyOptions, LegacyFindOneOptions } from '../utils';
import { ITryRequest } from './try-request';
import {
FindOptions as MikroFindOptions,
@@ -39,15 +40,15 @@ export interface ICrudService<T> {
export type IMikroOptions<T> = { where?: MikroFilterQuery<T> } & MikroFindOptions<T>;
export type ICountOptions<T> = FindManyOptions<T> | IMikroOptions<T>;
export type ICountOptions<T> = LegacyFindManyOptions<T> | IMikroOptions<T>;
export type ICountByOptions<T> = FindOptionsWhere<T> | MikroFilterQuery<T>;
export type IFindManyOptions<T> = FindManyOptions<T> | IMikroOptions<T>;
export type IFindManyOptions<T> = LegacyFindManyOptions<T> | IMikroOptions<T>;
export type IFindOneOptions<T> = FindOneOptions<T> | IMikroOptions<T>;
export type IFindOneOptions<T> = LegacyFindOneOptions<T> | IMikroOptions<T>;
export type IFindOrFailOptions<T> = FindOneOptions<T> | IMikroOptions<T>;
export type IFindOrFailOptions<T> = LegacyFindOneOptions<T> | IMikroOptions<T>;
export type IFindWhereOptions<T> = FindOptionsWhere<T> | MikroFilterQuery<T>;
@@ -1,8 +1,9 @@
import { NotFoundException } from '@nestjs/common';
import { DeleteResult, FindOptionsWhere, FindManyOptions, FindOneOptions, In, Repository, UpdateResult } from 'typeorm';
import { DeleteResult, FindOptionsWhere, In, Repository, UpdateResult } from 'typeorm';
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
import { ID, IPagination, IUser, PermissionsEnum } from '@gauzy/contracts';
import { isNotEmpty } from '@gauzy/utils';
import { LegacyFindManyOptions, LegacyFindOneOptions } from '../utils';
import { MikroOrmBaseEntityRepository } from '../../core/repository/mikro-orm-base-entity.repository';
import { RequestContext } from '../context';
import { TenantBaseEntity } from '../entities/internal';
@@ -163,7 +164,7 @@ export abstract class TenantAwareCrudService<T extends TenantBaseEntity>
* @param filter - Additional find options.
* @returns The find one options based on the current user's relationship with the tenant and additional options.
*/
private findOneWithTenant(filter?: FindOneOptions<T>): FindOneOptions<T> {
private findOneWithTenant(filter?: LegacyFindOneOptions<T>): LegacyFindOneOptions<T> {
const user = RequestContext.currentUser();
if (!user || !user.tenantId) {
return filter;
@@ -194,7 +195,7 @@ export abstract class TenantAwareCrudService<T extends TenantBaseEntity>
* @param filter - Additional find options.
* @returns The find many options based on the current user's relationship with the tenant and additional options.
*/
private findManyWithTenant(filter?: FindManyOptions<T>): FindManyOptions<T> {
private findManyWithTenant(filter?: LegacyFindManyOptions<T>): LegacyFindManyOptions<T> {
const user = RequestContext.currentUser();
if (!user || !user.tenantId) {
return filter;
@@ -226,7 +227,7 @@ export abstract class TenantAwareCrudService<T extends TenantBaseEntity>
* @param options
* @returns
*/
public async count(options?: FindManyOptions<T>): Promise<number> {
public async count(options?: LegacyFindManyOptions<T>): Promise<number> {
return await super.count(this.findManyWithTenant(options));
}
@@ -253,7 +254,7 @@ export abstract class TenantAwareCrudService<T extends TenantBaseEntity>
* @param filter
* @returns
*/
public async findAll(filter?: FindManyOptions<T>): Promise<IPagination<T>> {
public async findAll(filter?: LegacyFindManyOptions<T>): Promise<IPagination<T>> {
return await super.findAll(this.findManyWithTenant(filter));
}
@@ -263,7 +264,7 @@ export abstract class TenantAwareCrudService<T extends TenantBaseEntity>
* @param filter
* @returns
*/
public async find(filter?: FindManyOptions<T>): Promise<T[]> {
public async find(filter?: LegacyFindManyOptions<T>): Promise<T[]> {
return await super.find(this.findManyWithTenant(filter));
}
@@ -275,7 +276,7 @@ export abstract class TenantAwareCrudService<T extends TenantBaseEntity>
* @param filter
* @returns
*/
public async paginate(filter?: FindManyOptions<T>): Promise<IPagination<T>> {
public async paginate(filter?: LegacyFindManyOptions<T>): Promise<IPagination<T>> {
return await super.paginate(this.findManyWithTenant(filter));
}
@@ -293,7 +294,7 @@ export abstract class TenantAwareCrudService<T extends TenantBaseEntity>
* @param options
* @returns
*/
public async findOneOrFailByIdString(id: ID, options?: FindOneOptions<T>): Promise<ITryRequest<T>> {
public async findOneOrFailByIdString(id: ID, options?: LegacyFindOneOptions<T>): Promise<ITryRequest<T>> {
return await super.findOneOrFailByIdString(id, this.findOneWithTenant(options));
}
@@ -304,7 +305,7 @@ export abstract class TenantAwareCrudService<T extends TenantBaseEntity>
* @param options
* @returns
*/
public async findOneOrFailByOptions(options?: FindOneOptions<T>): Promise<ITryRequest<T>> {
public async findOneOrFailByOptions(options?: LegacyFindOneOptions<T>): Promise<ITryRequest<T>> {
return await super.findOneOrFailByOptions(this.findOneWithTenant(options));
}
@@ -336,7 +337,7 @@ export abstract class TenantAwareCrudService<T extends TenantBaseEntity>
* @param options
* @returns
*/
public async findOneByIdString(id: ID, options?: FindOneOptions<T>): Promise<T> {
public async findOneByIdString(id: ID, options?: LegacyFindOneOptions<T>): Promise<T> {
return await super.findOneByIdString(id, this.findOneWithTenant(options));
}
@@ -347,7 +348,7 @@ export abstract class TenantAwareCrudService<T extends TenantBaseEntity>
* @param options
* @returns
*/
public async findOneByOptions(options: FindOneOptions<T>): Promise<T> {
public async findOneByOptions(options: LegacyFindOneOptions<T>): Promise<T> {
return await super.findOneByOptions(this.findOneWithTenant(options));
}
@@ -517,7 +518,7 @@ export abstract class TenantAwareCrudService<T extends TenantBaseEntity>
* @param options - Additional options for querying, such as extra conditions or query parameters.
* @returns {Promise<DeleteResult>} - The result of the delete operation.
*/
public async delete(criteria: string | FindOptionsWhere<T>, options?: FindOneOptions<T>): Promise<DeleteResult> {
public async delete(criteria: string | FindOptionsWhere<T>, options?: LegacyFindOneOptions<T>): Promise<DeleteResult> {
try {
// Merge additional where conditions from options into criteria if needed
let where: FindOptionsWhere<T> =
@@ -588,7 +589,7 @@ export abstract class TenantAwareCrudService<T extends TenantBaseEntity>
*/
public async softDelete(
criteria: string | number | FindOptionsWhere<T>,
options?: FindOneOptions<T>
options?: LegacyFindOneOptions<T>
): Promise<UpdateResult | T> {
try {
let record: T | null;
@@ -0,0 +1,176 @@
import {
parseFindOptionsRelations,
parseFindOptionsSelect,
parseTypeORMFindOptions,
stringArrayToFindOptionsObject
} from './utils';
/**
* These specs pin the behaviour that replaces the runtime shim previously carried in
* `patches/typeorm+1.0.0.patch`. The conversion must stay byte-for-byte compatible with that shim so
* removing the patch does not change what reaches TypeORM.
*/
describe('stringArrayToFindOptionsObject', () => {
it('returns an empty object for an empty array', () => {
expect(stringArrayToFindOptionsObject([])).toEqual({});
});
it('converts a single flat relation', () => {
expect(stringArrayToFindOptionsObject(['role'])).toEqual({ role: true });
});
it('converts several flat relations', () => {
expect(stringArrayToFindOptionsObject(['id', 'name', 'email'])).toEqual({
id: true,
name: true,
email: true
});
});
it('nests a dot-notated path', () => {
expect(stringArrayToFindOptionsObject(['tenant.featureOrganizations'])).toEqual({
tenant: { featureOrganizations: true }
});
});
it('nests a deep dot-notated path', () => {
expect(stringArrayToFindOptionsObject(['a.b.c.d'])).toEqual({
a: { b: { c: { d: true } } }
});
});
it('upgrades a leaf to an object when a longer sibling path follows (parent first)', () => {
expect(stringArrayToFindOptionsObject(['tenant', 'tenant.featureOrganizations'])).toEqual({
tenant: { featureOrganizations: true }
});
});
it('does NOT clobber an existing nested object when a shorter path follows (child first)', () => {
// Order independence: the later bare `tenant` must not overwrite `{ featureOrganizations: true }`.
expect(stringArrayToFindOptionsObject(['tenant.featureOrganizations', 'tenant'])).toEqual({
tenant: { featureOrganizations: true }
});
});
it('merges sibling nested paths under a shared parent', () => {
expect(stringArrayToFindOptionsObject(['tenant.a', 'tenant.b'])).toEqual({
tenant: { a: true, b: true }
});
});
it('is idempotent for duplicate paths', () => {
expect(stringArrayToFindOptionsObject(['role', 'role'])).toEqual({ role: true });
});
it('skips empty and non-string entries', () => {
expect(stringArrayToFindOptionsObject(['role', '', null as any, undefined as any, 5 as any])).toEqual({
role: true
});
});
it('drops empty segments inside malformed dotted paths', () => {
expect(stringArrayToFindOptionsObject(['tenant..settings', 'role.'])).toEqual({
tenant: { settings: true },
role: true
});
});
it('rejects prototype-polluting paths without altering Object.prototype', () => {
const result = stringArrayToFindOptionsObject([
'__proto__',
'role.__proto__',
'constructor',
'a.prototype.b',
'role'
]);
// Only the safe `role` survives; every path containing a dangerous segment is dropped.
expect(result).toEqual({ role: true });
expect(({} as any).polluted).toBeUndefined();
expect(Object.prototype).not.toHaveProperty('polluted');
});
it('does not pollute Object.prototype via a crafted __proto__ payload', () => {
stringArrayToFindOptionsObject(['__proto__.polluted']);
expect(({} as any).polluted).toBeUndefined();
});
});
describe('parseFindOptionsRelations', () => {
it('converts the legacy string-array form', () => {
expect(parseFindOptionsRelations(['role', 'tenant.featureOrganizations'])).toEqual({
role: true,
tenant: { featureOrganizations: true }
});
});
it('passes the object form through unchanged', () => {
const relations = { role: true, tenant: { featureOrganizations: true } };
expect(parseFindOptionsRelations(relations)).toBe(relations);
});
it('passes undefined through unchanged', () => {
expect(parseFindOptionsRelations(undefined)).toBeUndefined();
});
});
describe('parseFindOptionsSelect', () => {
it('converts the legacy string-array form', () => {
expect(parseFindOptionsSelect(['id', 'name', 'role.name'])).toEqual({
id: true,
name: true,
role: { name: true }
});
});
it('passes the object form through unchanged', () => {
const select = { id: true, name: true };
expect(parseFindOptionsSelect(select)).toBe(select);
});
});
describe('parseTypeORMFindOptions', () => {
it('converts array relations and select while preserving other options', () => {
const where = { id: '1' };
const order = { createdAt: 'DESC' as const };
const result = parseTypeORMFindOptions({
where,
order,
take: 10,
skip: 2,
withDeleted: true,
relations: ['role', 'tenant.settings'],
select: ['id', 'name']
});
expect(result).toEqual({
where,
order,
take: 10,
skip: 2,
withDeleted: true,
relations: { role: true, tenant: { settings: true } },
select: { id: true, name: true }
});
});
it('returns the same reference when nothing needs converting', () => {
const options = { where: { id: '1' }, relations: { role: true } };
expect(parseTypeORMFindOptions(options)).toBe(options);
});
it('does not mutate the input when converting', () => {
const options = { relations: ['role'], where: { id: '1' } };
const result = parseTypeORMFindOptions(options);
expect(options.relations).toEqual(['role']);
expect(result).not.toBe(options);
expect(result.relations).toEqual({ role: true });
});
it('passes null/undefined through unchanged', () => {
expect(parseTypeORMFindOptions(undefined as any)).toBeUndefined();
expect(parseTypeORMFindOptions(null as any)).toBeNull();
});
});
@@ -1,5 +1,6 @@
import { QueryBuilder } from '@mikro-orm/knex';
import { EntityTarget, FindManyOptions, SelectQueryBuilder } from 'typeorm';
import { EntityTarget, SelectQueryBuilder } from 'typeorm';
import { LegacyFindManyOptions } from '../../utils';
export interface IQueryBuilder<Entity> {
alias: string;
@@ -7,7 +8,7 @@ export interface IQueryBuilder<Entity> {
getQueryBuilder(): SelectQueryBuilder<Entity> | QueryBuilder<any>;
clone(): this;
subQuery(): IQueryBuilder<Entity>;
setFindOptions(findOptions: FindManyOptions<Entity>): this;
setFindOptions(findOptions: LegacyFindManyOptions<Entity>): this;
select(selection: string, selectionAliasName?: string): this;
addSelect(selection: string, selectionAliasName?: string): this;
from(
@@ -1,7 +1,8 @@
import { EntityRepository, QueryBuilder, QueryOrder } from '@mikro-orm/knex';
import { IQueryBuilder } from './iquery-builder';
import { Brackets, EntityTarget, FindManyOptions } from 'typeorm';
import { Brackets, EntityTarget } from 'typeorm';
import { convertTypeOrmConationAndParamsToMikroOrm, getConationFromQuery } from '../utils';
import { flatten, LegacyFindManyOptions } from '../../utils';
export class MikroOrmQueryBuilder<Entity extends object> implements IQueryBuilder<Entity> {
@@ -46,7 +47,9 @@ export class MikroOrmQueryBuilder<Entity extends object> implements IQueryBuilde
return qb;
}
setFindOptions(findOptions: FindManyOptions<Entity>): this {
setFindOptions(findOptions: LegacyFindManyOptions<Entity>): this {
// MikroORM natively accepts the string-array `relations`/`select` form (it flattens to
// populate/fields), so no conversion is needed here — only the widened parameter type.
const { select, where, order, skip, take, relations } = findOptions;
if (select) {
@@ -67,7 +70,9 @@ export class MikroOrmQueryBuilder<Entity extends object> implements IQueryBuilde
}
if (relations) {
this.applyRelationsToQueryBuilder(this.qb, relations as any);
// The widened option type accepts both the legacy string[] and the v1 object form;
// flatten() normalizes either to the dot-notated string[] applyRelationsToQueryBuilder expects.
this.applyRelationsToQueryBuilder(this.qb, flatten(relations) as any);
// this.qb.populate(relations as any);
}
@@ -119,7 +124,7 @@ export class MikroOrmQueryBuilder<Entity extends object> implements IQueryBuilde
}
addFrom(_entityTarget: ((qb: QueryBuilder<any>) => QueryBuilder<any>) | EntityTarget<Entity>, _aliasName?: string): this {
throw new Error(`Note: This is conceptual; MikrORM typically don't support multiple FROMs like typeORM. You might use this for sub-queries or additional joins instead.`);
throw new Error(`Note: This is conceptual; MikroORM typically don't support multiple FROMs like typeORM. You might use this for sub-queries or additional joins instead.`);
}
innerJoin(propertyPath: string, alias: string, condition?: any): this {
@@ -1,5 +1,6 @@
import { EntityTarget, FindManyOptions, Repository, SelectQueryBuilder } from 'typeorm';
import { LegacyFindManyOptions, parseTypeORMFindOptions } from '../../utils';
import { IQueryBuilder } from './iquery-builder';
export class TypeOrmQueryBuilder<Entity extends Object> implements IQueryBuilder<Entity> {
@@ -40,8 +41,10 @@ export class TypeOrmQueryBuilder<Entity extends Object> implements IQueryBuilder
return this;
}
setFindOptions(findOptions: FindManyOptions<Entity>) {
this.qb.setFindOptions(findOptions);
setFindOptions(findOptions: LegacyFindManyOptions<Entity>) {
// Normalize legacy string-array `relations`/`select` to object form before handing off to
// TypeORM's native query builder (which rejects the string-array syntax since v1.0).
this.qb.setFindOptions(parseTypeORMFindOptions(findOptions) as FindManyOptions<Entity>);
return this;
}
+168 -2
View File
@@ -10,7 +10,14 @@ import { SOFT_DELETABLE_FILTER } from 'mikro-orm-soft-delete';
import { BetterSqliteDriver } from '@mikro-orm/better-sqlite';
import { PostgreSqlDriver } from '@mikro-orm/postgresql';
import { MySqlDriver } from '@mikro-orm/mysql';
import { FindManyOptions, FindOperator, FindOptionsOrder } from 'typeorm';
import {
FindManyOptions,
FindOneOptions,
FindOperator,
FindOptionsOrder,
FindOptionsRelations,
FindOptionsSelect
} from 'typeorm';
import { sample } from 'underscore';
import * as path from 'path';
import * as fs from 'fs';
@@ -441,6 +448,163 @@ export const flatten = (input: any): any => {
return [];
};
/**
* TypeORM `FindManyOptions` widened to also accept the legacy string-array `relations`/`select`
* syntax that TypeORM removed in v1.0 (e.g. `relations: ['role', 'tenant.featureOrganizations']`).
*
* Ever Gauzy still passes this syntax in many dynamic call sites. Rather than patching TypeORM's own
* type declarations (the old `patches/typeorm+1.0.0.patch` approach), we widen our own option types
* and convert the arrays to object form at the TypeORM data-access boundary (see
* {@link parseTypeORMFindOptions}). The `relations`/`select` element types are pulled from TypeORM
* via indexed access so they track upstream automatically.
*/
export type LegacyFindManyOptions<T> = Omit<FindManyOptions<T>, 'relations' | 'select'> & {
relations?: FindManyOptions<T>['relations'] | string[];
select?: FindManyOptions<T>['select'] | string[];
};
/**
* TypeORM `FindOneOptions` widened to also accept the legacy string-array `relations`/`select`
* syntax. See {@link LegacyFindManyOptions}.
*/
export type LegacyFindOneOptions<T> = Omit<FindOneOptions<T>, 'relations' | 'select'> & {
relations?: FindOneOptions<T>['relations'] | string[];
select?: FindOneOptions<T>['select'] | string[];
};
/**
* Path segments that must never be used as object keys when building find-option objects from
* (potentially untrusted) string input, to avoid prototype-pollution assignments.
*/
const UNSAFE_FIND_OPTION_SEGMENTS = new Set(['__proto__', 'prototype', 'constructor']);
/**
* Converts the legacy string-array find-option syntax (`['role', 'tenant.featureOrganizations']`)
* into the nested object form TypeORM v1 requires (`{ role: true, tenant: { featureOrganizations: true } }`).
* A dot in a segment denotes nesting.
*
* This replaces the runtime compatibility shim that previously lived in
* `patches/typeorm+1.0.0.patch` (TypeORM removed the string-array `relations`/`select` syntax in
* v1.0). Applying the conversion in application code — at the TypeORM data-access boundary — lets us
* drop that node_modules patch while keeping the many dynamic `string[]` call sites working.
*
* The merge rules match the shim exactly so behaviour is unchanged: a leaf only sets `true` when the
* key is still unset (an existing nested object from a longer sibling path is preserved), and an
* intermediate segment upgrades a `true` leaf to a nested object. Empty / non-string segments are
* skipped.
*
* @param paths - The dot-notated relation/column paths to convert.
* @returns The equivalent nested object form.
*/
export function stringArrayToFindOptionsObject(paths: readonly string[]): Record<string, any> {
let result: Record<string, any> = {};
for (const rawPath of paths) {
if (typeof rawPath !== 'string' || rawPath.length === 0) {
continue;
}
// Drop empty segments so malformed inputs like `role.` or `tenant..settings` don't create
// bogus `''` relation keys, and reject any path that carries a prototype-polluting segment
// (these values can originate from untrusted API `relations`/`select` query params).
const segments = rawPath.split('.').filter((segment) => segment.length > 0);
if (segments.length === 0 || segments.some((segment) => UNSAFE_FIND_OPTION_SEGMENTS.has(segment))) {
continue;
}
let cursor = result;
for (let i = 0; i < segments.length; i++) {
const segment = segments[i];
const isLeaf = i === segments.length - 1;
if (isLeaf) {
if (cursor[segment] === undefined) {
cursor[segment] = true;
}
} else {
if (cursor[segment] === true || cursor[segment] === undefined) {
cursor[segment] = {};
}
cursor = cursor[segment];
}
}
}
return result;
}
/**
* Normalizes a TypeORM `relations` find-option, converting the legacy `string[]` form to the object
* form v1 expects and passing the object form (or `undefined`) through unchanged. Safe to call on any
* `relations` value, so it can wrap options that may already use either syntax.
*
* @param relations - The `relations` option in either legacy `string[]` or object form.
* @returns The `relations` option in object form, or the original value when not an array.
*/
export function parseFindOptionsRelations<T = unknown>(
relations: string[] | FindOptionsRelations<any> | undefined
): FindOptionsRelations<T> | undefined {
// NOTE: the parameter is intentionally `FindOptionsRelations<any>` (not `FindOptionsRelations<T>`)
// so `T` is inferred from the assignment context (the field being populated), never from the
// argument. Inferring `T` from a `string[]` argument makes `FindOptionsRelations<T>` resolve to a
// bogus array type, which would then reject at every call site.
if (Array.isArray(relations)) {
return stringArrayToFindOptionsObject(relations) as FindOptionsRelations<T>;
}
return relations as FindOptionsRelations<T> | undefined;
}
/**
* Normalizes a TypeORM `select` find-option, converting the legacy `string[]` form to the object form
* v1 expects and passing the object form (or `undefined`) through unchanged. Safe to call on any
* `select` value, so it can wrap options that may already use either syntax.
*
* @param select - The `select` option in either legacy `string[]` or object form.
* @returns The `select` option in object form, or the original value when not an array.
*/
export function parseFindOptionsSelect<T = unknown>(
select: string[] | FindOptionsSelect<any> | undefined
): FindOptionsSelect<T> | undefined {
// See parseFindOptionsRelations: parameter is `FindOptionsSelect<any>` so `T` is inferred from the
// assignment context rather than a `string[]` argument.
if (Array.isArray(select)) {
return stringArrayToFindOptionsObject(select) as FindOptionsSelect<T>;
}
return select as FindOptionsSelect<T> | undefined;
}
/**
* Normalizes the `relations` and `select` members of a TypeORM find-options object in place-safe
* fashion, returning a shallow copy with the legacy `string[]` form converted to object form. Any
* other options (`where`, `order`, `skip`, `take`, …) are preserved untouched.
*
* Use this at TypeORM data-access boundaries (repository / query-builder calls) that must not receive
* the legacy `string[]` syntax now that the TypeORM patch is removed. The MikroORM path does NOT need
* this — {@link flatten} already accepts both forms — so callers should convert only on the TypeORM
* branch to keep MikroORM behaviour identical.
*
* @param options - The find-options to normalize. `null`/`undefined` is returned unchanged.
* @returns A normalized shallow copy, or the original value when there is nothing to convert.
*/
export function parseTypeORMFindOptions<T, O extends { relations?: any; select?: any }>(options: O): O {
if (!options || typeof options !== 'object') {
return options;
}
const hasArrayRelations = Array.isArray(options.relations);
const hasArraySelect = Array.isArray(options.select);
if (!hasArrayRelations && !hasArraySelect) {
return options;
}
return {
...options,
...(hasArrayRelations ? { relations: stringArrayToFindOptionsObject(options.relations) } : {}),
...(hasArraySelect ? { select: stringArrayToFindOptionsObject(options.select) } : {})
};
}
/**
* Concatenate an ID to the given MikroORM where condition.
*
@@ -483,10 +647,12 @@ export function enhanceWhereWithTenantId<T>(tenantId: any, where: MikroFilterQue
* @param options - TypeORM's FindManyOptions.
* @returns An object with MikroORM's where and options.
*/
export function parseTypeORMFindToMikroOrm<T>(options: FindManyOptions): {
export function parseTypeORMFindToMikroOrm<T>(options: LegacyFindManyOptions<any>): {
where: MikroFilterQuery<T>;
mikroOptions: MikroORMFindOptions<T, any, any, any>;
} {
// The parameter accepts the legacy string-array `relations`/`select` form: the MikroORM path
// consumes both forms natively (see `flatten`), so no conversion is applied here.
const mikroOptions: MikroORMFindOptions<T, any, any, any> = {
disableIdentityMap: true,
populate: []
@@ -3,6 +3,7 @@ import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import { EmployeeService } from '../../../employee/employee.service';
import { ExpenseService } from '../../expense.service';
import { FindSplitExpenseQuery } from '../expense.find-split-expense.query';
import { parseFindOptionsRelations } from '../../../core/utils';
/**
* Finds the split expense for a given organization.
@@ -47,7 +48,7 @@ export class FindSplitExpenseHandler
}
}
],
relations
relations: parseFindOptionsRelations(relations)
},
filterDate
);
@@ -29,7 +29,7 @@ import {
import { isEmpty, isNotEmpty } from '@gauzy/utils';
import { BaseQueryDTO, TenantAwareCrudService } from './../core/crud';
import { RequestContext } from '../core/context';
import { MultiORMEnum } from '../core/utils';
import { MultiORMEnum, parseFindOptionsRelations, parseFindOptionsSelect } from '../core/utils';
import { LIKE_OPERATOR } from '../core/util';
import { OrganizationProjectModule } from './organization-project-module.entity';
import { prepareSQLQuery as p } from './../database/database.helper';
@@ -621,7 +621,7 @@ export class OrganizationProjectModuleService extends TenantAwareCrudService<Org
}
if (params.select) {
options.select = params.select;
options.select = parseFindOptionsSelect(params.select);
}
if (params.order) {
@@ -629,7 +629,7 @@ export class OrganizationProjectModuleService extends TenantAwareCrudService<Org
}
if (params.relations) {
options.relations = params.relations;
options.relations = parseFindOptionsRelations(params.relations);
}
// Apply pagination and query options
@@ -23,7 +23,7 @@ import { isNotEmpty } from '@gauzy/utils';
import { RelationsQueryDTO } from '../shared/dto';
import { BaseQueryDTO, TenantAwareCrudService } from '../core/crud';
import { RequestContext } from '../core/context';
import { MultiORMEnum } from '../core/utils';
import { MultiORMEnum, parseFindOptionsRelations } from '../core/utils';
import { OrganizationProjectEmployee } from '../core/entities/internal';
import { FavoriteService } from '../core/decorators';
import { prepareSQLQuery as p } from './../database/database.helper';
@@ -446,7 +446,7 @@ export class OrganizationProjectService extends TenantAwareCrudService<Organizat
owner: true,
taskListType: true
},
relations
relations: parseFindOptionsRelations(relations)
});
query
.innerJoin(`${query.alias}.members`, 'project_members')
@@ -27,7 +27,13 @@ import {
import { isNotEmpty, parseToBoolean } from '@gauzy/utils';
import { FavoriteService } from '../core/decorators';
import { Employee, OrganizationTeamEmployee } from '../core/entities/internal';
import { MultiORMEnum, enhanceWhereWithTenantId, parseTypeORMFindToMikroOrm } from '../core/utils';
import {
MultiORMEnum,
enhanceWhereWithTenantId,
parseTypeORMFindToMikroOrm,
parseFindOptionsRelations,
parseFindOptionsSelect
} from '../core/utils';
import { BaseQueryDTO, TenantAwareCrudService } from '../core/crud';
import { RequestContext } from '../core/context';
import { RoleService } from '../role/role.service';
@@ -560,8 +566,8 @@ export class OrganizationTeamService extends TenantAwareCrudService<Organization
typeOrmQueryBuilder.setFindOptions({
...(options.skip ? { skip: options.take * (options.skip - 1) } : {}),
...(options.take ? { take: options.take } : {}),
...(options.select ? { select: options.select } : {}),
...(options.relations ? { relations: options.relations } : {}),
...(options.select ? { select: parseFindOptionsSelect(options.select) } : {}),
...(options.relations ? { relations: parseFindOptionsRelations(options.relations) } : {}),
...(options.where ? { where: options.where } : {}),
...(options.order ? { order: options.order } : {})
});
@@ -7,6 +7,7 @@ import { Pipeline } from './pipeline.entity';
import { Deal, PipelineStage } from './../core/entities/internal';
import { RequestContext } from '../core/context/request-context';
import { LIKE_OPERATOR } from '../core/util';
import { parseFindOptionsRelations } from '../core/utils';
import { TenantAwareCrudService } from './../core/crud/tenant-aware-crud.service';
import { TypeOrmDealRepository } from '../deal/repository/type-orm-deal.repository';
import { TypeOrmUserRepository } from '../user/repository/type-orm-user.repository';
@@ -68,7 +69,7 @@ export class PipelineService extends TenantAwareCrudService<Pipeline> {
};
if (relations.length) {
queryOptions.relations = relations;
queryOptions.relations = parseFindOptionsRelations(relations);
}
try {
@@ -4,6 +4,7 @@ import { FindOptionsSelect, FindOptionsWhere } from 'typeorm';
import { IEmployee, IPagination } from '@gauzy/contracts';
import { Employee } from './../../core/entities/internal';
import { TypeOrmEmployeeRepository } from '../../employee/repository/type-orm-employee.repository';
import { parseFindOptionsRelations } from '../../core/utils';
/**
* Display-safe field allowlist for the public employee profile.
@@ -99,10 +100,9 @@ export class PublicEmployeeService {
relations: string[] = []
): Promise<IPagination<IEmployee>> {
try {
// TODO(typeorm-v1): `relations` no longer accepts a string array. This value references a variable whose shape can't be determined statically — if it holds `string[]`, wrap it: `Object.fromEntries(<expr>?.map(r => [r, true]) ?? [])` (dot-paths need extra nesting handling). If it already holds the v1 object shape, no change needed.
const [items = [], total = 0] = await this.typeOrmEmployeeRepository.findAndCount({
where,
relations,
relations: parseFindOptionsRelations(relations),
// Restrict the response to display-safe fields only (GHSA-49ff-8859-537j).
select: PUBLIC_EMPLOYEE_SELECT
});
@@ -121,10 +121,9 @@ export class PublicEmployeeService {
*/
async findOneByConditions(where: FindOptionsWhere<Employee>, relations: string[]): Promise<IEmployee> {
try {
// TODO(typeorm-v1): `relations` no longer accepts a string array. This value references a variable whose shape can't be determined statically — if it holds `string[]`, wrap it: `Object.fromEntries(<expr>?.map(r => [r, true]) ?? [])` (dot-paths need extra nesting handling). If it already holds the v1 object shape, no change needed.
const employee = await this.typeOrmEmployeeRepository.findOneOrFail({
where,
relations,
relations: parseFindOptionsRelations(relations),
// Restrict the response to display-safe fields only (GHSA-49ff-8859-537j).
select: PUBLIC_EMPLOYEE_SELECT
});
@@ -5,6 +5,7 @@ import { verify } from 'jsonwebtoken';
import { IInvoice, IInvoiceUpdateInput } from '@gauzy/contracts';
import { environment } from '@gauzy/config';
import { Invoice } from './../../core/entities/internal';
import { parseFindOptionsRelations } from '../../core/utils';
import { TypeOrmInvoiceRepository } from '../../invoice/repository/type-orm-invoice.repository';
@Injectable()
@@ -101,7 +102,7 @@ export class PublicInvoiceService {
organizationId,
tenantId
},
...(relations ? { relations: relations } : {})
...(relations ? { relations: parseFindOptionsRelations(relations) } : {})
});
} catch (error) {
throw new ForbiddenException();
@@ -1,7 +1,7 @@
import { IOrganization, IOrganizationContact, IPagination } from '@gauzy/contracts';
import { Injectable, NotFoundException } from '@nestjs/common';
import { FindOptionsSelect, FindOptionsWhere } from 'typeorm';
import { MultiORM, MultiORMEnum, getORMType } from '../../core/utils';
import { MultiORM, MultiORMEnum, getORMType, parseFindOptionsRelations } from '../../core/utils';
import { Organization, OrganizationContact, OrganizationProject } from './../../core/entities/internal';
/**
@@ -106,10 +106,9 @@ export class PublicOrganizationService {
break;
case MultiORMEnum.TypeORM:
default:
// TODO(typeorm-v1): `relations` no longer accepts a string array. This value references a variable whose shape can't be determined statically — if it holds `string[]`, wrap it: `Object.fromEntries(<expr>?.map(r => [r, true]) ?? [])` (dot-paths need extra nesting handling). If it already holds the v1 object shape, no change needed.
organization = await this.typeOrmOrganizationRepository.findOneOrFail({
where,
relations,
relations: parseFindOptionsRelations(relations),
// Restrict the response to display-safe fields only (GHSA-49ff-8859-537j).
select: PUBLIC_ORGANIZATION_SELECT
});
@@ -8,6 +8,7 @@ import {
ITimerStatus
} from '@gauzy/contracts';
import { parseToBoolean } from '@gauzy/utils';
import { parseFindOptionsRelations } from '../../core/utils';
import { OrganizationTeam } from './../../core/entities/internal';
import { StatisticService } from './../../time-tracking/statistic';
import { TimerService } from './../../time-tracking/timer/timer.service';
@@ -62,7 +63,7 @@ export class PublicTeamService {
public: true,
...params
},
...(options.relations ? { relations: options.relations } : {})
...(options.relations ? { relations: parseFindOptionsRelations(options.relations) } : {})
};
try {
@@ -1,7 +1,7 @@
import { Injectable, Logger } from '@nestjs/common';
import { GetReportMenuItemsInput, IPagination, IReport } from '@gauzy/contracts';
import { CrudService } from '../core/crud';
import { MultiORMEnum } from '../core/utils';
import { MultiORMEnum, parseFindOptionsRelations } from '../core/utils';
import { RequestContext } from './../core/context';
import { Report } from './report.entity';
import { MikroOrmReportRepository } from './repository/mikro-orm-report.repository';
@@ -62,7 +62,7 @@ export class ReportService extends CrudService<Report> {
// Fetch all reports and their associated organizations in a single query
const qb = this.typeOrmRepository.createQueryBuilder('report');
qb.setFindOptions({
...(filter.relations ? { relations: filter.relations } : {})
...(filter.relations ? { relations: parseFindOptionsRelations(filter.relations) } : {})
});
qb.leftJoinAndSelect(
'report.reportOrganizations',
@@ -17,7 +17,7 @@ import { prepareSQLQuery as p } from './../database/database.helper';
import { RequestContext } from '../core/context';
import { RequestApprovalEmployee, RequestApprovalTeam } from './../core/entities/internal';
import { TenantAwareCrudService } from './../core/crud';
import { MultiORMEnum } from './../core/utils';
import { MultiORMEnum, parseFindOptionsRelations } from './../core/utils';
import { RequestApproval } from './request-approval.entity';
import { MikroOrmRequestApprovalRepository } from './repository/mikro-orm-request-approval.repository';
import { TypeOrmRequestApprovalRepository } from './repository/type-orm-request-approval.repository';
@@ -144,7 +144,7 @@ export class RequestApprovalService extends TenantAwareCrudService<RequestApprov
const relations = filter.relations as string[];
if (relations && relations.length > 0) {
query.setFindOptions({ relations });
query.setFindOptions({ relations: parseFindOptionsRelations(relations) });
}
const [items, total] = await query
@@ -209,10 +209,9 @@ export class RequestApprovalService extends TenantAwareCrudService<RequestApprov
break;
case MultiORMEnum.TypeORM:
default:
// TODO(typeorm-v1): `relations` no longer accepts a string array. This value references a variable whose shape can't be determined statically — if it holds `string[]`, wrap it: `Object.fromEntries(<expr>?.map(r => [r, true]) ?? [])` (dot-paths need extra nesting handling). If it already holds the v1 object shape, no change needed.
employee = await this.typeOrmEmployeeRepository.findOne({
where: { id },
relations
relations: parseFindOptionsRelations(relations)
});
break;
}
+3 -3
View File
@@ -5,7 +5,7 @@ import { FileStorageProviderEnum, IPagination, ITag, ITagFindInput } from '@gauz
import { getConfig } from '@gauzy/config';
import { RequestContext } from '../core/context';
import { TenantAwareCrudService } from '../core/crud';
import { MultiORMEnum } from '../core/utils';
import { MultiORMEnum, parseFindOptionsRelations } from '../core/utils';
import { LIKE_OPERATOR } from '../core/util';
import { Tag } from './tag.entity';
import { FileStorage } from './../core/file-storage';
@@ -53,7 +53,7 @@ export class TagService extends TenantAwareCrudService<Tag> {
// Add relations if specified
if (relations.length) {
query.setFindOptions({ relations });
query.setFindOptions({ relations: parseFindOptionsRelations(relations) });
}
// Apply filter criteria
@@ -115,7 +115,7 @@ export class TagService extends TenantAwareCrudService<Tag> {
const query = this.typeOrmRepository.createQueryBuilder(this.tableName);
// Define special criteria to find specific relations
query.setFindOptions({
...(relations ? { relations: relations } : {})
...(relations ? { relations: parseFindOptionsRelations(relations) } : {})
});
// Left join all relational tables with tag table
@@ -14,7 +14,7 @@ import { isNotEmpty } from '@gauzy/utils';
import { prepareSQLQuery as p } from '../../database/database.helper';
import { BaseQueryDTO, TenantAwareCrudService } from '../../core/crud';
import { RequestContext } from '../../core/context/request-context';
import { MultiORMEnum } from '../../core/utils';
import { MultiORMEnum, parseFindOptionsRelations } from '../../core/utils';
import { EmployeeService } from '../../employee/employee.service';
import { ManagedEmployeeService } from '../../employee/managed-employee.service';
import { TaskService } from '../task.service';
@@ -159,7 +159,7 @@ export class DailyPlanService extends TenantAwareCrudService<DailyPlan> {
}),
...(isNotEmpty(options) &&
isNotEmpty(options.relations) && {
relations: options.relations
relations: parseFindOptionsRelations(options.relations)
})
});
@@ -235,7 +235,7 @@ export class DailyPlanService extends TenantAwareCrudService<DailyPlan> {
query.setFindOptions({
where: isNotEmpty(where) && where,
relations: isNotEmpty(relations) && relations
relations: isNotEmpty(relations) && parseFindOptionsRelations(relations)
});
// Filter conditions
+9 -9
View File
@@ -35,7 +35,7 @@ import {
import { isEmpty, isNotEmpty } from '@gauzy/utils';
import { isSqlite } from '@gauzy/config';
import { TenantAwareCrudService, BaseQueryDTO } from './../core/crud';
import { MultiORMEnum } from './../core/utils';
import { MultiORMEnum, parseFindOptionsRelations, parseFindOptionsSelect } from './../core/utils';
import { addBetween, LIKE_OPERATOR } from './../core/util';
import { RequestContext } from '../core/context';
import { TaskViewService } from './views/view.service';
@@ -444,7 +444,7 @@ export class TaskService extends TenantAwareCrudService<Task> {
});
}
query.setFindOptions({
...(options.relations ? { relations: options.relations } : {})
...(options.relations ? { relations: parseFindOptionsRelations(options.relations) } : {})
});
}
@@ -581,7 +581,7 @@ export class TaskService extends TenantAwareCrudService<Task> {
}),
...(isNotEmpty(options) &&
isNotEmpty(options.relations) && {
relations: options.relations
relations: parseFindOptionsRelations(options.relations)
})
});
@@ -714,8 +714,8 @@ export class TaskService extends TenantAwareCrudService<Task> {
});
}
query.setFindOptions({
...(options.select ? { select: options.select } : {}),
...(options.relations ? { relations: options.relations } : {}),
...(options.select ? { select: parseFindOptionsSelect(options.select) } : {}),
...(options.relations ? { relations: parseFindOptionsRelations(options.relations) } : {}),
...(options.order ? { order: options.order } : {})
});
}
@@ -1131,8 +1131,8 @@ export class TaskService extends TenantAwareCrudService<Task> {
// Apply find options if provided
if (isNotEmpty(options)) {
query.setFindOptions({
...(options.select && { select: options.select }),
...(options.relations && { relations: options.relations }),
...(options.select && { select: parseFindOptionsSelect(options.select) }),
...(options.relations && { relations: parseFindOptionsRelations(options.relations) }),
...(options.order && { order: options.order })
});
}
@@ -1301,7 +1301,7 @@ export class TaskService extends TenantAwareCrudService<Task> {
};
// Define find options
const findOptions: FindManyOptions<Task> = { where, ...(relations && { relations }) };
const findOptions: FindManyOptions<Task> = { where, ...(relations && { relations: parseFindOptionsRelations(relations) }) };
// Retrieve tasks using base class method
return await super.findAll(findOptions);
@@ -1437,7 +1437,7 @@ export class TaskService extends TenantAwareCrudService<Task> {
// Check if relations were provided and include them
query.setFindOptions({
...(relations ? { relations } : {})
...(relations ? { relations: parseFindOptionsRelations(relations) } : {})
});
const [items, total] = await query.getManyAndCount();
@@ -15,7 +15,7 @@ import { TimeOffRequest } from './time-off-request.entity';
import { RequestApproval } from '../request-approval/request-approval.entity';
import { TenantAwareCrudService } from './../core/crud';
import { RequestContext } from './../core/context';
import { MultiORMEnum } from '../core/utils';
import { MultiORMEnum, parseFindOptionsRelations } from '../core/utils';
import { prepareSQLQuery as p } from './../database/database.helper';
import { TypeOrmRequestApprovalRepository } from '../request-approval/repository/type-orm-request-approval.repository';
import { MikroOrmTimeOffRequestRepository } from './repository/mikro-orm-time-off-request.repository';
@@ -228,7 +228,7 @@ export class TimeOffRequestService extends TenantAwareCrudService<TimeOffRequest
take: options.take ? options.take : 10,
...(options.join ? { join: options.join } : {}),
...(options.relations ? { relations: options.relations } : {})
...(options.relations ? { relations: parseFindOptionsRelations(options.relations) } : {})
});
}
query.where((qb: SelectQueryBuilder<TimeOffRequest>) => {
@@ -37,7 +37,7 @@ import {
TimeLogDeleteCommand,
TimeLogUpdateCommand
} from './commands';
import { getDateRangeFormat, getDaysBetweenDates, MultiORMEnum } from './../../core/utils';
import { getDateRangeFormat, getDaysBetweenDates, MultiORMEnum, parseFindOptionsRelations } from './../../core/utils';
import { RequestContext } from '../../core/context';
import { moment } from './../../core/moment-extend';
import { calculateAverage, calculateAverageActivity, calculateDuration } from './time-log.utils';
@@ -123,7 +123,7 @@ export class TimeLogService extends TenantAwareCrudService<TimeLog> {
}
}
},
relations: [...(request.relations ? request.relations : [])],
relations: parseFindOptionsRelations([...(request.relations ? request.relations : [])]),
order: {
// Order results by the 'startedAt' field in ascending order
startedAt: 'ASC'
@@ -6,7 +6,7 @@ import { isEmpty, isNotEmpty } from '@gauzy/utils';
import { RequestContext } from '../../core/context';
import { TenantAwareCrudService } from './../../core/crud';
import { moment } from '../../core/moment-extend';
import { getDateRangeFormat, MultiORMEnum } from './../../core/utils';
import { getDateRangeFormat, MultiORMEnum, parseFindOptionsRelations } from './../../core/utils';
import { generateTimeSlots } from './utils';
import { TimeSlot } from './time-slot.entity';
import {
@@ -138,7 +138,7 @@ export class TimeSlotService extends TenantAwareCrudService<TimeSlot> {
}
},
// Spread relations if provided, otherwise an empty array
relations: request.relations || []
relations: parseFindOptionsRelations(request.relations || [])
});
// Add where conditions to the query
@@ -25,6 +25,7 @@ import {
MultiORMEnum,
getDateRangeFormat,
getORMType,
parseFindOptionsRelations,
parseTypeORMFindToMikroOrm,
wrapSerialize,
validateDateRange
@@ -168,7 +169,10 @@ export class TimerService {
addRelationsToQuery(lastLogQueryParamsTypeOrm, request); // Adds relations from the request to the query parameters.
// Get today's last log (running or completed)
lastLog = await this.typeOrmTimeLogRepository.findOne(lastLogQueryParamsTypeOrm);
lastLog = await this.typeOrmTimeLogRepository.findOne({
...lastLogQueryParamsTypeOrm,
relations: parseFindOptionsRelations(lastLogQueryParamsTypeOrm.relations)
});
break;
default:
@@ -779,7 +783,7 @@ export class TimerService {
const query = this.typeOrmTimeLogRepository.createQueryBuilder('time_log');
// query.innerJoin(`${query.alias}.timeSlots`, 'timeSlots');
query.setFindOptions({
...(request['relations'] ? { relations: request['relations'] } : {})
...(request['relations'] ? { relations: parseFindOptionsRelations(request['relations']) } : {})
});
query.where({
startedAt: Not(IsNull()),
@@ -4,7 +4,7 @@ import * as moment from 'moment';
import { IGetTimesheetInput, PermissionsEnum, ITimesheet, TimesheetStatus } from '@gauzy/contracts';
import { RequestContext } from './../../core/context';
import { TenantAwareCrudService } from './../../core/crud';
import { getDateRangeFormat, MultiORMEnum } from './../../core/utils';
import { getDateRangeFormat, MultiORMEnum, parseFindOptionsRelations } from './../../core/utils';
import { Timesheet } from './timesheet.entity';
import { prepareSQLQuery as p } from './../../database/database.helper';
import { TypeOrmTimesheetRepository } from './repository/type-orm-timesheet.repository';
@@ -139,7 +139,7 @@ export class TimeSheetService extends TenantAwareCrudService<Timesheet> {
brandColor: true
}
},
...(request?.relations ? { relations: request.relations } : {})
...(request?.relations ? { relations: parseFindOptionsRelations(request.relations) } : {})
});
// Apply filters to the query
+4 -2
View File
@@ -31,7 +31,7 @@ import { isNotEmpty } from '@gauzy/utils';
import { prepareSQLQuery as p } from './../database/database.helper';
import { TenantAwareCrudService } from './../core/crud';
import { RequestContext } from './../core/context';
import { freshTimestamp, MultiORMEnum } from './../core/utils';
import { freshTimestamp, MultiORMEnum, parseFindOptionsRelations } from './../core/utils';
import { EmployeeService } from '../employee/employee.service';
import { TaskService } from '../tasks/task.service';
import { MikroOrmUserRepository } from './repository/mikro-orm-user.repository';
@@ -133,7 +133,9 @@ export class UserService extends TenantAwareCrudService<User> {
// Fetch employee details if 'includeEmployee' is true
if (options.includeEmployee) {
const relations = options.includeOrganization ? { organization: true } : [];
employee = await this._employeeService.findOneByUserId(user.id, undefined, { relations });
employee = await this._employeeService.findOneByUserId(user.id, undefined, {
relations: parseFindOptionsRelations(relations)
});
}
// Return user data combined with employee data, if it exists.
@@ -39,7 +39,7 @@ import {
ITimeLog,
IIntegrationSetting
} from '@gauzy/contracts';
import { RequestContext, mergeOverlappingDateRanges, unixTimestampToDate } from '@gauzy/core';
import { RequestContext, mergeOverlappingDateRanges, parseFindOptionsRelations, unixTimestampToDate } from '@gauzy/core';
import {
ExpenseService,
IncomeService,
@@ -101,7 +101,7 @@ export class UpworkService {
settingsValue: config.consumerKey,
organizationId: organizationId
},
relations: ['integration']
relations: parseFindOptionsRelations(['integration'])
})
);
if (!integrationSetting) {
@@ -216,7 +216,7 @@ export class UpworkService {
settingsValue: requestToken,
organizationId
},
relations: ['integration']
relations: parseFindOptionsRelations(['integration'])
})
);
const integrationSettings: IIntegrationSetting[] = await this._commandBus.execute(
@@ -20,7 +20,8 @@ import {
DEFAULT_ENTITY_SETTINGS,
PROJECT_TIED_ENTITIES,
RequestContext,
IntegrationTenantService
IntegrationTenantService,
parseFindOptionsRelations
} from '@gauzy/core';
import { ZAPIER_API_URL, ZAPIER_BASE_URL, ZAPIER_TOKEN_EXPIRATION_TIME, ZAPIER_OAUTH_SCOPES } from './zapier.config';
import {
@@ -869,7 +870,7 @@ export class ZapierService {
tenantId,
name: IntegrationEnum.ZAPIER
} as IIntegrationFilter,
relations: ['settings']
relations: parseFindOptionsRelations(['settings'])
});
if (total === 0) {
@@ -1,7 +1,7 @@
import { In } from 'typeorm';
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { GauzyAIService } from '@gauzy/plugin-integration-ai';
import { TypeOrmEmployeeRepository } from '@gauzy/core';
import { parseFindOptionsRelations, TypeOrmEmployeeRepository } from '@gauzy/core';
import { EmployeeUpworkJobsSearchCriterion } from '../../employee-upwork-jobs-search-criterion.entity';
import { JobPreset } from '../../job-preset.entity';
import { SaveEmployeePresetCommand } from '../save-employee-preset.command';
@@ -30,7 +30,7 @@ export class SaveEmployeePresetHandler implements ICommandHandler<SaveEmployeePr
// Find the employee with related data
let employee = await this._typeOrmEmployeeRepository.findOne({
where: { id: employeeId },
relations: ['user', 'organization', 'customFields.jobPresets']
relations: parseFindOptionsRelations(['user', 'organization', 'customFields.jobPresets'])
});
// Find the job preset with related criteria
@@ -60,7 +60,7 @@ export class SaveEmployeePresetHandler implements ICommandHandler<SaveEmployeePr
// Find the employee with related data
employee = await this._typeOrmEmployeeRepository.findOne({
where: { id: employeeId },
relations: ['customFields.jobPresets']
relations: parseFindOptionsRelations(['customFields.jobPresets'])
});
return employee.customFields['jobPresets'];
@@ -15,6 +15,7 @@ import {
import {
LIKE_OPERATOR,
MultiORMEnum,
parseFindOptionsRelations,
RequestContext,
TenantAwareCrudService,
TypeOrmEmployeeRepository
@@ -244,7 +245,7 @@ export class JobPresetService extends TenantAwareCrudService<JobPreset> {
// Find the employee with the specified ID and include jobPresets relation
const employee = await this.typeOrmEmployeeRepository.findOne({
where: { id: employeeId },
relations: ['customFields.jobPresets']
relations: parseFindOptionsRelations(['customFields.jobPresets'])
});
// Return the job presets associated with the employee
@@ -12,6 +12,8 @@ import {
} from 'typeorm';
import {
MultiORMEnum,
parseFindOptionsRelations,
parseFindOptionsSelect,
RequestContext,
TenantAwareCrudService,
BaseQueryDTO,
@@ -141,8 +143,8 @@ export class HelpCenterArticleService extends TenantAwareCrudService<HelpCenterA
// Apply find options if provided
if (isNotEmpty(options)) {
query.setFindOptions({
...(options.select && { select: options.select }),
...(options.relations && { relations: options.relations }),
...(options.select && { select: parseFindOptionsSelect(options.select) }),
...(options.relations && { relations: parseFindOptionsRelations(options.relations) }),
...(options.order && { order: options.order }),
...(options.take && { take: options.take }),
...(options.skip && { skip: options.skip })
@@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common';
import { Not } from 'typeorm';
import { IEmployee, IOrganization, ITenant } from '@gauzy/contracts';
import { getDefaultOrganizations, getDefaultEmployees, SeedDataService, Tenant, Employee } from '@gauzy/core';
import { getDefaultOrganizations, getDefaultEmployees, SeedDataService, Tenant, Employee, parseFindOptionsRelations } from '@gauzy/core';
import { createHelpCenter } from './help-center';
import { createHelpCenterArticle } from './help-center-article/help-center-article.seed';
import { createDefaultHelpCenterAuthor, createRandomHelpCenterAuthor } from './help-center-author';
@@ -69,7 +69,7 @@ export class HelpCenterSeederService {
where: {
name: Not(name)
},
relations: ['organizations']
relations: parseFindOptionsRelations(['organizations'])
});
const tenantOrganizationsMap: Map<ITenant, IOrganization[]> = new Map();
@@ -1,9 +1,9 @@
import { LegacyFindManyOptions } from '@gauzy/core';
import { IQuery } from '@nestjs/cqrs';
import { FindManyOptions } from 'typeorm';
import { PluginCategory } from '../../../domain';
export class GetPluginCategoriesQuery implements IQuery {
static readonly type = '[Plugin Category] Get Categories';
constructor(public readonly options?: FindManyOptions<PluginCategory>) {}
constructor(public readonly options?: LegacyFindManyOptions<PluginCategory>) {}
}
@@ -1,4 +1,4 @@
import { RequestContext } from '@gauzy/core';
import { RequestContext, parseFindOptionsRelations } from '@gauzy/core';
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import { FindManyOptions, FindOptionsWhere } from 'typeorm';
import { PluginTenant, PluginTenantService } from '../../../../domain';
@@ -68,7 +68,7 @@ export class GetAllPluginTenantsHandler implements IQueryHandler<GetAllPluginTen
const options: FindManyOptions = {
where,
relations: ['plugin', 'approvedBy', 'allowedRoles', 'allowedUsers', 'deniedUsers'],
relations: parseFindOptionsRelations(['plugin', 'approvedBy', 'allowedRoles', 'allowedUsers', 'deniedUsers']),
order: { createdAt: 'DESC' }
};
@@ -1,6 +1,7 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { parseFindOptionsRelations } from '@gauzy/core';
import { IPluginTag } from '../../shared';
import { PluginTag } from '../entities/plugin-tag.entity';
@@ -36,7 +37,7 @@ export class TypeOrmPluginTagRepository extends Repository<PluginTag> {
async findByPluginId(pluginId: string, relations: string[] = ['tag']): Promise<IPluginTag[]> {
return this.find({
where: { pluginId },
relations
relations: parseFindOptionsRelations(relations)
});
}
@@ -50,7 +51,7 @@ export class TypeOrmPluginTagRepository extends Repository<PluginTag> {
async findByTagId(tagId: string, relations: string[] = ['plugin']): Promise<IPluginTag[]> {
return this.find({
where: { tagId },
relations
relations: parseFindOptionsRelations(relations)
});
}
@@ -1,5 +1,5 @@
import { PluginBillingStatus } from '@gauzy/contracts';
import { MultiORMEnum, TenantAwareCrudService } from '@gauzy/core';
import { MultiORMEnum, parseFindOptionsRelations, TenantAwareCrudService } from '@gauzy/core';
import { Injectable } from '@nestjs/common';
import { Between, LessThan, UpdateResult } from 'typeorm';
import {
@@ -176,7 +176,7 @@ export class PluginBillingService extends TenantAwareCrudService<PluginBilling>
status: PluginBillingStatus.PENDING,
dueDate: LessThan(now)
},
relations: ['subscription'],
relations: parseFindOptionsRelations(['subscription']),
order: { dueDate: 'ASC' }
});
}
@@ -1,4 +1,4 @@
import { TenantAwareCrudService } from '@gauzy/core';
import { parseFindOptionsRelations, TenantAwareCrudService } from '@gauzy/core';
import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { FindManyOptions, FindOptionsWhere } from 'typeorm';
import {
@@ -81,7 +81,7 @@ export class PluginSettingService extends TenantAwareCrudService<PluginSetting>
const result = await this.findAll({
where,
relations,
relations: parseFindOptionsRelations(relations),
order: {
category: {
name: 'ASC'
@@ -113,7 +113,7 @@ export class PluginSettingService extends TenantAwareCrudService<PluginSetting>
const result = await this.findAll({
where,
relations,
relations: parseFindOptionsRelations(relations),
order: {
category: {
name: 'ASC'
@@ -172,7 +172,7 @@ export class PluginSettingService extends TenantAwareCrudService<PluginSetting>
const result = await this.findAll({
where,
relations,
relations: parseFindOptionsRelations(relations),
order: { order: 'ASC', key: 'ASC' }
} as FindManyOptions);
@@ -1,5 +1,5 @@
import { ID, PluginSubscriptionType } from '@gauzy/contracts';
import { CrudService, MultiORMEnum } from '@gauzy/core';
import { CrudService, MultiORMEnum, parseFindOptionsRelations } from '@gauzy/core';
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { FindManyOptions, FindOneOptions, MoreThan } from 'typeorm';
import {
@@ -144,7 +144,7 @@ export class PluginSubscriptionPlanService extends CrudService<PluginSubscriptio
pluginId,
...(onlyActive && { isActive: true })
},
relations,
relations: parseFindOptionsRelations(relations),
order: {
sortOrder: 'ASC',
price: 'ASC'
@@ -172,7 +172,7 @@ export class PluginSubscriptionPlanService extends CrudService<PluginSubscriptio
...(pluginId && { pluginId }),
...(type && { type })
},
relations,
relations: parseFindOptionsRelations(relations),
order: {
sortOrder: 'ASC',
price: 'ASC'
@@ -347,7 +347,7 @@ export class PluginSubscriptionPlanService extends CrudService<PluginSubscriptio
try {
const queryOptions: FindOneOptions<PluginSubscriptionPlan> = {
where: { id },
relations
relations: parseFindOptionsRelations(relations)
};
const { success, record: plan } = await this.findOneOrFailByOptions(queryOptions);
@@ -1,5 +1,5 @@
import { IPagination, PluginScope } from '@gauzy/contracts';
import { RequestContext, TenantAwareCrudService } from '@gauzy/core';
import { parseFindOptionsRelations, RequestContext, TenantAwareCrudService } from '@gauzy/core';
import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { FindManyOptions, FindOptionsWhere } from 'typeorm';
import { IPluginTenant } from '../../shared/models/plugin-tenant.model';
@@ -133,7 +133,7 @@ export class PluginTenantService extends TenantAwareCrudService<PluginTenant> {
const result = await this.findAll({
where: { pluginId },
relations,
relations: parseFindOptionsRelations(relations),
order: { createdAt: 'DESC' },
...(skip !== undefined && { skip }),
...(take !== undefined && { take })
@@ -168,7 +168,7 @@ export class PluginTenantService extends TenantAwareCrudService<PluginTenant> {
const result = await this.findAll({
where,
relations,
relations: parseFindOptionsRelations(relations),
order: { createdAt: 'DESC' },
...(skip !== undefined && { skip }),
...(take !== undefined && { take })
@@ -1,9 +1,9 @@
import { IPlugin } from '@gauzy/contracts';
import { LegacyFindOneOptions } from '@gauzy/core';
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsArray, IsBoolean, IsObject, IsOptional } from 'class-validator';
import { FindOneOptions } from 'typeorm';
export class PluginQueryOptions implements FindOneOptions<IPlugin> {
export class PluginQueryOptions implements LegacyFindOneOptions<IPlugin> {
@ApiPropertyOptional({
description: 'Conditions to find a specific plugin',
type: () => Object
+2
View File
@@ -0,0 +1,2 @@
# Keep this directory tracked so Docker `COPY patches ./patches` and `patch-package` still
# work when there are no active patches. Add real `*.patch` files here as needed.
-94
View File
@@ -1,94 +0,0 @@
diff --git a/node_modules/typeorm/find-options/FindOneOptions.d.ts b/node_modules/typeorm/find-options/FindOneOptions.d.ts
index 2e19a6a..4896a8d 100644
--- a/node_modules/typeorm/find-options/FindOneOptions.d.ts
+++ b/node_modules/typeorm/find-options/FindOneOptions.d.ts
@@ -15,7 +15,7 @@ export interface FindOneOptions<Entity = any> {
/**
* Specifies what columns should be retrieved.
*/
- select?: FindOptionsSelect<Entity>;
+ select?: FindOptionsSelect<Entity> | string[];
/**
* Simple condition that should be applied to match entities.
*/
@@ -23,7 +23,7 @@ export interface FindOneOptions<Entity = any> {
/**
* Indicates what relations of entity should be loaded (simplified left join form).
*/
- relations?: FindOptionsRelations<Entity>;
+ relations?: FindOptionsRelations<Entity> | string[];
/**
* Specifies how relations must be loaded - using "joins" or separate queries.
* If you are loading too much data with nested joins it's better to load relations
diff --git a/node_modules/typeorm/find-options/FindOptionsUtils.js b/node_modules/typeorm/find-options/FindOptionsUtils.js
index d5c79d8..6966629 100644
--- a/node_modules/typeorm/find-options/FindOptionsUtils.js
+++ b/node_modules/typeorm/find-options/FindOptionsUtils.js
@@ -37,9 +37,29 @@ class FindOptionsUtils {
typeof options === "object" &&
"select" in options &&
Array.isArray(options.select)) {
- throw new error_2.TypeORMError(`String-array "select" syntax has been removed. ` +
- `Use object syntax instead, e.g. select: { id: true, name: true }. ` +
- `See the v1 migration guide for details.`);
+ // Ever Gauzy compat shim: convert the legacy string-array `select` syntax (removed in
+ // v1.0) to the object form v0.3 accepted, instead of rejecting. e.g.
+ // ["id","name","role.name"] => { id: true, name: true, role: { name: true } }
+ const obj = {};
+ for (const col of options.select) {
+ if (typeof col !== "string" || col.length === 0)
+ continue;
+ let cur = obj;
+ const parts = col.split(".");
+ for (let i = 0; i < parts.length; i++) {
+ const p = parts[i];
+ if (i === parts.length - 1) {
+ if (cur[p] === undefined)
+ cur[p] = true;
+ }
+ else {
+ if (cur[p] === true || cur[p] === undefined)
+ cur[p] = {};
+ cur = cur[p];
+ }
+ }
+ }
+ options.select = obj;
}
}
/**
@@ -53,9 +73,31 @@ class FindOptionsUtils {
typeof options === "object" &&
"relations" in options &&
Array.isArray(options.relations)) {
- throw new error_2.TypeORMError(`String-array "relations" syntax has been removed. ` +
- `Use object syntax instead, e.g. relations: { profile: true, posts: true }. ` +
- `See the v1 migration guide for details.`);
+ // Ever Gauzy compat shim: instead of rejecting the legacy string-array `relations`
+ // syntax (removed in typeorm v1.0), convert it to the object form that v0.3 accepted.
+ // The codebase passes dynamic `string[]` relations in many places; converting here
+ // keeps them working at runtime. e.g. ["role","tenant.featureOrganizations"] =>
+ // { role: true, tenant: { featureOrganizations: true } }
+ const obj = {};
+ for (const rel of options.relations) {
+ if (typeof rel !== "string" || rel.length === 0)
+ continue;
+ let cur = obj;
+ const parts = rel.split(".");
+ for (let i = 0; i < parts.length; i++) {
+ const p = parts[i];
+ if (i === parts.length - 1) {
+ if (cur[p] === undefined)
+ cur[p] = true;
+ }
+ else {
+ if (cur[p] === true || cur[p] === undefined)
+ cur[p] = {};
+ cur = cur[p];
+ }
+ }
+ }
+ options.relations = obj;
}
}
/**