diff --git a/packages/core/src/lib/activity-log/activity-log.service.ts b/packages/core/src/lib/activity-log/activity-log.service.ts index b1d91645e5..8d1c9a513b 100644 --- a/packages/core/src/lib/activity-log/activity-log.service.ts +++ b/packages/core/src/lib/activity-log/activity-log.service.ts @@ -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 { // Ensure that filters are properly defined const queryOptions: FindManyOptions = { where, - ...(relations && { relations }), + ...(relations && { relations: parseFindOptionsRelations(relations) }), take: take, skip: take * (skip - 1) // Calculate offset (skip) based on validated skip value }; diff --git a/packages/core/src/lib/auth/auth.service.ts b/packages/core/src/lib/auth/auth.service.ts index 0ca7aa6590..c4daa8b0c1 100644 --- a/packages/core/src/lib/auth/auth.service.ts +++ b/packages/core/src/lib/auth/auth.service.ts @@ -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(?.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(?.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(?.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; } diff --git a/packages/core/src/lib/broadcast/broadcast.service.ts b/packages/core/src/lib/broadcast/broadcast.service.ts index d0f16021fb..071a012c84 100644 --- a/packages/core/src/lib/broadcast/broadcast.service.ts +++ b/packages/core/src/lib/broadcast/broadcast.service.ts @@ -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 { // 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 { // Retrieve broadcasts matching base criteria with pagination const queryOptions: FindManyOptions = { where, - relations, + relations: parseFindOptionsRelations(relations), order: { publishedAt: 'DESC' }, ...(take !== undefined && { take }), ...(skip !== undefined && { skip }) @@ -340,7 +343,7 @@ export class BroadcastService extends TenantAwareCrudService { organizationId: organizationId || RequestContext.currentOrganizationId() }) }, - relations: [relationName] + relations: parseFindOptionsRelations([relationName]) }); if (!entityWithMembers) { @@ -522,7 +525,7 @@ export class BroadcastService extends TenantAwareCrudService { const entityWithMembers = await repository.findOne({ where: { id: entityId, tenantId, organizationId }, - relations: [relationName] + relations: parseFindOptionsRelations([relationName]) }); if (!entityWithMembers) return []; diff --git a/packages/core/src/lib/core/crud/crud.service.ts b/packages/core/src/lib/core/crud/crud.service.ts index 20ef30391d..2e87477e4f 100644 --- a/packages/core/src/lib/core/crud/crud.service.ts +++ b/packages/core/src/lib/core/crud/crud.service.ts @@ -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 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); + [items, total] = await this.typeOrmRepository.findAndCount( + parseTypeORMFindOptions(options as FindManyOptions) + ); break; default: throw new Error(`Not implemented for ${this.ormType}`); @@ -165,7 +176,7 @@ export abstract class CrudService 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); + return await this.typeOrmRepository.find(parseTypeORMFindOptions(options as FindManyOptions)); default: throw new Error(`Not implemented for ${this.ormType}`); } @@ -179,7 +190,7 @@ export abstract class CrudService implements ICrudService< * @param options * @returns */ - public async paginate(options?: FindManyOptions): Promise> { + public async paginate(options?: IFindManyOptions): Promise> { try { let total: number; let items: T[]; @@ -191,19 +202,23 @@ export abstract class CrudService 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); [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 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); break; @@ -285,7 +302,9 @@ export abstract class CrudService implements ICrudService< record = (await this.mikroOrmRepository.findOneOrFail(where, mikroOptions)) as any; break; case MultiORMEnum.TypeORM: - record = await this.typeOrmRepository.findOneOrFail(options as FindOneOptions); + record = await this.typeOrmRepository.findOneOrFail( + parseTypeORMFindOptions(options as FindOneOptions) + ); break; default: throw new Error(`Not implemented for ${this.ormType}`); @@ -363,8 +382,10 @@ export abstract class CrudService 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); @@ -395,7 +416,7 @@ export abstract class CrudService implements ICrudService< record = (await this.mikroOrmRepository.findOne(where, mikroOptions)) as any; break; case MultiORMEnum.TypeORM: - record = await this.typeOrmRepository.findOne(options as FindOneOptions); + record = await this.typeOrmRepository.findOne(parseTypeORMFindOptions(options as FindOneOptions)); break; default: throw new Error(`Not implemented for ${this.ormType}`); diff --git a/packages/core/src/lib/core/crud/icrud.service.ts b/packages/core/src/lib/core/crud/icrud.service.ts index e9a1049b95..b66594ddb3 100644 --- a/packages/core/src/lib/core/crud/icrud.service.ts +++ b/packages/core/src/lib/core/crud/icrud.service.ts @@ -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 { export type IMikroOptions = { where?: MikroFilterQuery } & MikroFindOptions; -export type ICountOptions = FindManyOptions | IMikroOptions; +export type ICountOptions = LegacyFindManyOptions | IMikroOptions; export type ICountByOptions = FindOptionsWhere | MikroFilterQuery; -export type IFindManyOptions = FindManyOptions | IMikroOptions; +export type IFindManyOptions = LegacyFindManyOptions | IMikroOptions; -export type IFindOneOptions = FindOneOptions | IMikroOptions; +export type IFindOneOptions = LegacyFindOneOptions | IMikroOptions; -export type IFindOrFailOptions = FindOneOptions | IMikroOptions; +export type IFindOrFailOptions = LegacyFindOneOptions | IMikroOptions; export type IFindWhereOptions = FindOptionsWhere | MikroFilterQuery; diff --git a/packages/core/src/lib/core/crud/tenant-aware-crud.service.ts b/packages/core/src/lib/core/crud/tenant-aware-crud.service.ts index db8f5d989b..12c310a9d8 100644 --- a/packages/core/src/lib/core/crud/tenant-aware-crud.service.ts +++ b/packages/core/src/lib/core/crud/tenant-aware-crud.service.ts @@ -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 * @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): FindOneOptions { + private findOneWithTenant(filter?: LegacyFindOneOptions): LegacyFindOneOptions { const user = RequestContext.currentUser(); if (!user || !user.tenantId) { return filter; @@ -194,7 +195,7 @@ export abstract class TenantAwareCrudService * @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): FindManyOptions { + private findManyWithTenant(filter?: LegacyFindManyOptions): LegacyFindManyOptions { const user = RequestContext.currentUser(); if (!user || !user.tenantId) { return filter; @@ -226,7 +227,7 @@ export abstract class TenantAwareCrudService * @param options * @returns */ - public async count(options?: FindManyOptions): Promise { + public async count(options?: LegacyFindManyOptions): Promise { return await super.count(this.findManyWithTenant(options)); } @@ -253,7 +254,7 @@ export abstract class TenantAwareCrudService * @param filter * @returns */ - public async findAll(filter?: FindManyOptions): Promise> { + public async findAll(filter?: LegacyFindManyOptions): Promise> { return await super.findAll(this.findManyWithTenant(filter)); } @@ -263,7 +264,7 @@ export abstract class TenantAwareCrudService * @param filter * @returns */ - public async find(filter?: FindManyOptions): Promise { + public async find(filter?: LegacyFindManyOptions): Promise { return await super.find(this.findManyWithTenant(filter)); } @@ -275,7 +276,7 @@ export abstract class TenantAwareCrudService * @param filter * @returns */ - public async paginate(filter?: FindManyOptions): Promise> { + public async paginate(filter?: LegacyFindManyOptions): Promise> { return await super.paginate(this.findManyWithTenant(filter)); } @@ -293,7 +294,7 @@ export abstract class TenantAwareCrudService * @param options * @returns */ - public async findOneOrFailByIdString(id: ID, options?: FindOneOptions): Promise> { + public async findOneOrFailByIdString(id: ID, options?: LegacyFindOneOptions): Promise> { return await super.findOneOrFailByIdString(id, this.findOneWithTenant(options)); } @@ -304,7 +305,7 @@ export abstract class TenantAwareCrudService * @param options * @returns */ - public async findOneOrFailByOptions(options?: FindOneOptions): Promise> { + public async findOneOrFailByOptions(options?: LegacyFindOneOptions): Promise> { return await super.findOneOrFailByOptions(this.findOneWithTenant(options)); } @@ -336,7 +337,7 @@ export abstract class TenantAwareCrudService * @param options * @returns */ - public async findOneByIdString(id: ID, options?: FindOneOptions): Promise { + public async findOneByIdString(id: ID, options?: LegacyFindOneOptions): Promise { return await super.findOneByIdString(id, this.findOneWithTenant(options)); } @@ -347,7 +348,7 @@ export abstract class TenantAwareCrudService * @param options * @returns */ - public async findOneByOptions(options: FindOneOptions): Promise { + public async findOneByOptions(options: LegacyFindOneOptions): Promise { return await super.findOneByOptions(this.findOneWithTenant(options)); } @@ -517,7 +518,7 @@ export abstract class TenantAwareCrudService * @param options - Additional options for querying, such as extra conditions or query parameters. * @returns {Promise} - The result of the delete operation. */ - public async delete(criteria: string | FindOptionsWhere, options?: FindOneOptions): Promise { + public async delete(criteria: string | FindOptionsWhere, options?: LegacyFindOneOptions): Promise { try { // Merge additional where conditions from options into criteria if needed let where: FindOptionsWhere = @@ -588,7 +589,7 @@ export abstract class TenantAwareCrudService */ public async softDelete( criteria: string | number | FindOptionsWhere, - options?: FindOneOptions + options?: LegacyFindOneOptions ): Promise { try { let record: T | null; diff --git a/packages/core/src/lib/core/find-options.utils.spec.ts b/packages/core/src/lib/core/find-options.utils.spec.ts new file mode 100644 index 0000000000..200a844a6e --- /dev/null +++ b/packages/core/src/lib/core/find-options.utils.spec.ts @@ -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(); + }); +}); diff --git a/packages/core/src/lib/core/orm/query-builder/iquery-builder.ts b/packages/core/src/lib/core/orm/query-builder/iquery-builder.ts index f9643aa180..2360387131 100644 --- a/packages/core/src/lib/core/orm/query-builder/iquery-builder.ts +++ b/packages/core/src/lib/core/orm/query-builder/iquery-builder.ts @@ -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 { alias: string; @@ -7,7 +8,7 @@ export interface IQueryBuilder { getQueryBuilder(): SelectQueryBuilder | QueryBuilder; clone(): this; subQuery(): IQueryBuilder; - setFindOptions(findOptions: FindManyOptions): this; + setFindOptions(findOptions: LegacyFindManyOptions): this; select(selection: string, selectionAliasName?: string): this; addSelect(selection: string, selectionAliasName?: string): this; from( diff --git a/packages/core/src/lib/core/orm/query-builder/mikro-orm-query-builder.ts b/packages/core/src/lib/core/orm/query-builder/mikro-orm-query-builder.ts index 340ed9f9f6..4592c2e61e 100644 --- a/packages/core/src/lib/core/orm/query-builder/mikro-orm-query-builder.ts +++ b/packages/core/src/lib/core/orm/query-builder/mikro-orm-query-builder.ts @@ -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 implements IQueryBuilder { @@ -46,7 +47,9 @@ export class MikroOrmQueryBuilder implements IQueryBuilde return qb; } - setFindOptions(findOptions: FindManyOptions): this { + setFindOptions(findOptions: LegacyFindManyOptions): 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 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 implements IQueryBuilde } addFrom(_entityTarget: ((qb: QueryBuilder) => QueryBuilder) | EntityTarget, _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 { diff --git a/packages/core/src/lib/core/orm/query-builder/typeorm-query-builder.ts b/packages/core/src/lib/core/orm/query-builder/typeorm-query-builder.ts index 6474b0d721..3aada5187e 100644 --- a/packages/core/src/lib/core/orm/query-builder/typeorm-query-builder.ts +++ b/packages/core/src/lib/core/orm/query-builder/typeorm-query-builder.ts @@ -1,5 +1,6 @@ import { EntityTarget, FindManyOptions, Repository, SelectQueryBuilder } from 'typeorm'; +import { LegacyFindManyOptions, parseTypeORMFindOptions } from '../../utils'; import { IQueryBuilder } from './iquery-builder'; export class TypeOrmQueryBuilder implements IQueryBuilder { @@ -40,8 +41,10 @@ export class TypeOrmQueryBuilder implements IQueryBuilder return this; } - setFindOptions(findOptions: FindManyOptions) { - this.qb.setFindOptions(findOptions); + setFindOptions(findOptions: LegacyFindManyOptions) { + // 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); return this; } diff --git a/packages/core/src/lib/core/utils.ts b/packages/core/src/lib/core/utils.ts index 20b262f9d1..4e8f763f3d 100644 --- a/packages/core/src/lib/core/utils.ts +++ b/packages/core/src/lib/core/utils.ts @@ -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 = Omit, 'relations' | 'select'> & { + relations?: FindManyOptions['relations'] | string[]; + select?: FindManyOptions['select'] | string[]; +}; + +/** + * TypeORM `FindOneOptions` widened to also accept the legacy string-array `relations`/`select` + * syntax. See {@link LegacyFindManyOptions}. + */ +export type LegacyFindOneOptions = Omit, 'relations' | 'select'> & { + relations?: FindOneOptions['relations'] | string[]; + select?: FindOneOptions['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 { + let result: Record = {}; + + 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( + relations: string[] | FindOptionsRelations | undefined +): FindOptionsRelations | undefined { + // NOTE: the parameter is intentionally `FindOptionsRelations` (not `FindOptionsRelations`) + // so `T` is inferred from the assignment context (the field being populated), never from the + // argument. Inferring `T` from a `string[]` argument makes `FindOptionsRelations` resolve to a + // bogus array type, which would then reject at every call site. + if (Array.isArray(relations)) { + return stringArrayToFindOptionsObject(relations) as FindOptionsRelations; + } + return relations as FindOptionsRelations | 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( + select: string[] | FindOptionsSelect | undefined +): FindOptionsSelect | undefined { + // See parseFindOptionsRelations: parameter is `FindOptionsSelect` so `T` is inferred from the + // assignment context rather than a `string[]` argument. + if (Array.isArray(select)) { + return stringArrayToFindOptionsObject(select) as FindOptionsSelect; + } + return select as FindOptionsSelect | 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(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(tenantId: any, where: MikroFilterQue * @param options - TypeORM's FindManyOptions. * @returns An object with MikroORM's where and options. */ -export function parseTypeORMFindToMikroOrm(options: FindManyOptions): { +export function parseTypeORMFindToMikroOrm(options: LegacyFindManyOptions): { where: MikroFilterQuery; mikroOptions: MikroORMFindOptions; } { + // 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 = { disableIdentityMap: true, populate: [] diff --git a/packages/core/src/lib/expense/queries/handlers/expense.find-split-expense.handler.ts b/packages/core/src/lib/expense/queries/handlers/expense.find-split-expense.handler.ts index 5a558dbf67..f789ad15cd 100644 --- a/packages/core/src/lib/expense/queries/handlers/expense.find-split-expense.handler.ts +++ b/packages/core/src/lib/expense/queries/handlers/expense.find-split-expense.handler.ts @@ -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 ); diff --git a/packages/core/src/lib/organization-project-module/organization-project-module.service.ts b/packages/core/src/lib/organization-project-module/organization-project-module.service.ts index b692bea859..c102d8df3e 100644 --- a/packages/core/src/lib/organization-project-module/organization-project-module.service.ts +++ b/packages/core/src/lib/organization-project-module/organization-project-module.service.ts @@ -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 { }; if (relations.length) { - queryOptions.relations = relations; + queryOptions.relations = parseFindOptionsRelations(relations); } try { diff --git a/packages/core/src/lib/public-share/employee/public-employee.service.ts b/packages/core/src/lib/public-share/employee/public-employee.service.ts index b9488b5338..a34c4a5c12 100644 --- a/packages/core/src/lib/public-share/employee/public-employee.service.ts +++ b/packages/core/src/lib/public-share/employee/public-employee.service.ts @@ -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> { 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(?.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, relations: string[]): Promise { 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(?.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 }); diff --git a/packages/core/src/lib/public-share/invoice/public-invoice.service.ts b/packages/core/src/lib/public-share/invoice/public-invoice.service.ts index 9c87d9a3c1..ce93fa4026 100644 --- a/packages/core/src/lib/public-share/invoice/public-invoice.service.ts +++ b/packages/core/src/lib/public-share/invoice/public-invoice.service.ts @@ -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(); diff --git a/packages/core/src/lib/public-share/organization/public-organization.service.ts b/packages/core/src/lib/public-share/organization/public-organization.service.ts index 9b41d35be6..278d9ad7c4 100644 --- a/packages/core/src/lib/public-share/organization/public-organization.service.ts +++ b/packages/core/src/lib/public-share/organization/public-organization.service.ts @@ -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(?.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 }); diff --git a/packages/core/src/lib/public-share/team/public-team.service.ts b/packages/core/src/lib/public-share/team/public-team.service.ts index 36adaa2dc8..9d7487e5e3 100644 --- a/packages/core/src/lib/public-share/team/public-team.service.ts +++ b/packages/core/src/lib/public-share/team/public-team.service.ts @@ -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 { diff --git a/packages/core/src/lib/reports/report.service.ts b/packages/core/src/lib/reports/report.service.ts index b227a2189a..f04b9a48d9 100644 --- a/packages/core/src/lib/reports/report.service.ts +++ b/packages/core/src/lib/reports/report.service.ts @@ -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 { // 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', diff --git a/packages/core/src/lib/request-approval/request-approval.service.ts b/packages/core/src/lib/request-approval/request-approval.service.ts index cd82edcabd..90d5a512c0 100644 --- a/packages/core/src/lib/request-approval/request-approval.service.ts +++ b/packages/core/src/lib/request-approval/request-approval.service.ts @@ -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 0) { - query.setFindOptions({ relations }); + query.setFindOptions({ relations: parseFindOptionsRelations(relations) }); } const [items, total] = await query @@ -209,10 +209,9 @@ export class RequestApprovalService extends TenantAwareCrudService?.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; } diff --git a/packages/core/src/lib/tags/tag.service.ts b/packages/core/src/lib/tags/tag.service.ts index a020acdc69..2fe4f28ef0 100644 --- a/packages/core/src/lib/tags/tag.service.ts +++ b/packages/core/src/lib/tags/tag.service.ts @@ -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 { // 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 { 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 diff --git a/packages/core/src/lib/tasks/daily-plan/daily-plan.service.ts b/packages/core/src/lib/tasks/daily-plan/daily-plan.service.ts index ee27643ac7..a063368d5d 100644 --- a/packages/core/src/lib/tasks/daily-plan/daily-plan.service.ts +++ b/packages/core/src/lib/tasks/daily-plan/daily-plan.service.ts @@ -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 { }), ...(isNotEmpty(options) && isNotEmpty(options.relations) && { - relations: options.relations + relations: parseFindOptionsRelations(options.relations) }) }); @@ -235,7 +235,7 @@ export class DailyPlanService extends TenantAwareCrudService { query.setFindOptions({ where: isNotEmpty(where) && where, - relations: isNotEmpty(relations) && relations + relations: isNotEmpty(relations) && parseFindOptionsRelations(relations) }); // Filter conditions diff --git a/packages/core/src/lib/tasks/task.service.ts b/packages/core/src/lib/tasks/task.service.ts index 5ac8fa71ba..70a5c8a76c 100644 --- a/packages/core/src/lib/tasks/task.service.ts +++ b/packages/core/src/lib/tasks/task.service.ts @@ -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 { }); } query.setFindOptions({ - ...(options.relations ? { relations: options.relations } : {}) + ...(options.relations ? { relations: parseFindOptionsRelations(options.relations) } : {}) }); } @@ -581,7 +581,7 @@ export class TaskService extends TenantAwareCrudService { }), ...(isNotEmpty(options) && isNotEmpty(options.relations) && { - relations: options.relations + relations: parseFindOptionsRelations(options.relations) }) }); @@ -714,8 +714,8 @@ export class TaskService extends TenantAwareCrudService { }); } 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 { // 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 { }; // Define find options - const findOptions: FindManyOptions = { where, ...(relations && { relations }) }; + const findOptions: FindManyOptions = { 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 { // Check if relations were provided and include them query.setFindOptions({ - ...(relations ? { relations } : {}) + ...(relations ? { relations: parseFindOptionsRelations(relations) } : {}) }); const [items, total] = await query.getManyAndCount(); diff --git a/packages/core/src/lib/time-off-request/time-off-request.service.ts b/packages/core/src/lib/time-off-request/time-off-request.service.ts index aff16041d9..989f1bcd39 100644 --- a/packages/core/src/lib/time-off-request/time-off-request.service.ts +++ b/packages/core/src/lib/time-off-request/time-off-request.service.ts @@ -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) => { diff --git a/packages/core/src/lib/time-tracking/time-log/time-log.service.ts b/packages/core/src/lib/time-tracking/time-log/time-log.service.ts index 6364f69d80..ce8e14e13d 100644 --- a/packages/core/src/lib/time-tracking/time-log/time-log.service.ts +++ b/packages/core/src/lib/time-tracking/time-log/time-log.service.ts @@ -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 { } } }, - relations: [...(request.relations ? request.relations : [])], + relations: parseFindOptionsRelations([...(request.relations ? request.relations : [])]), order: { // Order results by the 'startedAt' field in ascending order startedAt: 'ASC' diff --git a/packages/core/src/lib/time-tracking/time-slot/time-slot.service.ts b/packages/core/src/lib/time-tracking/time-slot/time-slot.service.ts index 0bfc4592c6..f5fe620be4 100644 --- a/packages/core/src/lib/time-tracking/time-slot/time-slot.service.ts +++ b/packages/core/src/lib/time-tracking/time-slot/time-slot.service.ts @@ -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 { } }, // Spread relations if provided, otherwise an empty array - relations: request.relations || [] + relations: parseFindOptionsRelations(request.relations || []) }); // Add where conditions to the query diff --git a/packages/core/src/lib/time-tracking/timer/timer.service.ts b/packages/core/src/lib/time-tracking/timer/timer.service.ts index a95c688637..c1530e48fb 100644 --- a/packages/core/src/lib/time-tracking/timer/timer.service.ts +++ b/packages/core/src/lib/time-tracking/timer/timer.service.ts @@ -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()), diff --git a/packages/core/src/lib/time-tracking/timesheet/timesheet.service.ts b/packages/core/src/lib/time-tracking/timesheet/timesheet.service.ts index acfce3ad69..4e1b9499fb 100644 --- a/packages/core/src/lib/time-tracking/timesheet/timesheet.service.ts +++ b/packages/core/src/lib/time-tracking/timesheet/timesheet.service.ts @@ -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 { brandColor: true } }, - ...(request?.relations ? { relations: request.relations } : {}) + ...(request?.relations ? { relations: parseFindOptionsRelations(request.relations) } : {}) }); // Apply filters to the query diff --git a/packages/core/src/lib/user/user.service.ts b/packages/core/src/lib/user/user.service.ts index 38ab510535..c3b997112a 100644 --- a/packages/core/src/lib/user/user.service.ts +++ b/packages/core/src/lib/user/user.service.ts @@ -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 { // 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. diff --git a/packages/plugins/integration-upwork/src/lib/upwork.service.ts b/packages/plugins/integration-upwork/src/lib/upwork.service.ts index 1bbc4bece7..5030adfe36 100644 --- a/packages/plugins/integration-upwork/src/lib/upwork.service.ts +++ b/packages/plugins/integration-upwork/src/lib/upwork.service.ts @@ -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( diff --git a/packages/plugins/integration-zapier/src/lib/zapier.service.ts b/packages/plugins/integration-zapier/src/lib/zapier.service.ts index cbaa476b98..f27b22035a 100644 --- a/packages/plugins/integration-zapier/src/lib/zapier.service.ts +++ b/packages/plugins/integration-zapier/src/lib/zapier.service.ts @@ -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) { diff --git a/packages/plugins/job-search/src/lib/employee-job-preset/commands/handlers/save-employee-preset.handler.ts b/packages/plugins/job-search/src/lib/employee-job-preset/commands/handlers/save-employee-preset.handler.ts index d14e1c25e4..514b28741f 100644 --- a/packages/plugins/job-search/src/lib/employee-job-preset/commands/handlers/save-employee-preset.handler.ts +++ b/packages/plugins/job-search/src/lib/employee-job-preset/commands/handlers/save-employee-preset.handler.ts @@ -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 { // 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 diff --git a/packages/plugins/knowledge-base/src/lib/help-center-article/help-center-article.service.ts b/packages/plugins/knowledge-base/src/lib/help-center-article/help-center-article.service.ts index 04c62a4ee2..ee798d5614 100644 --- a/packages/plugins/knowledge-base/src/lib/help-center-article/help-center-article.service.ts +++ b/packages/plugins/knowledge-base/src/lib/help-center-article/help-center-article.service.ts @@ -12,6 +12,8 @@ import { } from 'typeorm'; import { MultiORMEnum, + parseFindOptionsRelations, + parseFindOptionsSelect, RequestContext, TenantAwareCrudService, BaseQueryDTO, @@ -141,8 +143,8 @@ export class HelpCenterArticleService extends TenantAwareCrudService = new Map(); diff --git a/packages/plugins/registry/src/lib/application/plugin-category/queries/get-plugin-categories.query.ts b/packages/plugins/registry/src/lib/application/plugin-category/queries/get-plugin-categories.query.ts index fd0e81cb01..cc0c105bd7 100644 --- a/packages/plugins/registry/src/lib/application/plugin-category/queries/get-plugin-categories.query.ts +++ b/packages/plugins/registry/src/lib/application/plugin-category/queries/get-plugin-categories.query.ts @@ -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) {} + constructor(public readonly options?: LegacyFindManyOptions) {} } diff --git a/packages/plugins/registry/src/lib/application/plugin-tenant/queries/handlers/get-all-plugin-tenants.handler.ts b/packages/plugins/registry/src/lib/application/plugin-tenant/queries/handlers/get-all-plugin-tenants.handler.ts index 2b3bbf7e83..3ea3c06909 100644 --- a/packages/plugins/registry/src/lib/application/plugin-tenant/queries/handlers/get-all-plugin-tenants.handler.ts +++ b/packages/plugins/registry/src/lib/application/plugin-tenant/queries/handlers/get-all-plugin-tenants.handler.ts @@ -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 { async findByPluginId(pluginId: string, relations: string[] = ['tag']): Promise { return this.find({ where: { pluginId }, - relations + relations: parseFindOptionsRelations(relations) }); } @@ -50,7 +51,7 @@ export class TypeOrmPluginTagRepository extends Repository { async findByTagId(tagId: string, relations: string[] = ['plugin']): Promise { return this.find({ where: { tagId }, - relations + relations: parseFindOptionsRelations(relations) }); } diff --git a/packages/plugins/registry/src/lib/domain/services/plugin-billing.service.ts b/packages/plugins/registry/src/lib/domain/services/plugin-billing.service.ts index a53c24022e..5fdbd45307 100644 --- a/packages/plugins/registry/src/lib/domain/services/plugin-billing.service.ts +++ b/packages/plugins/registry/src/lib/domain/services/plugin-billing.service.ts @@ -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 status: PluginBillingStatus.PENDING, dueDate: LessThan(now) }, - relations: ['subscription'], + relations: parseFindOptionsRelations(['subscription']), order: { dueDate: 'ASC' } }); } diff --git a/packages/plugins/registry/src/lib/domain/services/plugin-setting.service.ts b/packages/plugins/registry/src/lib/domain/services/plugin-setting.service.ts index 9538217138..986599eaea 100644 --- a/packages/plugins/registry/src/lib/domain/services/plugin-setting.service.ts +++ b/packages/plugins/registry/src/lib/domain/services/plugin-setting.service.ts @@ -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 const result = await this.findAll({ where, - relations, + relations: parseFindOptionsRelations(relations), order: { category: { name: 'ASC' @@ -113,7 +113,7 @@ export class PluginSettingService extends TenantAwareCrudService const result = await this.findAll({ where, - relations, + relations: parseFindOptionsRelations(relations), order: { category: { name: 'ASC' @@ -172,7 +172,7 @@ export class PluginSettingService extends TenantAwareCrudService const result = await this.findAll({ where, - relations, + relations: parseFindOptionsRelations(relations), order: { order: 'ASC', key: 'ASC' } } as FindManyOptions); diff --git a/packages/plugins/registry/src/lib/domain/services/plugin-subscription-plan.service.ts b/packages/plugins/registry/src/lib/domain/services/plugin-subscription-plan.service.ts index afaa2b0057..86354691ee 100644 --- a/packages/plugins/registry/src/lib/domain/services/plugin-subscription-plan.service.ts +++ b/packages/plugins/registry/src/lib/domain/services/plugin-subscription-plan.service.ts @@ -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 = { where: { id }, - relations + relations: parseFindOptionsRelations(relations) }; const { success, record: plan } = await this.findOneOrFailByOptions(queryOptions); diff --git a/packages/plugins/registry/src/lib/domain/services/plugin-tenant.service.ts b/packages/plugins/registry/src/lib/domain/services/plugin-tenant.service.ts index 6d981cea23..0ddde298b6 100644 --- a/packages/plugins/registry/src/lib/domain/services/plugin-tenant.service.ts +++ b/packages/plugins/registry/src/lib/domain/services/plugin-tenant.service.ts @@ -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 { 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 { const result = await this.findAll({ where, - relations, + relations: parseFindOptionsRelations(relations), order: { createdAt: 'DESC' }, ...(skip !== undefined && { skip }), ...(take !== undefined && { take }) diff --git a/packages/plugins/registry/src/lib/shared/dto/plugin-query-options.dto.ts b/packages/plugins/registry/src/lib/shared/dto/plugin-query-options.dto.ts index d2d5cb8122..5d1c17e6e5 100644 --- a/packages/plugins/registry/src/lib/shared/dto/plugin-query-options.dto.ts +++ b/packages/plugins/registry/src/lib/shared/dto/plugin-query-options.dto.ts @@ -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 { +export class PluginQueryOptions implements LegacyFindOneOptions { @ApiPropertyOptional({ description: 'Conditions to find a specific plugin', type: () => Object diff --git a/patches/.gitkeep b/patches/.gitkeep new file mode 100644 index 0000000000..f0c1c47eac --- /dev/null +++ b/patches/.gitkeep @@ -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. diff --git a/patches/typeorm+1.0.0.patch b/patches/typeorm+1.0.0.patch deleted file mode 100644 index 5253fea723..0000000000 --- a/patches/typeorm+1.0.0.patch +++ /dev/null @@ -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 { - /** - * Specifies what columns should be retrieved. - */ -- select?: FindOptionsSelect; -+ select?: FindOptionsSelect | string[]; - /** - * Simple condition that should be applied to match entities. - */ -@@ -23,7 +23,7 @@ export interface FindOneOptions { - /** - * Indicates what relations of entity should be loaded (simplified left join form). - */ -- relations?: FindOptionsRelations; -+ relations?: FindOptionsRelations | 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; - } - } - /**