Files
twenty/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts
T
Félix Malfait 5a4ebca226 refactor(server): unify the two metadata override mechanisms into one (#22417)
## Unify the two metadata override mechanisms into one

Twenty had **two** override mechanisms:

- **`standardOverrides`** — a bespoke JSONB column on
`objectMetadata`/`fieldMetadata` with typed DTOs and a per-locale
`translations` map, resolved by two i18n-aware resolvers.
- **`OverridableEntity.overrides`** — a flat, registry-driven JSONB blob
on view / view-field / view-field-group / command-menu-item /
page-layout-tab / page-layout-widget, resolved by a plain spread.

This PR collapses them into **one** concept: a single `overrides` blob,
one registry-driven overridable set, one i18n-aware read path, and one
write path (`computeMetadataOverridesBlob`, extracted in #22404).

Object/field **stay on `SyncableEntity`** (not reparented to
`OverridableEntity`) so their `isActive` default stays **FALSE** — this
sidesteps the `isActive` default conflict entirely.

### GraphQL breaking change (accepted)

The `standardOverrides` field is **removed** with no deprecation alias —
`overrides` (a `JSON` scalar) is exposed instead on `Object` and
`Field`. Product confirmed negligible external usage; the front-end has
no hand-written consumer (only generated types), which are regenerated
here.

### Commit structure (reviewable commit-by-commit)

1. **Unified resolver + parity harness** —
`resolveEffectiveEntityProperty` is a strict superset of the three
legacy resolvers; a corpus parity spec compares it against a *frozen
reference* of the old logic across every locale, `isStandardApp` branch
and override shape.
2. **Registry-driven** — object/field presentation props tagged
`isOverridable` + `translatable`; the overridable/translatable sets are
derived from the registry (a test asserts they equal the legacy
hardcoded lists).
3. **Rename + swap + delete** — `standardOverrides` → `overrides` across
entities, DTOs, flat/universal types, producers, the ~12
resolve/write/create/sync call sites, mocks and specs; the reconciler's
two compare entries collapse to one; the three legacy resolvers, both
DTOs and the hardcoded constants/types are deleted.
4. **Migration (zero-downtime, two-phase)** — split across two releases
so a rolling deploy never drops a column a previous-release pod still
`SELECT`s:
   - **2.19 fast** — add the `overrides` column (schema only).
- **2.19 slow** — backfill `overrides` from `standardOverrides` in
`runDataMigration` (kept out of the schema transaction so the bulk write
doesn't hold the ACCESS EXCLUSIVE lock; skipped on fresh installs, which
have no data to copy).
- **2.20 fast** — drop the legacy `standardOverrides` column (gated by
`TWENTY_NEXT_VERSIONS`, so it stays dormant until the instance reaches
2.20).
5. **Front/client-SDK regen** — regenerated metadata GraphQL types.
6. **Integration specs + i18n** — updated the standard object/field
update integration specs + snapshots, and the reworded validator message
catalog entry.

### Rolling-deploy safety

`standardOverrides` is retained through 2.19 and only dropped in 2.20,
mirroring the codebase's deferred-drop convention
(`isUIReadOnly`/`isCustom`). During the 2.19 rollout both columns exist,
so old and new pods coexist without "column does not exist" errors. The
backfill lives in a slow `runDataMigration` (per the
`no-data-mutation-in-fast-instance-command` rule) so it doesn't stall
reads.

### `isActive` guard

The migration never reads or writes `isActive`; the backfill asserts the
active-row count is unchanged and aborts otherwise. Verified on a real
DB: apply + revert preserves the blob **and** the nested `translations`
map, with `isActive` counts identical before/after.

### Verification (local)

- `nx typecheck twenty-server` + `nx typecheck twenty-front` — green
- `nx lint:diff-with-main twenty-server` (oxlint `--type-aware` + oxfmt)
— green
- `nx test twenty-server` — green (unit + parity + registry + migration
tests)
- `nx run twenty-server:test:integration:with-db-reset` — green
- `database:reset` applies the 2.19 phases and leaves **both** columns
present (2.20 drop stays dormant); backfill + revert round-trip verified
on a real DB
- Metadata integration suites (standard object/field update, application
sync) pass end-to-end against the two-column schema
- Metadata GraphQL types regenerated against a booted server; zero
`standardOverrides` references remain in application code (only the
migration commands + the legacy schema baseline)

---------

Co-authored-by: prastoin <paul@twenty.com>
2026-07-02 12:01:15 +02:00

965 lines
35 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import DataLoader from 'dataloader';
import { type APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type IndexMetadataInterface } from 'src/engine/metadata-modules/index-metadata/interfaces/index-metadata.interface';
import { ApplicationRegistrationVariableService } from 'src/engine/core-modules/application/application-registration-variable/application-registration-variable.service';
import { ApplicationTranslationCacheService } from 'src/engine/core-modules/application/application-translation/application-translation-cache.service';
import { type FlatApplicationCacheMaps } from 'src/engine/core-modules/application/types/flat-application-cache-maps.type';
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
import { type IDataloaders } from 'src/engine/dataloaders/dataloader.interface';
import { filterMorphRelationDuplicateFields } from 'src/engine/dataloaders/utils/filter-morph-relation-duplicate-fields.util';
import { type FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
import { RelationDTO } from 'src/engine/metadata-modules/field-metadata/dtos/relation.dto';
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ALL_OVERRIDABLE_PROPERTIES_BY_METADATA_NAME } from 'src/engine/metadata-modules/flat-entity/constant/all-overridable-properties-by-metadata-name.constant';
import { resolveEffectiveEntityProperty } from 'src/engine/metadata-modules/utils/resolve-effective-entity-property.util';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { findManyFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps.util';
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { fromFlatFieldMetadataToFieldMetadataDto } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-flat-field-metadata-to-field-metadata-dto.util';
import { belongsToTwentyStandardApp } from 'src/engine/metadata-modules/utils/belongs-to-twenty-standard-app.util';
import { getTwentyStandardApplicationIdOrThrow } from 'src/engine/metadata-modules/utils/get-twenty-standard-application-id-or-throw.util';
import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
import { resolveMorphRelationsFromFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/resolve-morph-relations-from-flat-field-metadata.util';
import { resolveRelationFromFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/resolve-relation-from-flat-field-metadata.util';
import { fromFlatObjectMetadataToObjectMetadataDto } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadata-to-object-metadata-dto.util';
import { getMorphNameFromMorphFieldMetadataName } from 'src/engine/metadata-modules/flat-object-metadata/utils/get-morph-name-from-morph-field-metadata-name.util';
import { fromFlatSearchFieldMetadataToSearchFieldMetadataDto } from 'src/engine/metadata-modules/flat-search-field-metadata/utils/from-flat-search-field-metadata-to-search-field-metadata-dto.util';
import { fromFlatViewFieldGroupToViewFieldGroupDto } from 'src/engine/metadata-modules/view-field-group/utils/from-flat-view-field-group-to-view-field-group-dto.util';
import { fromFlatViewFieldToViewFieldDto } from 'src/engine/metadata-modules/view-field/utils/from-flat-view-field-to-view-field-dto.util';
import { fromFlatViewFilterToViewFilterDto } from 'src/engine/metadata-modules/view-filter/utils/from-flat-view-filter-to-view-filter-dto.util';
import { fromFlatViewFilterGroupToViewFilterGroupDto } from 'src/engine/metadata-modules/view-filter-group/utils/from-flat-view-filter-group-to-view-filter-group-dto.util';
import { fromFlatViewGroupToViewGroupDto } from 'src/engine/metadata-modules/view-group/utils/from-flat-view-group-to-view-group-dto.util';
import { fromFlatViewSortToViewSortDto } from 'src/engine/metadata-modules/view-sort/utils/from-flat-view-sort-to-view-sort-dto.util';
import { type IndexFieldMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-field-metadata.dto';
import { type IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-metadata.dto';
import { ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto';
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { type SearchFieldMetadataDTO } from 'src/engine/metadata-modules/search-field-metadata/dtos/search-field-metadata.dto';
export type RelationMetadataLoaderPayload = {
workspaceId: string;
fieldMetadata: Pick<FieldMetadataEntity, 'type' | 'id' | 'objectMetadataId'>;
};
export type RelationLoaderPayload = {
workspaceId: string;
fieldMetadataId: string;
objectMetadataId: string;
};
export type MorphRelationLoaderPayload = {
workspaceId: string;
fieldMetadataId: string;
objectMetadataId: string;
};
export type FieldMetadataLoaderPayload = {
workspaceId: string;
objectMetadata: Pick<ObjectMetadataEntity, 'id'>;
locale?: keyof typeof APP_LOCALES;
};
export type IndexMetadataLoaderPayload = {
workspaceId: string;
objectMetadata: Pick<ObjectMetadataEntity, 'id'>;
};
export type IndexFieldMetadataLoaderPayload = {
workspaceId: string;
objectMetadata: Pick<ObjectMetadataEntity, 'id'>;
indexMetadata: Pick<IndexMetadataInterface, 'id'>;
};
export type SearchFieldMetadataLoaderPayload = {
workspaceId: string;
objectMetadata: Pick<ObjectMetadataEntity, 'id'>;
};
export type ObjectMetadataLoaderPayload = {
workspaceId: string;
objectMetadataId: string;
};
export type ViewFieldGroupsByViewIdLoaderPayload = {
workspaceId: string;
viewId: string;
};
export type ViewFieldsByViewFieldGroupIdLoaderPayload = {
workspaceId: string;
viewFieldGroupId: string;
};
export type ViewFieldsByViewIdLoaderPayload = {
workspaceId: string;
viewId: string;
};
export type ViewFiltersByViewIdLoaderPayload = {
workspaceId: string;
viewId: string;
};
export type ViewSortsByViewIdLoaderPayload = {
workspaceId: string;
viewId: string;
};
export type ViewGroupsByViewIdLoaderPayload = {
workspaceId: string;
viewId: string;
};
export type ViewFilterGroupsByViewIdLoaderPayload = {
workspaceId: string;
viewId: string;
};
export type IsConfiguredLoaderPayload = {
applicationRegistrationId: string;
};
export type StandardApplicationIdLoaderPayload = {
workspaceId: string;
};
export type ApplicationTranslationCatalogLoaderPayload = {
applicationId: string;
workspaceId: string;
locale: keyof typeof APP_LOCALES;
};
@Injectable()
export class DataloaderService {
constructor(
private readonly i18nService: I18nService,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly applicationRegistrationVariableService: ApplicationRegistrationVariableService,
private readonly applicationTranslationCacheService: ApplicationTranslationCacheService,
) {}
createLoaders(): IDataloaders {
const relationLoader = this.createRelationLoader();
const morphRelationLoader = this.createMorphRelationLoader();
const fieldMetadataLoader = this.createFieldMetadataLoader();
const indexMetadataLoader = this.createIndexMetadataLoader();
const indexFieldMetadataLoader = this.createIndexFieldMetadataLoader();
const searchFieldMetadataLoader = this.createSearchFieldMetadataLoader();
const objectMetadataLoader = this.createObjectMetadataLoader();
const viewFieldGroupsByViewIdLoader =
this.createViewFieldGroupsByViewIdLoader();
const viewFieldsByViewFieldGroupIdLoader =
this.createViewFieldsByViewFieldGroupIdLoader();
const viewFieldsByViewIdLoader = this.createViewFieldsByViewIdLoader();
const viewFiltersByViewIdLoader = this.createViewFiltersByViewIdLoader();
const viewSortsByViewIdLoader = this.createViewSortsByViewIdLoader();
const viewGroupsByViewIdLoader = this.createViewGroupsByViewIdLoader();
const viewFilterGroupsByViewIdLoader =
this.createViewFilterGroupsByViewIdLoader();
const isConfiguredLoader = this.createIsConfiguredLoader();
const standardApplicationIdLoader =
this.createStandardApplicationIdLoader();
const applicationTranslationCatalogLoader =
this.createApplicationTranslationCatalogLoader();
return {
relationLoader,
morphRelationLoader,
fieldMetadataLoader,
indexMetadataLoader,
indexFieldMetadataLoader,
searchFieldMetadataLoader,
objectMetadataLoader,
viewFieldGroupsByViewIdLoader,
viewFieldsByViewFieldGroupIdLoader,
viewFieldsByViewIdLoader,
viewFiltersByViewIdLoader,
viewSortsByViewIdLoader,
viewGroupsByViewIdLoader,
viewFilterGroupsByViewIdLoader,
isConfiguredLoader,
standardApplicationIdLoader,
applicationTranslationCatalogLoader,
};
}
private createRelationLoader() {
return new DataLoader<RelationLoaderPayload, RelationDTO | null>(
async (dataLoaderParams: RelationLoaderPayload[]) => {
const workspaceId = dataLoaderParams[0].workspaceId;
const { flatFieldMetadataMaps, flatObjectMetadataMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatFieldMetadataMaps', 'flatObjectMetadataMaps'],
},
);
return dataLoaderParams.map(({ fieldMetadataId }) => {
const sourceFlatFieldMetadata =
findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: fieldMetadataId,
flatEntityMaps: flatFieldMetadataMaps,
});
if (
!isFlatFieldMetadataOfType(
sourceFlatFieldMetadata,
FieldMetadataType.RELATION,
)
) {
return null;
}
return resolveRelationFromFlatFieldMetadata({
sourceFlatFieldMetadata,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
});
});
},
);
}
private createMorphRelationLoader() {
return new DataLoader<MorphRelationLoaderPayload, RelationDTO[] | null>(
async (dataLoaderParams: MorphRelationLoaderPayload[]) => {
const workspaceId = dataLoaderParams[0].workspaceId;
const { flatFieldMetadataMaps, flatObjectMetadataMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatFieldMetadataMaps', 'flatObjectMetadataMaps'],
},
);
return dataLoaderParams.map(({ fieldMetadataId }) => {
const morphFlatFieldMetadata =
findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: fieldMetadataId,
flatEntityMaps: flatFieldMetadataMaps,
});
if (
!isFlatFieldMetadataOfType(
morphFlatFieldMetadata,
FieldMetadataType.MORPH_RELATION,
)
) {
return null;
}
return resolveMorphRelationsFromFlatFieldMetadata({
morphFlatFieldMetadata,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
});
});
},
);
}
private createIndexMetadataLoader() {
return new DataLoader<IndexMetadataLoaderPayload, IndexMetadataDTO[]>(
async (dataLoaderParams: IndexMetadataLoaderPayload[]) => {
const workspaceId = dataLoaderParams[0].workspaceId;
const objectMetadataIds = dataLoaderParams.map(
(dataLoaderParam) => dataLoaderParam.objectMetadata.id,
);
const { flatIndexMaps, flatObjectMetadataMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatIndexMaps', 'flatObjectMetadataMaps'],
},
);
const indexMetadataCollection = objectMetadataIds.map(
(objectMetadataId) => {
const flatObjectMetadata =
findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: objectMetadataId,
flatEntityMaps: flatObjectMetadataMaps,
});
const indexMetadatas =
findManyFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityIds: flatObjectMetadata.indexMetadataIds,
flatEntityMaps: flatIndexMaps,
});
return indexMetadatas.map((indexMetadata) => ({
...indexMetadata,
indexFieldMetadatas: indexMetadata.flatIndexFieldMetadatas,
createdAt: new Date(indexMetadata.createdAt),
updatedAt: new Date(indexMetadata.updatedAt),
id: indexMetadata.id,
indexWhereClause: indexMetadata.indexWhereClause ?? undefined,
objectMetadataId,
workspaceId,
}));
},
);
return indexMetadataCollection;
},
);
}
private createSearchFieldMetadataLoader() {
return new DataLoader<
SearchFieldMetadataLoaderPayload,
SearchFieldMetadataDTO[]
>(async (dataLoaderParams: SearchFieldMetadataLoaderPayload[]) => {
const workspaceId = dataLoaderParams[0].workspaceId;
const objectMetadataIds = dataLoaderParams.map(
(dataLoaderParam) => dataLoaderParam.objectMetadata.id,
);
const { flatSearchFieldMetadataMaps, flatObjectMetadataMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: [
'flatSearchFieldMetadataMaps',
'flatObjectMetadataMaps',
],
},
);
return objectMetadataIds.map((objectMetadataId) => {
const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: objectMetadataId,
flatEntityMaps: flatObjectMetadataMaps,
});
const searchFieldMetadatas =
findManyFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityIds: flatObjectMetadata.searchFieldMetadataIds,
flatEntityMaps: flatSearchFieldMetadataMaps,
});
return searchFieldMetadatas.map(
fromFlatSearchFieldMetadataToSearchFieldMetadataDto,
);
});
});
}
private createFieldMetadataLoader() {
return new DataLoader<FieldMetadataLoaderPayload, FieldMetadataDTO[]>(
async (dataLoaderParams: FieldMetadataLoaderPayload[]) => {
const locale = dataLoaderParams[0].locale;
const safeLocale = locale ?? SOURCE_LOCALE;
const i18nInstance = this.i18nService.getI18nInstance(safeLocale);
const workspaceId = dataLoaderParams[0].workspaceId;
const objectMetadataIds = dataLoaderParams.map(
(dataLoaderParam) => dataLoaderParam.objectMetadata.id,
);
const {
flatFieldMetadataMaps,
flatObjectMetadataMaps,
flatApplicationMaps,
} =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: [
'flatFieldMetadataMaps',
'flatObjectMetadataMaps',
'flatApplicationMaps',
],
},
);
const objectFlatFieldMetadatasList = objectMetadataIds.map(
(objectMetadataId) => {
const flatObjectMetadata =
findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: objectMetadataId,
flatEntityMaps: flatObjectMetadataMaps,
});
return findManyFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityIds: flatObjectMetadata.fieldIds,
flatEntityMaps: flatFieldMetadataMaps,
});
},
);
const applicationCatalogByRegistrationId =
await this.loadApplicationCatalogByRegistrationId({
applicationIds: objectFlatFieldMetadatasList
.flat()
.map((flatFieldMetadata) => flatFieldMetadata.applicationId),
flatApplicationMaps,
locale: safeLocale,
});
const fieldMetadataCollection = objectFlatFieldMetadatasList.map(
(objectFlatFieldMetadatas) => {
const overriddenFieldMetadataEntities =
objectFlatFieldMetadatas.map((flatFieldMetadata) => {
const applicationRegistrationId =
flatApplicationMaps.byId[flatFieldMetadata.applicationId]
?.applicationRegistrationId;
const applicationCatalog = isDefined(applicationRegistrationId)
? applicationCatalogByRegistrationId.get(
applicationRegistrationId,
)
: undefined;
const overrides = flatFieldMetadata.overrides ?? undefined;
const i18nContext = {
locale,
i18nInstance,
isStandardApp: belongsToTwentyStandardApp(flatFieldMetadata),
applicationCatalog,
};
return ALL_OVERRIDABLE_PROPERTIES_BY_METADATA_NAME.fieldMetadata.reduce(
(acc, property) => ({
...acc,
[property]: resolveEffectiveEntityProperty({
metadataName: 'fieldMetadata',
baseValue: flatFieldMetadata[property],
overrides,
property,
i18nContext,
}),
}),
flatFieldMetadata,
);
});
const filteredFieldMetadataEntities =
filterMorphRelationDuplicateFields(
overriddenFieldMetadataEntities,
);
const filteredFieldMetadataEntitiesWithMorphRenamed =
filteredFieldMetadataEntities.map((flatFieldMetadata) => {
if (
isFlatFieldMetadataOfType(
flatFieldMetadata,
FieldMetadataType.MORPH_RELATION,
)
) {
const relationTargetObjectMetadata =
findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId:
flatFieldMetadata.relationTargetObjectMetadataId,
flatEntityMaps: flatObjectMetadataMaps,
});
return {
...flatFieldMetadata,
name: getMorphNameFromMorphFieldMetadataName({
morphRelationFlatFieldMetadata: flatFieldMetadata,
nameSingular: relationTargetObjectMetadata.nameSingular,
namePlural: relationTargetObjectMetadata.namePlural,
}),
};
}
return flatFieldMetadata;
});
return filteredFieldMetadataEntitiesWithMorphRenamed.map(
fromFlatFieldMetadataToFieldMetadataDto,
);
},
);
return fieldMetadataCollection;
},
);
}
private createIndexFieldMetadataLoader() {
return new DataLoader<
IndexFieldMetadataLoaderPayload,
IndexFieldMetadataDTO[]
>(async (dataLoaderParams: IndexFieldMetadataLoaderPayload[]) => {
const workspaceId = dataLoaderParams[0].workspaceId;
const { flatIndexMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatIndexMaps'],
},
);
return dataLoaderParams.map(
({ indexMetadata: { id: indexMetadataId } }) => {
const indexMetadataEntity = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: indexMetadataId,
flatEntityMaps: flatIndexMaps,
});
if (!isDefined(indexMetadataEntity)) {
return [];
}
return [...indexMetadataEntity.flatIndexFieldMetadatas]
.sort((a, b) => a.order - b.order)
.map((indexFieldMetadata) => {
return {
id: indexFieldMetadata.id,
fieldMetadataId: indexFieldMetadata.fieldMetadataId,
subFieldName: indexFieldMetadata.subFieldName ?? undefined,
order: indexFieldMetadata.order,
createdAt: new Date(indexFieldMetadata.createdAt),
updatedAt: new Date(indexFieldMetadata.updatedAt),
indexMetadataId,
workspaceId,
};
});
},
);
});
}
private createObjectMetadataLoader() {
return new DataLoader<
ObjectMetadataLoaderPayload,
ObjectMetadataDTO | null
>(async (dataLoaderParams: ObjectMetadataLoaderPayload[]) => {
const workspaceId = dataLoaderParams[0].workspaceId;
const { flatObjectMetadataMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatObjectMetadataMaps'],
},
);
return dataLoaderParams.map((dataLoaderParam) => {
const flatObjectMetadata = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: dataLoaderParam.objectMetadataId,
flatEntityMaps: flatObjectMetadataMaps,
});
if (!isDefined(flatObjectMetadata)) {
return null;
}
return fromFlatObjectMetadataToObjectMetadataDto(flatObjectMetadata);
});
});
}
private createViewFieldGroupsByViewIdLoader() {
return new DataLoader<
ViewFieldGroupsByViewIdLoaderPayload,
ReturnType<typeof fromFlatViewFieldGroupToViewFieldGroupDto>[]
>(async (dataLoaderParams: ViewFieldGroupsByViewIdLoaderPayload[]) => {
const workspaceId = dataLoaderParams[0].workspaceId;
const { flatViewMaps, flatViewFieldGroupMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatViewMaps', 'flatViewFieldGroupMaps'],
},
);
return dataLoaderParams.map(({ viewId }) => {
const flatView = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: viewId,
flatEntityMaps: flatViewMaps,
});
if (!isDefined(flatView)) {
return [];
}
return findManyFlatEntityByIdInFlatEntityMaps({
flatEntityIds: flatView.viewFieldGroupIds,
flatEntityMaps: flatViewFieldGroupMaps,
})
.filter(
(flatViewFieldGroup) =>
flatViewFieldGroup.deletedAt === null &&
flatViewFieldGroup.isActive,
)
.map(fromFlatViewFieldGroupToViewFieldGroupDto);
});
});
}
private createViewFieldsByViewFieldGroupIdLoader() {
return new DataLoader<
ViewFieldsByViewFieldGroupIdLoaderPayload,
ReturnType<typeof fromFlatViewFieldToViewFieldDto>[]
>(async (dataLoaderParams: ViewFieldsByViewFieldGroupIdLoaderPayload[]) => {
const workspaceId = dataLoaderParams[0].workspaceId;
const { flatViewFieldGroupMaps, flatViewFieldMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatViewFieldGroupMaps', 'flatViewFieldMaps'],
},
);
const viewFieldsByResolvedGroupId = new Map<
string,
ReturnType<typeof fromFlatViewFieldToViewFieldDto>[]
>();
for (const flatViewField of Object.values(
flatViewFieldMaps.byUniversalIdentifier,
)) {
if (
!isDefined(flatViewField) ||
flatViewField.deletedAt !== null ||
!flatViewField.isActive
) {
continue;
}
const resolvedGroupId =
flatViewField.overrides?.viewFieldGroupId !== undefined
? flatViewField.overrides.viewFieldGroupId
: flatViewField.viewFieldGroupId;
if (!isDefined(resolvedGroupId)) {
continue;
}
if (!viewFieldsByResolvedGroupId.has(resolvedGroupId)) {
viewFieldsByResolvedGroupId.set(resolvedGroupId, []);
}
viewFieldsByResolvedGroupId
.get(resolvedGroupId)!
.push(fromFlatViewFieldToViewFieldDto(flatViewField));
}
return dataLoaderParams.map(({ viewFieldGroupId }) => {
const flatViewFieldGroup = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: viewFieldGroupId,
flatEntityMaps: flatViewFieldGroupMaps,
});
if (!isDefined(flatViewFieldGroup)) {
return [];
}
return viewFieldsByResolvedGroupId.get(viewFieldGroupId) ?? [];
});
});
}
private createViewFieldsByViewIdLoader() {
return new DataLoader<
ViewFieldsByViewIdLoaderPayload,
ReturnType<typeof fromFlatViewFieldToViewFieldDto>[]
>(async (dataLoaderParams: ViewFieldsByViewIdLoaderPayload[]) => {
const workspaceId = dataLoaderParams[0].workspaceId;
const { flatViewMaps, flatViewFieldMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatViewMaps', 'flatViewFieldMaps'],
},
);
return dataLoaderParams.map(({ viewId }) => {
const flatView = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: viewId,
flatEntityMaps: flatViewMaps,
});
if (!isDefined(flatView)) {
return [];
}
return findManyFlatEntityByIdInFlatEntityMaps({
flatEntityIds: flatView.viewFieldIds,
flatEntityMaps: flatViewFieldMaps,
})
.filter(
(flatViewField) =>
flatViewField.deletedAt === null && flatViewField.isActive,
)
.map(fromFlatViewFieldToViewFieldDto);
});
});
}
private createViewFiltersByViewIdLoader() {
return new DataLoader<
ViewFiltersByViewIdLoaderPayload,
ReturnType<typeof fromFlatViewFilterToViewFilterDto>[]
>(async (dataLoaderParams: ViewFiltersByViewIdLoaderPayload[]) => {
const workspaceId = dataLoaderParams[0].workspaceId;
const { flatViewMaps, flatViewFilterMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatViewMaps', 'flatViewFilterMaps'],
},
);
return dataLoaderParams.map(({ viewId }) => {
const flatView = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: viewId,
flatEntityMaps: flatViewMaps,
});
if (!isDefined(flatView)) {
return [];
}
return findManyFlatEntityByIdInFlatEntityMaps({
flatEntityIds: flatView.viewFilterIds,
flatEntityMaps: flatViewFilterMaps,
})
.filter((flatViewFilter) => flatViewFilter.deletedAt === null)
.map(fromFlatViewFilterToViewFilterDto);
});
});
}
private createViewSortsByViewIdLoader() {
return new DataLoader<
ViewSortsByViewIdLoaderPayload,
ReturnType<typeof fromFlatViewSortToViewSortDto>[]
>(async (dataLoaderParams: ViewSortsByViewIdLoaderPayload[]) => {
const workspaceId = dataLoaderParams[0].workspaceId;
const { flatViewMaps, flatViewSortMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatViewMaps', 'flatViewSortMaps'],
},
);
return dataLoaderParams.map(({ viewId }) => {
const flatView = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: viewId,
flatEntityMaps: flatViewMaps,
});
if (!isDefined(flatView)) {
return [];
}
return findManyFlatEntityByIdInFlatEntityMaps({
flatEntityIds: flatView.viewSortIds,
flatEntityMaps: flatViewSortMaps,
})
.filter((flatViewSort) => flatViewSort.deletedAt === null)
.map(fromFlatViewSortToViewSortDto);
});
});
}
private createViewGroupsByViewIdLoader() {
return new DataLoader<
ViewGroupsByViewIdLoaderPayload,
ReturnType<typeof fromFlatViewGroupToViewGroupDto>[]
>(async (dataLoaderParams: ViewGroupsByViewIdLoaderPayload[]) => {
const workspaceId = dataLoaderParams[0].workspaceId;
const { flatViewMaps, flatViewGroupMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatViewMaps', 'flatViewGroupMaps'],
},
);
return dataLoaderParams.map(({ viewId }) => {
const flatView = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: viewId,
flatEntityMaps: flatViewMaps,
});
if (!isDefined(flatView)) {
return [];
}
return findManyFlatEntityByIdInFlatEntityMaps({
flatEntityIds: flatView.viewGroupIds,
flatEntityMaps: flatViewGroupMaps,
})
.filter((flatViewGroup) => flatViewGroup.deletedAt === null)
.map(fromFlatViewGroupToViewGroupDto);
});
});
}
private createViewFilterGroupsByViewIdLoader() {
return new DataLoader<
ViewFilterGroupsByViewIdLoaderPayload,
ReturnType<typeof fromFlatViewFilterGroupToViewFilterGroupDto>[]
>(async (dataLoaderParams: ViewFilterGroupsByViewIdLoaderPayload[]) => {
const workspaceId = dataLoaderParams[0].workspaceId;
const { flatViewMaps, flatViewFilterGroupMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatViewMaps', 'flatViewFilterGroupMaps'],
},
);
return dataLoaderParams.map(({ viewId }) => {
const flatView = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: viewId,
flatEntityMaps: flatViewMaps,
});
if (!isDefined(flatView)) {
return [];
}
return findManyFlatEntityByIdInFlatEntityMaps({
flatEntityIds: flatView.viewFilterGroupIds,
flatEntityMaps: flatViewFilterGroupMaps,
})
.filter(
(flatViewFilterGroup) => flatViewFilterGroup.deletedAt === null,
)
.map(fromFlatViewFilterGroupToViewFilterGroupDto);
});
});
}
private createIsConfiguredLoader() {
return new DataLoader<IsConfiguredLoaderPayload, boolean>(
async (params: IsConfiguredLoaderPayload[]) => {
const ids = params.map((p) => p.applicationRegistrationId);
const resultMap =
await this.applicationRegistrationVariableService.isConfiguredBatch(
ids,
);
return params.map(
(p) => resultMap.get(p.applicationRegistrationId) ?? true,
);
},
);
}
private createStandardApplicationIdLoader() {
return new DataLoader<StandardApplicationIdLoaderPayload, string>(
async (params: StandardApplicationIdLoaderPayload[]) => {
const workspaceId = params[0].workspaceId;
const { flatApplicationMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatApplicationMaps'],
},
);
const standardApplicationId =
getTwentyStandardApplicationIdOrThrow(flatApplicationMaps);
return params.map(() => standardApplicationId);
},
);
}
private createApplicationTranslationCatalogLoader() {
return new DataLoader<
ApplicationTranslationCatalogLoaderPayload,
Record<string, string> | undefined
>(async (params: ApplicationTranslationCatalogLoaderPayload[]) => {
const workspaceId = params[0].workspaceId;
const locale = params[0].locale;
const { flatApplicationMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatApplicationMaps'],
},
);
const standardApplicationId =
getTwentyStandardApplicationIdOrThrow(flatApplicationMaps);
const catalogByRegistrationId =
await this.loadApplicationCatalogByRegistrationId({
applicationIds: params.map((param) => param.applicationId),
flatApplicationMaps,
locale,
});
return params.map((param) => {
if (param.applicationId === standardApplicationId) {
return undefined;
}
const applicationRegistrationId =
flatApplicationMaps.byId[param.applicationId]
?.applicationRegistrationId;
return isDefined(applicationRegistrationId)
? catalogByRegistrationId.get(applicationRegistrationId)
: undefined;
});
});
}
private async loadApplicationCatalogByRegistrationId({
applicationIds,
flatApplicationMaps,
locale,
}: {
applicationIds: string[];
flatApplicationMaps: FlatApplicationCacheMaps;
locale: keyof typeof APP_LOCALES;
}): Promise<Map<string, Record<string, string>>> {
const registrationIds = [
...new Set(
applicationIds
.map(
(applicationId) =>
flatApplicationMaps.byId[applicationId]
?.applicationRegistrationId,
)
.filter(isDefined),
),
];
const catalogByRegistrationId = new Map<string, Record<string, string>>();
await Promise.all(
registrationIds.map(async (applicationRegistrationId) => {
const catalog =
await this.applicationTranslationCacheService.getCatalog({
applicationRegistrationId,
locale,
});
catalogByRegistrationId.set(applicationRegistrationId, catalog);
}),
);
return catalogByRegistrationId;
}
}