Gql direct execution - Handle introspection queries (#19219)

### Context
GraphQL introspection queries (__schema, __type) were going through the
full Yoga server pipeline, which forces a complete workspace schema
build, loading flat metadata maps, building all GraphQL types, wiring
all resolver factories, and calling makeExecutableSchema. This is
expensive, even though introspection only needs the type structure and
zero resolver execution.

A new WorkspaceGraphqlSchemaSDLService extracts the SDL computation that
was previously embedded inside WorkspaceSchemaFactory.

From `direct-execution.service.ts` : 
- `buildSchema(sdl)` reconstructs a resolver-free GraphQLSchema from the
SDL
     - pure CPU, not cached, not sure it worths it ?

- `execute({ schema, document, variableValues })` from graphql-js,
introspection is answered entirely by the graphql-js runtime from type
metadata, no resolver execution needed


#### Nice to do ?
- Use new cache service for typeDefs ?


### Renaming bonus: `typeDefs` → `sdl`

`typeDefs` is an Apollo/graphql-tools convention. It's the parameter
name in
`makeExecutableSchema({ typeDefs, resolvers })`, not a native GraphQL
spec term.

In proper GraphQL semantics, what this service produces is the **SDL**
(Schema
Definition Language): the official term for the string representation of
a schema.
`printSchema()` produces it, `buildSchema()` consumes it.

##### Why the distinction matters

- **Type definitions** implies partial type declarations (objects,
scalars, enums…)
- **Schema SDL** conveys a *complete* schema document: all types
**plus** the root
operation types (`Query`, `Mutation`) which is exactly what
`printSchema(schema)`
  produces
This commit is contained in:
Etienne
2026-04-02 13:50:18 +02:00
committed by GitHub
parent 579714b62f
commit a303d9ca1b
17 changed files with 658 additions and 612 deletions
@@ -1,16 +1,10 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { ScalarsExplorerService } from 'src/engine/api/graphql/services/scalars-explorer.service';
import { WorkspaceGraphqlSchemaSDLService } from 'src/engine/api/graphql/workspace-graphql-schema-sdl/workspace-graphql-schema-sdl.service';
import { WorkspaceResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory';
import { WorkspaceGraphQLSchemaGenerator } from 'src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory';
import { WorkspaceSchemaFactory } from 'src/engine/api/graphql/workspace-schema.factory';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
describe('WorkspaceSchemaFactory', () => {
let service: WorkspaceSchemaFactory;
@@ -19,40 +13,16 @@ describe('WorkspaceSchemaFactory', () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
WorkspaceSchemaFactory,
{
provide: DataSourceService,
useValue: {},
},
{
provide: ObjectMetadataService,
useValue: {},
},
{
provide: ScalarsExplorerService,
useValue: {},
},
{
provide: WorkspaceGraphQLSchemaGenerator,
useValue: {},
},
{
provide: WorkspaceResolverFactory,
useValue: {},
},
{
provide: WorkspaceCacheStorageService,
useValue: {},
},
{
provide: WorkspaceManyOrAllFlatEntityMapsCacheService,
useValue: {},
},
{
provide: FeatureFlagService,
useValue: {},
},
{
provide: TwentyConfigService,
provide: WorkspaceGraphqlSchemaSDLService,
useValue: {},
},
{
@@ -1,24 +1,16 @@
import { Module } from '@nestjs/common';
import { ScalarsExplorerService } from 'src/engine/api/graphql/services/scalars-explorer.service';
import { WorkspaceGraphqlSchemaSDLModule } from 'src/engine/api/graphql/workspace-graphql-schema-sdl/workspace-graphql-schema-sdl.module';
import { WorkspaceResolverBuilderModule } from 'src/engine/api/graphql/workspace-resolver-builder/workspace-resolver-builder.module';
import { WorkspaceSchemaBuilderModule } from 'src/engine/api/graphql/workspace-schema-builder/workspace-schema-builder.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
import { WorkspaceSchemaFactory } from './workspace-schema.factory';
@Module({
imports: [
DataSourceModule,
WorkspaceSchemaBuilderModule,
WorkspaceResolverBuilderModule,
WorkspaceCacheStorageModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
FeatureFlagModule,
WorkspaceGraphqlSchemaSDLModule,
MetricsModule,
],
providers: [WorkspaceSchemaFactory, ScalarsExplorerService],
@@ -4,6 +4,8 @@ import { CoreCommonApiModule } from 'src/engine/api/common/core-common-api.modul
import { DirectExecutionService } from 'src/engine/api/graphql/direct-execution/direct-execution.service';
import { WorkspaceResolverNameMapCacheService } from 'src/engine/api/graphql/direct-execution/services/workspace-resolver-name-map-cache.service';
import { WorkspaceResolverBuilderModule } from 'src/engine/api/graphql/workspace-resolver-builder/workspace-resolver-builder.module';
import { WorkspaceGraphqlSchemaSDLModule } from 'src/engine/api/graphql/workspace-graphql-schema-sdl/workspace-graphql-schema-sdl.module';
import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
@@ -13,6 +15,8 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
WorkspaceManyOrAllFlatEntityMapsCacheModule,
WorkspaceCacheModule,
WorkspaceResolverBuilderModule,
WorkspaceGraphqlSchemaSDLModule,
MetricsModule,
],
providers: [DirectExecutionService, WorkspaceResolverNameMapCacheService],
exports: [DirectExecutionService],
@@ -4,6 +4,8 @@ import { type MessageDescriptor } from '@lingui/core';
import { type Request } from 'express';
import {
GraphQLError,
buildSchema,
execute,
type DocumentNode,
type FieldNode,
type GraphQLFormattedError,
@@ -40,6 +42,7 @@ import { graphQLBuildFragmentMap } from 'src/engine/api/graphql/direct-execution
import { graphQLBuildPartialResolveInfo } from 'src/engine/api/graphql/direct-execution/utils/graphql-build-partial-resolve-info.util';
import { graphQLExtractTopLevelFields } from 'src/engine/api/graphql/direct-execution/utils/graphql-extract-top-level-fields.util';
import { graphQLFormatResultFromSelectedFields } from 'src/engine/api/graphql/direct-execution/utils/graphql-format-result-from-selected-fields.util';
import { WorkspaceGraphqlSchemaSDLService } from 'src/engine/api/graphql/workspace-graphql-schema-sdl/workspace-graphql-schema-sdl.service';
import { ResolverOutput } from 'src/engine/api/graphql/workspace-query-runner/interfaces/resolver-output';
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
@@ -62,6 +65,8 @@ import { type WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/gr
import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util';
@@ -84,8 +89,10 @@ export class DirectExecutionService {
constructor(
private readonly workspaceFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly workspaceGraphqlSchemaSDLService: WorkspaceGraphqlSchemaSDLService,
private readonly twentyConfigService: TwentyConfigService,
private readonly i18nService: I18nService,
private readonly metricsService: MetricsService,
private readonly findManyResolverFactory: FindManyResolverFactory,
private readonly findOneResolverFactory: FindOneResolverFactory,
private readonly findDuplicatesResolverFactory: FindDuplicatesResolverFactory,
@@ -144,7 +151,7 @@ export class DirectExecutionService {
]);
}
async getGeneratedWorkspaceResolverNames(
async getWorkspaceResolverNames(
workspaceId: string,
): Promise<Set<string> | null> {
const { graphQLResolverNameMap } =
@@ -158,6 +165,25 @@ export class DirectExecutionService {
async execute(
req: Request,
document: DocumentNode,
hasIntrospectionFields: boolean,
hasWorkspaceFields: boolean,
): Promise<DirectExecutionResult | null> {
const [introspectionResult, workspaceResult] = await Promise.all([
hasIntrospectionFields
? this.executeIntrospectionQuery(req, document)
: null,
hasWorkspaceFields ? this.executeWorkspaceQuery(req, document) : null,
]);
return this.mergeDirectExecutionResults(
introspectionResult,
workspaceResult,
);
}
private async executeWorkspaceQuery(
req: Request,
document: DocumentNode,
): Promise<DirectExecutionResult | null> {
try {
const workspaceId = req.workspace?.id;
@@ -255,6 +281,69 @@ export class DirectExecutionService {
}
}
private async executeIntrospectionQuery(
req: Request,
document: DocumentNode,
): Promise<DirectExecutionResult | null> {
try {
if (!isDefined(req.workspace)) {
return null;
}
const schemaSDLResult =
await this.workspaceGraphqlSchemaSDLService.getOrComputeSchemaSDL(
req.workspace,
req.application?.id ?? undefined,
);
if (!isDefined(schemaSDLResult)) {
return null;
}
const schema = buildSchema(schemaSDLResult.sdl);
const result = await execute({
schema,
document,
operationName: req.body?.operationName as string | undefined,
variableValues: (req.body?.variables as Record<string, unknown>) ?? {},
});
await this.metricsService.incrementCounter({
key: MetricsKeys.GraphqlIntrospectionDirectExecution,
shouldStoreInCache: false,
});
return {
data: result.data as Record<string, unknown> | undefined,
errors: result.errors?.map((error) => error.toJSON()),
};
} catch (error) {
return { errors: [this.formatError(error, req)] };
}
}
private mergeDirectExecutionResults(
introspectionResult: DirectExecutionResult | null,
workspaceResult: DirectExecutionResult | null,
): DirectExecutionResult | null {
if (!introspectionResult && !workspaceResult) {
return null;
}
const errors = [
...(introspectionResult?.errors ?? []),
...(workspaceResult?.errors ?? []),
];
return {
data: {
...introspectionResult?.data,
...workspaceResult?.data,
},
...(errors.length > 0 ? { errors } : {}),
};
}
private async executeField({
entry,
args,
@@ -5,9 +5,8 @@ import { FeatureFlagKey } from 'twenty-shared/types';
import { isNull } from '@sniptt/guards';
import { type DirectExecutionService } from 'src/engine/api/graphql/direct-execution/direct-execution.service';
import { computeSkipWorkspaceSchemaCreation } from 'src/engine/api/graphql/direct-execution/utils/compute-skip-workspace-schema-creation.util';
import { classifyTopLevelFields } from 'src/engine/api/graphql/direct-execution/utils/classify-top-level-fields.util';
import { findOperationDefinition } from 'src/engine/api/graphql/direct-execution/utils/find-operation-definition.util';
import { hasOnlyGeneratedWorkspaceResolvers } from 'src/engine/api/graphql/direct-execution/utils/has-only-generated-workspace-resolvers.util';
import { isSubscriptionOperation } from 'src/engine/api/graphql/direct-execution/utils/is-subscription-operation.util';
import { type FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
@@ -37,15 +36,6 @@ export function useDirectExecution(
return;
}
const generatedWorkspaceResolverNames =
await config.directExecutionService.getGeneratedWorkspaceResolverNames(
req.workspace.id,
);
if (!generatedWorkspaceResolverNames) {
return;
}
const queryString = req.body.query as string;
const operationName = req.body.operationName as string | undefined;
@@ -63,28 +53,32 @@ export function useDirectExecution(
return;
}
if (
computeSkipWorkspaceSchemaCreation(
queryString,
document,
operationName,
generatedWorkspaceResolverNames,
)
) {
req.skipWorkspaceSchemaCreation = true;
}
const workspaceResolverNames =
await config.directExecutionService.getWorkspaceResolverNames(
req.workspace.id,
);
if (
!hasOnlyGeneratedWorkspaceResolvers(
document,
operationName,
generatedWorkspaceResolverNames,
)
) {
if (!workspaceResolverNames) {
return;
}
const result = await config.directExecutionService.execute(req, document);
const { hasIntrospectionFields, hasWorkspaceFields, hasCoreFields } =
classifyTopLevelFields(document, operationName, workspaceResolverNames);
if (!hasCoreFields) {
req.skipWorkspaceSchemaCreation = true;
}
if (hasCoreFields) {
return;
}
const result = await config.directExecutionService.execute(
req,
document,
hasIntrospectionFields,
hasWorkspaceFields,
);
if (isNull(result)) {
return;
@@ -0,0 +1,169 @@
import { parse } from 'graphql';
import { classifyTopLevelFields } from 'src/engine/api/graphql/direct-execution/utils/classify-top-level-fields.util';
const WORKSPACE_RESOLVERS = new Set([
'findManyCompanies',
'findOneCompany',
'createOneCompany',
'findManyPeople',
'findOnePerson',
]);
describe('classifyTopLevelFields', () => {
it('should classify a pure introspection query', () => {
const query = `
query {
__schema { types { name } }
__type(name: "Company") { name }
}
`;
expect(
classifyTopLevelFields(parse(query), undefined, WORKSPACE_RESOLVERS),
).toEqual({
hasIntrospectionFields: true,
hasWorkspaceFields: false,
hasCoreFields: false,
});
});
it('should classify a pure workspace query', () => {
const query = `
query {
findManyCompanies { id name }
findOnePerson { id }
}
`;
expect(
classifyTopLevelFields(parse(query), undefined, WORKSPACE_RESOLVERS),
).toEqual({
hasIntrospectionFields: false,
hasWorkspaceFields: true,
hasCoreFields: false,
});
});
it('should classify a pure core query', () => {
const query = `
query {
currentWorkspace { id }
currentUser { id }
}
`;
expect(
classifyTopLevelFields(parse(query), undefined, WORKSPACE_RESOLVERS),
).toEqual({
hasIntrospectionFields: false,
hasWorkspaceFields: false,
hasCoreFields: true,
});
});
it('should classify a mixed introspection + workspace query', () => {
const query = `
query {
__schema { types { name } }
findManyCompanies { id }
}
`;
expect(
classifyTopLevelFields(parse(query), undefined, WORKSPACE_RESOLVERS),
).toEqual({
hasIntrospectionFields: true,
hasWorkspaceFields: true,
hasCoreFields: false,
});
});
it('should classify a mixed workspace + core query', () => {
const query = `
query {
findManyCompanies { id }
currentWorkspace { id }
}
`;
expect(
classifyTopLevelFields(parse(query), undefined, WORKSPACE_RESOLVERS),
).toEqual({
hasIntrospectionFields: false,
hasWorkspaceFields: true,
hasCoreFields: true,
});
});
it('should not classify __typename as introspection', () => {
const query = `
query {
currentWorkspace { id __typename }
}
`;
expect(
classifyTopLevelFields(parse(query), undefined, WORKSPACE_RESOLVERS),
).toEqual({
hasIntrospectionFields: false,
hasWorkspaceFields: false,
hasCoreFields: true,
});
});
it('should expand fragment spreads when classifying', () => {
const query = `
query {
__schema { types { name } }
...WorkspaceFragment
}
fragment WorkspaceFragment on Query {
findManyCompanies { id }
}
`;
expect(
classifyTopLevelFields(parse(query), undefined, WORKSPACE_RESOLVERS),
).toEqual({
hasIntrospectionFields: true,
hasWorkspaceFields: true,
hasCoreFields: false,
});
});
it('should respect operationName', () => {
const query = `
query IntrospectionQuery {
__schema { types { name } }
}
query WorkspaceQuery {
findManyCompanies { id }
}
`;
expect(
classifyTopLevelFields(
parse(query),
'IntrospectionQuery',
WORKSPACE_RESOLVERS,
),
).toEqual({
hasIntrospectionFields: true,
hasWorkspaceFields: false,
hasCoreFields: false,
});
expect(
classifyTopLevelFields(
parse(query),
'WorkspaceQuery',
WORKSPACE_RESOLVERS,
),
).toEqual({
hasIntrospectionFields: false,
hasWorkspaceFields: true,
hasCoreFields: false,
});
});
});
@@ -1,178 +0,0 @@
import { parse } from 'graphql';
import { computeSkipWorkspaceSchemaCreation } from 'src/engine/api/graphql/direct-execution/utils/compute-skip-workspace-schema-creation.util';
const GENERATED_RESOLVERS = new Set([
'findManyCompanies',
'findOneCompany',
'createOneCompany',
'findManyPeople',
'findOnePerson',
]);
describe('computeSkipWorkspaceSchemaCreation', () => {
it('should return true when all fields are core resolvers', () => {
const query = `
query {
currentWorkspace { id }
}
`;
expect(
computeSkipWorkspaceSchemaCreation(
query,
parse(query),
undefined,
GENERATED_RESOLVERS,
),
).toBe(true);
});
it('should return true for multiple core resolver fields', () => {
const query = `
query {
currentWorkspace { id }
currentUser { id }
}
`;
expect(
computeSkipWorkspaceSchemaCreation(
query,
parse(query),
undefined,
GENERATED_RESOLVERS,
),
).toBe(true);
});
it('should return true when all fields are generated workspace resolvers', () => {
const query = `
query {
findManyCompanies { id name }
}
`;
expect(
computeSkipWorkspaceSchemaCreation(
query,
parse(query),
undefined,
GENERATED_RESOLVERS,
),
).toBe(true);
});
it('should return false for mixed queries', () => {
const query = `
query {
findManyCompanies { id }
currentWorkspace { id }
}
`;
expect(
computeSkipWorkspaceSchemaCreation(
query,
parse(query),
undefined,
GENERATED_RESOLVERS,
),
).toBe(false);
});
it('should return false for __schema introspection', () => {
const query = `
query {
__schema { types { name } }
}
`;
expect(
computeSkipWorkspaceSchemaCreation(
query,
parse(query),
undefined,
GENERATED_RESOLVERS,
),
).toBe(false);
});
it('should return false for __type introspection', () => {
const query = `
query {
__type(name: "Company") { name fields { name } }
}
`;
expect(
computeSkipWorkspaceSchemaCreation(
query,
parse(query),
undefined,
GENERATED_RESOLVERS,
),
).toBe(false);
});
it('should not treat __typename as introspection', () => {
const query = `
query {
currentWorkspace { id __typename }
}
`;
expect(
computeSkipWorkspaceSchemaCreation(
query,
parse(query),
undefined,
GENERATED_RESOLVERS,
),
).toBe(true);
});
it('should return true when no operation matches (no fields to check)', () => {
const query = `
query GetCompanies { findManyCompanies { id } }
`;
expect(
computeSkipWorkspaceSchemaCreation(
query,
parse(query),
'NonExistent',
GENERATED_RESOLVERS,
),
).toBe(true);
});
it('should respect operationName', () => {
const query = `
query CoreQuery {
currentWorkspace { id }
}
query WorkspaceQuery {
findManyCompanies { id }
}
`;
expect(
computeSkipWorkspaceSchemaCreation(
query,
parse(query),
'CoreQuery',
GENERATED_RESOLVERS,
),
).toBe(true);
expect(
computeSkipWorkspaceSchemaCreation(
query,
parse(query),
'WorkspaceQuery',
GENERATED_RESOLVERS,
),
).toBe(true);
});
});
@@ -73,4 +73,39 @@ describe('graphQLExtractTopLevelFields', () => {
expect(fields).toEqual([]);
});
it('should expand fragment spreads at the top level', () => {
const query = `
query {
__schema { types { name } }
...DataFragment
}
fragment DataFragment on Query {
companies { id }
}
`;
const fields = graphQLExtractTopLevelFields(parse(query), undefined);
expect(fields).toHaveLength(2);
expect(fields[0].name.value).toBe('__schema');
expect(fields[1].name.value).toBe('companies');
});
it('should expand inline fragments at the top level', () => {
const query = `
query {
__schema { types { name } }
... on Query {
companies { id }
}
}
`;
const fields = graphQLExtractTopLevelFields(parse(query), undefined);
expect(fields).toHaveLength(2);
expect(fields[0].name.value).toBe('__schema');
expect(fields[1].name.value).toBe('companies');
});
});
@@ -1,121 +0,0 @@
import { parse } from 'graphql';
import { hasOnlyGeneratedWorkspaceResolvers } from 'src/engine/api/graphql/direct-execution/utils/has-only-generated-workspace-resolvers.util';
const GENERATED_RESOLVERS = new Set([
'companies',
'company',
'createOneCompany',
'people',
'person',
]);
describe('hasOnlyGeneratedWorkspaceResolvers', () => {
it('should return true when all fields are generated workspace resolvers', () => {
const query = `
query {
companies { id name }
people { id email }
}
`;
expect(
hasOnlyGeneratedWorkspaceResolvers(
parse(query),
undefined,
GENERATED_RESOLVERS,
),
).toBe(true);
});
it('should return true for a single generated workspace resolver', () => {
const query = `
query {
company(filter: { id: { eq: "123" } }) { id }
}
`;
expect(
hasOnlyGeneratedWorkspaceResolvers(
parse(query),
undefined,
GENERATED_RESOLVERS,
),
).toBe(true);
});
it('should return false when all fields are core resolvers', () => {
const query = `
query {
search { id }
getTimelineCalendarEventsFromOpportunityId { id }
}
`;
expect(
hasOnlyGeneratedWorkspaceResolvers(
parse(query),
undefined,
GENERATED_RESOLVERS,
),
).toBe(false);
});
it('should return false for mixed queries', () => {
const query = `
query {
companies { id }
getTimelineCalendarEventsFromOpportunityId { id }
}
`;
expect(
hasOnlyGeneratedWorkspaceResolvers(
parse(query),
undefined,
GENERATED_RESOLVERS,
),
).toBe(false);
});
it('should return true when no operation matches (no fields to check)', () => {
const query = `
query GetCompanies { companies { id } }
`;
expect(
hasOnlyGeneratedWorkspaceResolvers(
parse(query),
'NonExistent',
GENERATED_RESOLVERS,
),
).toBe(true);
});
it('should respect operationName', () => {
const query = `
query WorkspaceQuery {
companies { id }
}
query CoreQuery {
getTimelineCalendarEventsFromOpportunityId { id }
}
`;
expect(
hasOnlyGeneratedWorkspaceResolvers(
parse(query),
'WorkspaceQuery',
GENERATED_RESOLVERS,
),
).toBe(true);
expect(
hasOnlyGeneratedWorkspaceResolvers(
parse(query),
'CoreQuery',
GENERATED_RESOLVERS,
),
).toBe(false);
});
});
@@ -0,0 +1,35 @@
import { type DocumentNode } from 'graphql';
import { graphQLExtractTopLevelFields } from 'src/engine/api/graphql/direct-execution/utils/graphql-extract-top-level-fields.util';
const INTROSPECTION_FIELD_NAMES = new Set(['__schema', '__type']);
type TopLevelFieldsClassification = {
hasIntrospectionFields: boolean;
hasWorkspaceFields: boolean;
hasCoreFields: boolean;
};
export const classifyTopLevelFields = (
document: DocumentNode,
operationName: string | undefined,
workspaceResolverNames: Set<string>,
): TopLevelFieldsClassification => {
const topLevelFields = graphQLExtractTopLevelFields(document, operationName);
let hasIntrospectionFields = false;
let hasWorkspaceFields = false;
let hasCoreFields = false;
for (const field of topLevelFields) {
if (INTROSPECTION_FIELD_NAMES.has(field.name.value)) {
hasIntrospectionFields = true;
} else if (workspaceResolverNames.has(field.name.value)) {
hasWorkspaceFields = true;
} else {
hasCoreFields = true;
}
}
return { hasIntrospectionFields, hasWorkspaceFields, hasCoreFields };
};
@@ -1,27 +0,0 @@
import { type DocumentNode } from 'graphql';
import { graphQLExtractTopLevelFields } from 'src/engine/api/graphql/direct-execution/utils/graphql-extract-top-level-fields.util';
const INTROSPECTION_PATTERN = /__schema|__type(?!name)/;
export const computeSkipWorkspaceSchemaCreation = (
queryString: string,
document: DocumentNode,
operationName: string | undefined,
generatedWorkspaceResolverNames: Set<string>,
): boolean => {
if (INTROSPECTION_PATTERN.test(queryString)) {
return false;
}
const topLevelFields = graphQLExtractTopLevelFields(document, operationName);
const hasCore = topLevelFields.some(
(field) => !generatedWorkspaceResolverNames.has(field.name.value),
);
const hasGenerated = topLevelFields.some((field) =>
generatedWorkspaceResolverNames.has(field.name.value),
);
return !(hasCore && hasGenerated);
};
@@ -1,6 +1,7 @@
import { type DocumentNode, type FieldNode, Kind } from 'graphql';
import { findOperationDefinition } from 'src/engine/api/graphql/direct-execution/utils/find-operation-definition.util';
import { graphQLBuildFragmentMap } from 'src/engine/api/graphql/direct-execution/utils/graphql-build-fragment-map.util';
export const graphQLExtractTopLevelFields = (
document: DocumentNode,
@@ -12,7 +13,30 @@ export const graphQLExtractTopLevelFields = (
return [];
}
return operationDefinition.selectionSet.selections.filter(
(selection): selection is FieldNode => selection.kind === Kind.FIELD,
);
const fragmentMap = graphQLBuildFragmentMap(document);
const fields: FieldNode[] = [];
for (const selection of operationDefinition.selectionSet.selections) {
if (selection.kind === Kind.FIELD) {
fields.push(selection);
} else if (selection.kind === Kind.FRAGMENT_SPREAD) {
const fragment = fragmentMap.get(selection.name.value);
if (fragment) {
for (const fragmentSelection of fragment.selectionSet.selections) {
if (fragmentSelection.kind === Kind.FIELD) {
fields.push(fragmentSelection);
}
}
}
} else if (selection.kind === Kind.INLINE_FRAGMENT) {
for (const inlineSelection of selection.selectionSet.selections) {
if (inlineSelection.kind === Kind.FIELD) {
fields.push(inlineSelection);
}
}
}
}
return fields;
};
@@ -1,15 +0,0 @@
import { type DocumentNode } from 'graphql';
import { graphQLExtractTopLevelFields } from 'src/engine/api/graphql/direct-execution/utils/graphql-extract-top-level-fields.util';
export const hasOnlyGeneratedWorkspaceResolvers = (
document: DocumentNode,
operationName: string | undefined,
generatedWorkspaceResolverNames: Set<string>,
): boolean => {
const topLevelFields = graphQLExtractTopLevelFields(document, operationName);
return topLevelFields.every((field) =>
generatedWorkspaceResolverNames.has(field.name.value),
);
};
@@ -0,0 +1,22 @@
import { Module } from '@nestjs/common';
import { ScalarsExplorerService } from 'src/engine/api/graphql/services/scalars-explorer.service';
import { WorkspaceSchemaBuilderModule } from 'src/engine/api/graphql/workspace-schema-builder/workspace-schema-builder.module';
import { WorkspaceGraphqlSchemaSDLService } from 'src/engine/api/graphql/workspace-graphql-schema-sdl/workspace-graphql-schema-sdl.service';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
@Module({
imports: [
WorkspaceSchemaBuilderModule,
WorkspaceCacheStorageModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
FeatureFlagModule,
DataSourceModule,
],
providers: [WorkspaceGraphqlSchemaSDLService, ScalarsExplorerService],
exports: [WorkspaceGraphqlSchemaSDLService],
})
export class WorkspaceGraphqlSchemaSDLModule {}
@@ -0,0 +1,232 @@
import { Injectable } from '@nestjs/common';
import { isNonEmptyString } from '@sniptt/guards';
import { printSchema } from 'graphql';
import { FeatureFlagKey } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { ScalarsExplorerService } from 'src/engine/api/graphql/services/scalars-explorer.service';
import { WorkspaceGraphQLSchemaGenerator } from 'src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { FlatWorkspace } from 'src/engine/core-modules/workspace/types/flat-workspace.type';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { getSubFlatEntityMapsByApplicationIdsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/get-sub-flat-entity-maps-by-application-ids-or-throw.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
export type WorkspaceGraphqlSchemaSDLResult = {
sdl: string;
usedScalarNames: string[];
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
};
@Injectable()
export class WorkspaceGraphqlSchemaSDLService {
constructor(
private readonly scalarsExplorerService: ScalarsExplorerService,
private readonly workspaceGraphQLSchemaGenerator: WorkspaceGraphQLSchemaGenerator,
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly featureFlagService: FeatureFlagService,
private readonly dataSourceService: DataSourceService,
) {}
async getOrComputeSchemaSDL(
workspace: FlatWorkspace,
applicationId?: string,
): Promise<WorkspaceGraphqlSchemaSDLResult | null> {
const isDataSourceMigrated = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_DATASOURCE_MIGRATED,
workspace.id,
);
const hasSchema = isDataSourceMigrated
? isNonEmptyString(workspace.databaseSchema)
: (
await this.dataSourceService.getDataSourcesMetadataFromWorkspaceId(
workspace.id,
)
).length > 0;
if (!hasSchema) {
return null;
}
const {
flatObjectMetadataMaps: allFlatObjectMetadataMaps,
flatFieldMetadataMaps: allFlatFieldMetadataMaps,
flatIndexMaps: allFlatIndexMaps,
flatApplicationMaps,
} = await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId: workspace.id,
flatMapsKeys: [
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
'flatIndexMaps',
'flatApplicationMaps',
],
},
);
if (!isDefined(allFlatObjectMetadataMaps)) {
throw new FlatEntityMapsException(
'Object metadata collection not found',
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
if (!isDefined(allFlatFieldMetadataMaps)) {
throw new FlatEntityMapsException(
'Field metadata collection not found',
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
let flatObjectMetadataMaps = allFlatObjectMetadataMaps;
let flatFieldMetadataMaps = allFlatFieldMetadataMaps;
let flatIndexMaps = allFlatIndexMaps;
if (isDefined(applicationId)) {
const twentyStandardApplicationId =
flatApplicationMaps?.idByUniversalIdentifier[
TWENTY_STANDARD_APPLICATION.universalIdentifier
];
const applicationIds = isDefined(twentyStandardApplicationId)
? [twentyStandardApplicationId, applicationId]
: [applicationId];
flatObjectMetadataMaps = this.filterFlatEntityMapsByApplicationIds(
allFlatObjectMetadataMaps,
applicationIds,
);
flatFieldMetadataMaps = this.filterFlatEntityMapsByApplicationIds(
allFlatFieldMetadataMaps,
applicationIds,
);
flatObjectMetadataMaps =
this.reconcileObjectFieldIdsWithFilteredFieldMaps(
flatObjectMetadataMaps,
flatFieldMetadataMaps,
);
if (isDefined(allFlatIndexMaps)) {
flatIndexMaps = this.filterFlatEntityMapsByApplicationIds(
allFlatIndexMaps,
applicationIds,
);
}
}
let metadataVersion =
await this.workspaceCacheStorageService.getMetadataVersion(workspace.id);
if (!isDefined(metadataVersion)) {
metadataVersion = isDefined(workspace.metadataVersion)
? workspace.metadataVersion
: 0;
await this.workspaceCacheStorageService.setMetadataVersion(
workspace.id,
metadataVersion,
);
}
let sdl = await this.workspaceCacheStorageService.getGraphQLTypeDefs(
workspace.id,
metadataVersion,
applicationId,
);
let usedScalarNames =
await this.workspaceCacheStorageService.getGraphQLUsedScalarNames(
workspace.id,
metadataVersion,
applicationId,
);
if (!sdl || !usedScalarNames) {
const autoGeneratedSchema =
await this.workspaceGraphQLSchemaGenerator.generateSchema({
flatObjectMetadataMaps,
flatFieldMetadataMaps,
flatIndexMaps,
});
usedScalarNames =
this.scalarsExplorerService.getUsedScalarNames(autoGeneratedSchema);
sdl = printSchema(autoGeneratedSchema);
await this.workspaceCacheStorageService.setGraphQLTypeDefs(
workspace.id,
metadataVersion,
sdl,
applicationId,
);
await this.workspaceCacheStorageService.setGraphQLUsedScalarNames(
workspace.id,
metadataVersion,
usedScalarNames,
applicationId,
);
}
return {
sdl,
usedScalarNames,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
};
}
private reconcileObjectFieldIdsWithFilteredFieldMaps(
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>,
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
): FlatEntityMaps<FlatObjectMetadata> {
const filteredFieldIds = new Set(
Object.keys(flatFieldMetadataMaps.universalIdentifierById),
);
const reconciledByUniversalIdentifier: Partial<
Record<string, FlatObjectMetadata>
> = {};
for (const [universalId, object] of Object.entries(
flatObjectMetadataMaps.byUniversalIdentifier,
)) {
if (!isDefined(object)) continue;
reconciledByUniversalIdentifier[universalId] = {
...object,
fieldIds: object.fieldIds.filter((id) => filteredFieldIds.has(id)),
};
}
return {
...flatObjectMetadataMaps,
byUniversalIdentifier: reconciledByUniversalIdentifier,
};
}
private filterFlatEntityMapsByApplicationIds<
T extends FlatObjectMetadata | FlatFieldMetadata | FlatIndexMetadata,
>(
flatEntityMaps: FlatEntityMaps<T>,
applicationIds: string[],
): FlatEntityMaps<T> {
return getSubFlatEntityMapsByApplicationIdsOrThrow({
applicationIds,
flatEntityMaps,
});
}
}
@@ -1,45 +1,25 @@
import { Injectable } from '@nestjs/common';
import { makeExecutableSchema } from '@graphql-tools/schema';
import { isNonEmptyString } from '@sniptt/guards';
import { GraphQLSchema, printSchema } from 'graphql';
import { GraphQLSchema } from 'graphql';
import { gql } from 'graphql-tag';
import { FeatureFlagKey } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { ScalarsExplorerService } from 'src/engine/api/graphql/services/scalars-explorer.service';
import { WorkspaceGraphqlSchemaSDLService } from 'src/engine/api/graphql/workspace-graphql-schema-sdl/workspace-graphql-schema-sdl.service';
import { workspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/factories/factories';
import { WorkspaceResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory';
import { WorkspaceGraphQLSchemaGenerator } from 'src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
import { FlatWorkspace } from 'src/engine/core-modules/workspace/types/flat-workspace.type';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { getSubFlatEntityMapsByApplicationIdsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/get-sub-flat-entity-maps-by-application-ids-or-throw.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
@Injectable()
export class WorkspaceSchemaFactory {
constructor(
private readonly scalarsExplorerService: ScalarsExplorerService,
private readonly workspaceGraphQLSchemaGenerator: WorkspaceGraphQLSchemaGenerator,
private readonly workspaceResolverFactory: WorkspaceResolverFactory,
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly featureFlagService: FeatureFlagService,
private readonly dataSourceService: DataSourceService,
private readonly workspaceGraphqlSchemaSDLService: WorkspaceGraphqlSchemaSDLService,
private readonly metricsService: MetricsService,
) {}
@@ -47,146 +27,27 @@ export class WorkspaceSchemaFactory {
workspace: FlatWorkspace,
applicationId?: string,
): Promise<GraphQLSchema> {
const isDataSourceMigrated = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_DATASOURCE_MIGRATED,
workspace.id,
);
const schemaSDLResult =
await this.workspaceGraphqlSchemaSDLService.getOrComputeSchemaSDL(
workspace,
applicationId,
);
const hasSchema = isDataSourceMigrated
? isNonEmptyString(workspace.databaseSchema)
: (
await this.dataSourceService.getDataSourcesMetadataFromWorkspaceId(
workspace.id,
)
).length > 0;
if (!hasSchema) {
if (!isDefined(schemaSDLResult)) {
return new GraphQLSchema({});
}
const {
flatObjectMetadataMaps: allFlatObjectMetadataMaps,
flatFieldMetadataMaps: allFlatFieldMetadataMaps,
flatIndexMaps: allFlatIndexMaps,
flatApplicationMaps,
} = await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId: workspace.id,
flatMapsKeys: [
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
'flatIndexMaps',
'flatApplicationMaps',
],
},
);
if (!isDefined(allFlatObjectMetadataMaps)) {
throw new FlatEntityMapsException(
'Object metadata collection not found',
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
if (!isDefined(allFlatFieldMetadataMaps)) {
throw new FlatEntityMapsException(
'Field metadata collection not found',
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
let flatObjectMetadataMaps = allFlatObjectMetadataMaps;
let flatFieldMetadataMaps = allFlatFieldMetadataMaps;
let flatIndexMaps = allFlatIndexMaps;
if (isDefined(applicationId)) {
const twentyStandardApplicationId =
flatApplicationMaps?.idByUniversalIdentifier[
TWENTY_STANDARD_APPLICATION.universalIdentifier
];
const applicationIds = isDefined(twentyStandardApplicationId)
? [twentyStandardApplicationId, applicationId]
: [applicationId];
flatObjectMetadataMaps = this.filterFlatEntityMapsByApplicationIds(
allFlatObjectMetadataMaps,
applicationIds,
);
flatFieldMetadataMaps = this.filterFlatEntityMapsByApplicationIds(
allFlatFieldMetadataMaps,
applicationIds,
);
flatObjectMetadataMaps =
this.reconcileObjectFieldIdsWithFilteredFieldMaps(
flatObjectMetadataMaps,
flatFieldMetadataMaps,
);
if (isDefined(allFlatIndexMaps)) {
flatIndexMaps = this.filterFlatEntityMapsByApplicationIds(
allFlatIndexMaps,
applicationIds,
);
}
}
let metadataVersion =
await this.workspaceCacheStorageService.getMetadataVersion(workspace.id);
if (!isDefined(metadataVersion)) {
metadataVersion = isDefined(workspace.metadataVersion)
? workspace.metadataVersion
: 0;
await this.workspaceCacheStorageService.setMetadataVersion(
workspace.id,
metadataVersion,
);
}
sdl,
usedScalarNames,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
} = schemaSDLResult;
const { idByNameSingular } = buildObjectIdByNameMaps(
flatObjectMetadataMaps,
);
let typeDefs = await this.workspaceCacheStorageService.getGraphQLTypeDefs(
workspace.id,
metadataVersion,
applicationId,
);
let usedScalarNames =
await this.workspaceCacheStorageService.getGraphQLUsedScalarNames(
workspace.id,
metadataVersion,
applicationId,
);
if (!typeDefs || !usedScalarNames) {
const autoGeneratedSchema =
await this.workspaceGraphQLSchemaGenerator.generateSchema({
flatObjectMetadataMaps,
flatFieldMetadataMaps,
flatIndexMaps,
});
usedScalarNames =
this.scalarsExplorerService.getUsedScalarNames(autoGeneratedSchema);
typeDefs = printSchema(autoGeneratedSchema);
await this.workspaceCacheStorageService.setGraphQLTypeDefs(
workspace.id,
metadataVersion,
typeDefs,
applicationId,
);
await this.workspaceCacheStorageService.setGraphQLUsedScalarNames(
workspace.id,
metadataVersion,
usedScalarNames,
applicationId,
);
}
const autoGeneratedResolvers = await this.workspaceResolverFactory.create(
flatObjectMetadataMaps,
flatFieldMetadataMaps,
@@ -198,7 +59,7 @@ export class WorkspaceSchemaFactory {
const executableSchema = makeExecutableSchema({
typeDefs: gql`
${typeDefs}
${sdl}
`,
resolvers: {
...scalarsResolvers,
@@ -213,45 +74,4 @@ export class WorkspaceSchemaFactory {
return executableSchema;
}
private reconcileObjectFieldIdsWithFilteredFieldMaps(
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>,
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
): FlatEntityMaps<FlatObjectMetadata> {
const filteredFieldIds = new Set(
Object.keys(flatFieldMetadataMaps.universalIdentifierById),
);
const reconciledByUniversalIdentifier: Partial<
Record<string, FlatObjectMetadata>
> = {};
for (const [universalId, object] of Object.entries(
flatObjectMetadataMaps.byUniversalIdentifier,
)) {
if (!isDefined(object)) continue;
reconciledByUniversalIdentifier[universalId] = {
...object,
fieldIds: object.fieldIds.filter((id) => filteredFieldIds.has(id)),
};
}
return {
...flatObjectMetadataMaps,
byUniversalIdentifier: reconciledByUniversalIdentifier,
};
}
private filterFlatEntityMapsByApplicationIds<
T extends FlatObjectMetadata | FlatFieldMetadata | FlatIndexMetadata,
>(
flatEntityMaps: FlatEntityMaps<T>,
applicationIds: string[],
): FlatEntityMaps<T> {
return getSubFlatEntityMapsByApplicationIdsOrThrow({
applicationIds,
flatEntityMaps,
});
}
}