Generate GQL schema based on applicationId (#17860)
## Add application-scoped GraphQL schema generation When an application token is used to authenticate, the `/graphql` schema is now dynamically filtered to only include entities belonging to that application (plus the Twenty Standard Application). This enables third-party applications and the SDK to introspect a schema that is relevant to their scope, rather than seeing the full workspace schema with all custom objects. ### Changes - **New `generateApplicationToken` mutation** on the `/metadata` endpoint, allowing callers to exchange an API key for an application-scoped JWT token - **Schema filtering by application** in `WorkspaceSchemaFactory` — when `request.application` is present (from an application token), flat entity maps are filtered by `[appId, standardAppId]` before schema generation - **Per-app caching** — both the Yoga in-memory cache and Redis cache now include the `appId` in their keys to avoid serving wrong schemas - **Consolidated `getSubFlatEntityMapsByApplicationIdsOrThrow`** — unified the single-ID and multi-ID filtering utilities into one - **Integration tests** covering token generation (admin + API key auth) and schema introspection filtering (standard app token excludes custom objects) Schema generated on seeds with applicationToken (see that pets is missing) <img width="782" height="994" alt="image" src="https://github.com/user-attachments/assets/82510031-0965-435d-bc26-77c9f5d74e1f" />
This commit is contained in:
@@ -7,6 +7,7 @@ import { MarketplaceResolver } from 'src/engine/core-modules/application/resolve
|
||||
import { ApplicationManifestMigrationService } from 'src/engine/core-modules/application/services/application-manifest-migration.service';
|
||||
import { ApplicationSyncService } from 'src/engine/core-modules/application/services/application-sync.service';
|
||||
import { ApplicationVariableEntityModule } from 'src/engine/core-modules/applicationVariable/application-variable.module';
|
||||
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
|
||||
import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module';
|
||||
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
|
||||
import { ObjectPermissionModule } from 'src/engine/metadata-modules/object-permission/object-permission.module';
|
||||
@@ -24,6 +25,7 @@ import { CodeStepBuildModule } from 'src/modules/workflow/workflow-builder/workf
|
||||
TypeOrmModule.forFeature([FileEntity]),
|
||||
ApplicationModule,
|
||||
ApplicationVariableEntityModule,
|
||||
TokenModule,
|
||||
WorkspaceMigrationModule,
|
||||
PermissionsModule,
|
||||
ObjectPermissionModule,
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { ArgsType, Field } from '@nestjs/graphql';
|
||||
|
||||
import { IsNotEmpty, IsUUID } from 'class-validator';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@ArgsType()
|
||||
export class GenerateApplicationTokenInput {
|
||||
@Field(() => UUIDScalarType)
|
||||
@IsNotEmpty()
|
||||
@IsUUID()
|
||||
applicationId: string;
|
||||
}
|
||||
+37
-2
@@ -10,8 +10,11 @@ import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
|
||||
import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interfaces/node-environment.interface';
|
||||
|
||||
import type { FileUpload } from 'graphql-upload/processRequest.mjs';
|
||||
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { ApplicationExceptionFilter } from 'src/engine/core-modules/application/application-exception-filter';
|
||||
import {
|
||||
@@ -20,18 +23,22 @@ import {
|
||||
} from 'src/engine/core-modules/application/application.exception';
|
||||
import { ApplicationDTO } from 'src/engine/core-modules/application/dtos/application.dto';
|
||||
import { ApplicationInput } from 'src/engine/core-modules/application/dtos/application.input';
|
||||
import { CreateApplicationInput } from 'src/engine/core-modules/application/dtos/create-application.input';
|
||||
import { GenerateApplicationTokenInput } from 'src/engine/core-modules/application/dtos/generate-application-token.input';
|
||||
import { InstallApplicationInput } from 'src/engine/core-modules/application/dtos/install-application.input';
|
||||
import { UninstallApplicationInput } from 'src/engine/core-modules/application/dtos/uninstallApplicationInput';
|
||||
import { UploadApplicationFileInput } from 'src/engine/core-modules/application/dtos/uploadApplicationFileInput';
|
||||
import { ApplicationSyncService } from 'src/engine/core-modules/application/services/application-sync.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { AuthToken } from 'src/engine/core-modules/auth/dto/auth-token.dto';
|
||||
import { ApplicationTokenService } from 'src/engine/core-modules/auth/token/services/application-token.service';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
||||
import { FileDTO } from 'src/engine/core-modules/file/dtos/file.dto';
|
||||
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { RequireFeatureFlag } from 'src/engine/guards/feature-flag.guard';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
@@ -39,7 +46,6 @@ import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/works
|
||||
import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor';
|
||||
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/services/workspace-migration-runner.service';
|
||||
import { streamToBuffer } from 'src/utils/stream-to-buffer';
|
||||
import { CreateApplicationInput } from 'src/engine/core-modules/application/dtos/create-application.input';
|
||||
|
||||
@UseGuards(
|
||||
WorkspaceAuthGuard,
|
||||
@@ -54,6 +60,8 @@ export class ApplicationResolver {
|
||||
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
|
||||
private readonly applicationSyncService: ApplicationSyncService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly applicationTokenService: ApplicationTokenService,
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
private readonly fileStorageService: FileStorageService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
) {}
|
||||
@@ -96,6 +104,33 @@ export class ApplicationResolver {
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation(() => AuthToken)
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_APPLICATION_ENABLED)
|
||||
async generateApplicationToken(
|
||||
@Args() { applicationId }: GenerateApplicationTokenInput,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
): Promise<AuthToken> {
|
||||
const nodeEnv = this.twentyConfigService.get('NODE_ENV');
|
||||
|
||||
if (
|
||||
nodeEnv !== NodeEnvironment.DEVELOPMENT &&
|
||||
nodeEnv !== NodeEnvironment.TEST
|
||||
) {
|
||||
throw new ApplicationException(
|
||||
'This endpoint is only available in development mode',
|
||||
ApplicationExceptionCode.FORBIDDEN,
|
||||
);
|
||||
}
|
||||
|
||||
const APPLICATION_TOKEN_EXPIRY_SECONDS = 30 * 24 * 60 * 60;
|
||||
|
||||
return this.applicationTokenService.generateApplicationToken({
|
||||
workspaceId,
|
||||
applicationId,
|
||||
expiresInSeconds: APPLICATION_TOKEN_EXPIRY_SECONDS,
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation(() => ApplicationDTO)
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_APPLICATION_ENABLED)
|
||||
async createOneApplication(
|
||||
|
||||
+3
-3
@@ -7,7 +7,7 @@ import {
|
||||
} from 'src/engine/core-modules/application/utils/compute-application-manifest-all-universal-flat-entity-maps.util';
|
||||
import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type';
|
||||
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
|
||||
import { getSubFlatEntityMapsByApplicationIdOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/get-sub-flat-entity-maps-by-application-id-or-throw.util';
|
||||
import { getSubFlatEntityMapsByApplicationIdsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/get-sub-flat-entity-maps-by-application-ids-or-throw.util';
|
||||
import { type AllUniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/all-universal-flat-entity-maps.type';
|
||||
|
||||
export type FromToApplicationManifestAllUniversalFlatEntityMaps = {
|
||||
@@ -34,10 +34,10 @@ export const getSubApplicationFromToAllFlatEntityMaps = ({
|
||||
const toFlatEntityMaps = toAllUniversalFlatEntityMaps[flatEntityMapsKey];
|
||||
|
||||
const fromTo = {
|
||||
from: getSubFlatEntityMapsByApplicationIdOrThrow<
|
||||
from: getSubFlatEntityMapsByApplicationIdsOrThrow<
|
||||
MetadataFlatEntity<typeof metadataName>
|
||||
>({
|
||||
applicationId,
|
||||
applicationIds: [applicationId],
|
||||
flatEntityMaps: fromFlatEntityMaps,
|
||||
}),
|
||||
to: toFlatEntityMaps,
|
||||
|
||||
Reference in New Issue
Block a user