Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code fe2c79ceb5 fix: strip deletedAt from connectedAccount where clause in findMany core path
https://sonarly.com/issue/17951?type=bug

The `MessageChannelDataAccessService.findMany` method passes a `deletedAt: IsNull()` filter on the `connectedAccount` relation to the core TypeORM repository, but the new `ConnectedAccountEntity` lacks a `deletedAt` column, causing an `EntityPropertyNotFoundError`.

Fix: Three changes in `MessageChannelDataAccessService`:

**1. `toCoreWhere` — always strip `connectedAccount` from the where clause**

Previously, after extracting `accountOwnerId`, any remaining `connectedAccount` properties (like `deletedAt: IsNull()`) were kept as a nested relation filter. But `ConnectedAccountEntity` in the core schema doesn't have `deletedAt` (or `accountOwnerId`), so TypeORM throws `EntityPropertyNotFoundError`. The fix unconditionally deletes `coreWhere.connectedAccount` after resolving `accountOwnerId` to `connectedAccountId`.

**2. `find` — route through `toCoreWhere`**

The `find` method was spreading `where` directly into the core query without processing `connectedAccount` filters. This would fail for any caller passing `connectedAccount` relation filters (e.g., the blocklist reimport job). Now routes through `toCoreWhere` like the other methods.

**3. `findMany` — apply relation-stripping pattern from `findOne`**

The `findOne` method already correctly handles the migrated path: it strips `connectedAccount` and `messageFolders` from `relations`, strips `connectedAccount` from `select`, queries the core repository, then resolves connected accounts and message folders separately via their data access services. The `findMany` method was missing this logic — it passed all options (including `relations: ['connectedAccount']` and nested `connectedAccount` select/where) directly to the core repository. The fix applies the identical pattern, with the optimization of batch-loading connected accounts via a Map to avoid N+1 queries.
2026-03-24 14:58:04 +00:00
@@ -50,8 +50,7 @@ export class MessageChannelDataAccessService {
};
if ('accountOwnerId' in connectedAccountWhere) {
const { accountOwnerId, ...restConnectedAccount } =
connectedAccountWhere;
const { accountOwnerId } = connectedAccountWhere;
const resolvedConnectedAccounts =
await this.connectedAccountDataAccessService.find(workspaceId, {
@@ -63,13 +62,9 @@ export class MessageChannelDataAccessService {
} else {
coreWhere.connectedAccountId = '00000000-0000-0000-0000-000000000000';
}
if (Object.keys(restConnectedAccount).length > 0) {
coreWhere.connectedAccount = restConnectedAccount;
} else {
delete coreWhere.connectedAccount;
}
}
delete coreWhere.connectedAccount;
}
return coreWhere;
@@ -157,11 +152,13 @@ export class MessageChannelDataAccessService {
where?: FindOptionsWhere<MessageChannelWorkspaceEntity>,
): Promise<MessageChannelWorkspaceEntity[]> {
if (await this.isMigrated(workspaceId)) {
const coreWhere = await this.toCoreWhere(
workspaceId,
(where ?? {}) as Record<string, unknown>,
);
return this.coreRepository.find({
where: {
...(where as Record<string, unknown>),
workspaceId,
} as FindOptionsWhere<MessageChannelEntity>,
where: coreWhere as FindOptionsWhere<MessageChannelEntity>,
}) as unknown as Promise<MessageChannelWorkspaceEntity[]>;
}
@@ -177,41 +174,111 @@ export class MessageChannelDataAccessService {
if (await this.isMigrated(workspaceId)) {
const baseWhere = options.where;
if (!baseWhere) {
return this.coreRepository.find({
...options,
where: { workspaceId },
} as FindManyOptions<MessageChannelEntity>) as unknown as Promise<
MessageChannelWorkspaceEntity[]
>;
}
const requestedRelations =
(options.relations as string[] | undefined)?.slice() ?? [];
if (Array.isArray(baseWhere)) {
const needsConnectedAccount =
requestedRelations.includes('connectedAccount');
const needsMessageFolders =
requestedRelations.includes('messageFolders');
const coreRelations = requestedRelations.filter(
(r) => r !== 'connectedAccount' && r !== 'messageFolders',
);
const coreSelect = options.select
? (({ connectedAccount, ...rest }) => rest)(
options.select as Record<string, unknown>,
)
: undefined;
const coreOptions = {
...options,
relations: coreRelations,
...(coreSelect ? { select: coreSelect } : {}),
};
let results: MessageChannelEntity[];
if (!baseWhere) {
results = await this.coreRepository.find({
...coreOptions,
where: { workspaceId },
} as FindManyOptions<MessageChannelEntity>);
} else if (Array.isArray(baseWhere)) {
const coreWhereArray = await Promise.all(
baseWhere.map((whereItem) =>
this.toCoreWhere(workspaceId, whereItem as Record<string, unknown>),
),
);
return this.coreRepository.find({
...options,
results = await this.coreRepository.find({
...coreOptions,
where: coreWhereArray,
} as FindManyOptions<MessageChannelEntity>) as unknown as Promise<
MessageChannelWorkspaceEntity[]
>;
} as FindManyOptions<MessageChannelEntity>);
} else {
const coreWhere = await this.toCoreWhere(
workspaceId,
baseWhere as Record<string, unknown>,
);
results = await this.coreRepository.find({
...coreOptions,
where: coreWhere,
} as FindManyOptions<MessageChannelEntity>);
}
const coreWhere = await this.toCoreWhere(
workspaceId,
baseWhere as Record<string, unknown>,
);
const workspaceResults =
results as unknown as MessageChannelWorkspaceEntity[];
return this.coreRepository.find({
...options,
where: coreWhere,
} as FindManyOptions<MessageChannelEntity>) as unknown as Promise<
MessageChannelWorkspaceEntity[]
>;
if (needsConnectedAccount && workspaceResults.length > 0) {
const connectedAccountIds = [
...new Set(
workspaceResults.map((result) => result.connectedAccountId),
),
];
const connectedAccounts = await Promise.all(
connectedAccountIds.map((id) =>
this.connectedAccountDataAccessService.findOne(workspaceId, {
where: { id },
}),
),
);
const connectedAccountById = new Map(
connectedAccounts
.filter((account) => account !== null)
.map((account) => [account.id, account]),
);
for (const result of workspaceResults) {
const connectedAccount = connectedAccountById.get(
result.connectedAccountId,
);
if (connectedAccount) {
result.connectedAccount = connectedAccount;
}
}
}
if (needsMessageFolders && workspaceResults.length > 0) {
const workspaceRepository =
await this.getWorkspaceRepository(workspaceId);
for (const result of workspaceResults) {
const workspaceChannel = await workspaceRepository.findOne({
where: { id: result.id },
relations: ['messageFolders'],
});
result.messageFolders = workspaceChannel?.messageFolders ?? [];
}
}
return workspaceResults;
}
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);