Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code 5b7306f256 Unnecessary role/permissions cache invalidation on metadata label updates causes ~5.7s latency
https://sonarly.com/issue/3783?type=bug

A simple Company label rename triggers full role/permissions cache recomputation (6 cache providers, 10+ redundant DB queries totaling ~17s of query time), inflating the POST /metadata transaction to ~5.7 seconds despite returning 200 OK.

Fix: The root cause is that `getLegacyCacheInvalidationPromises` used a single `invalidateAndRecompute` call gated by `shouldIncrementMetadataGraphqlSchemaVersion || shouldInvalidateRoleMapCache`, which meant **any** object/field metadata change (including a cosmetic label rename) would trigger recomputation of all 6 caches — including the 5 expensive role/permissions caches (`rolesPermissions`, `userWorkspaceRoleMap`, `flatRoleTargetMaps`, `apiKeyRoleMap`, `flatRoleTargetByAgentIdMaps`).

The fix splits the single conditional into two independent ones:

1. **`ORMEntityMetadatas`** is invalidated only when `shouldIncrementMetadataGraphqlSchemaVersion` is true — i.e., when object/field metadata actually changes and the ORM schema needs rebuilding. This is correct and necessary.

2. **Role/permissions caches** (`rolesPermissions`, `userWorkspaceRoleMap`, `flatRoleTargetMaps`, `apiKeyRoleMap`, `flatRoleTargetByAgentIdMaps`) are only invalidated when `shouldInvalidateRoleMapCache` is true — i.e., when `flatRoleMaps` or `flatRoleTargetMaps` keys are in the changed set. A label rename does **not** touch these keys, so the 10+ redundant DB queries against `core.roleTarget`, `core.application`, and `core.role` are no longer triggered.

```typescript file=packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/services/workspace-migration-runner.service.ts lines=88-106
    if (shouldIncrementMetadataGraphqlSchemaVersion) {
      asyncOperations.push(
        this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
          'ORMEntityMetadatas',
        ]),
      );
    }

    if (shouldInvalidateRoleMapCache) {
      asyncOperations.push(
        this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
          'rolesPermissions',
          'userWorkspaceRoleMap',
          'flatRoleTargetMaps',
          'apiKeyRoleMap',
          'flatRoleTargetByAgentIdMaps',
        ]),
      );
    }
```

This eliminates ~17s of unnecessary DB query time (partially parallelized to ~5.7s wall-clock) for all metadata label/cosmetic updates.
2026-03-04 20:32:03 +00:00
Sonarly Claude Code 5adfc5f7b5 Slow metadata query: SELECT DISTINCT on wide fieldMetadata rows takes 2+ seconds
https://sonarly.com/issue/3801?type=bug

The POST /metadata GraphQL endpoint takes ~4.1 seconds due to a nestjs-query-generated SELECT DISTINCT query on the fieldMetadata table that takes 2+ seconds, caused by expensive DISTINCT deduplication across wide rows with multiple jsonb columns.

Fix: The fix replaces the slow `fields(paging: { first: 1000 }) { edges { node { ... } } }` CursorConnection query with `fields: fieldsList { ... }` using a GraphQL field alias in the REST metadata query builder.

**What changed:**

```typescript file=packages/twenty-server/src/engine/api/rest/metadata/query-builder/utils/fetch-metadata-fields.utils.ts lines=70-76
      const fieldsPart = selector?.fields
        ? `
        fields: fieldsList {
          ${fieldsSelection}
        }
      `
        : '';
```

**Why this works:**

1. **Eliminates the slow `SELECT DISTINCT`**: The `fields(paging: { first: 1000 })` CursorConnection was routed through nestjs-query's auto-generated resolver which unconditionally adds `SELECT DISTINCT` across all columns of `fieldMetadata` — including 4 large jsonb columns — taking ~2045ms. Switching to `fieldsList` uses the existing `@ResolveField` on `ObjectMetadataResolver` (lines 189–213) that loads fields via a DataLoader backed by a multi-level cache (local memory → Redis → database).

2. **Field alias preserves the response key**: Using `fields: fieldsList` as a GraphQL field alias means the response still contains the key `fields` (not `fieldsList`), so no consumer-facing breaking change occurs. The `cleanGraphQLResponse` utility handles the new shape correctly: a plain array is not an object (`isObject` returns false for arrays), so it is assigned directly — producing the same `fields: [{...}]` output as before.

3. **Single-file, 5-line change**: Entirely within the REST metadata query builder utility; no framework code, shared utilities, or other callers are modified.
2026-03-04 20:31:18 +00:00
@@ -85,17 +85,21 @@ export class WorkspaceMigrationRunnerService {
flatMapsKeysSet.has('flatRoleMaps') ||
flatMapsKeysSet.has('flatRoleTargetMaps');
if (
shouldIncrementMetadataGraphqlSchemaVersion ||
shouldInvalidateRoleMapCache
) {
if (shouldIncrementMetadataGraphqlSchemaVersion) {
asyncOperations.push(
this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
'ORMEntityMetadatas',
]),
);
}
if (shouldInvalidateRoleMapCache) {
asyncOperations.push(
this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
'rolesPermissions',
'userWorkspaceRoleMap',
'flatRoleTargetMaps',
'apiKeyRoleMap',
'ORMEntityMetadatas',
'flatRoleTargetByAgentIdMaps',
]),
);