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:
@@ -2103,6 +2103,7 @@ export type Mutation = {
|
||||
evaluateAgentTurn: AgentTurnEvaluation;
|
||||
executeOneLogicFunction: LogicFunctionExecutionResult;
|
||||
generateApiKeyToken: ApiKeyToken;
|
||||
generateApplicationToken: AuthToken;
|
||||
generateTransientToken: TransientTokenOutput;
|
||||
getAuthTokensFromLoginToken: AuthTokens;
|
||||
getAuthTokensFromOTP: AuthTokens;
|
||||
@@ -2610,6 +2611,11 @@ export type MutationGenerateApiKeyTokenArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationGenerateApplicationTokenArgs = {
|
||||
applicationId: Scalars['UUID'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationGetAuthTokensFromLoginTokenArgs = {
|
||||
loginToken: Scalars['String'];
|
||||
origin: Scalars['String'];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
diff --git a/dist/cjs/index.js b/dist/cjs/index.js
|
||||
index 1684394..dd0773d 100644
|
||||
index 1684394..8604546 100644
|
||||
--- a/dist/cjs/index.js
|
||||
+++ b/dist/cjs/index.js
|
||||
@@ -3,10 +3,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
||||
@@ -34,8 +34,9 @@ index 1684394..dd0773d 100644
|
||||
+ const workspaceId = request.req.workspace?.id ?? 'anonymous'
|
||||
+ const workspaceCacheVersion = request.req.workspaceMetadataVersion ?? '0'
|
||||
+ const url = request.req.baseUrl
|
||||
+ const applicationId = request.req.application?.id ?? 'all'
|
||||
+
|
||||
+ const cacheKey = `${workspaceId}-${url}-${workspaceCacheVersion}`
|
||||
+ const cacheKey = `${workspaceId}-${url}-${workspaceCacheVersion}-${applicationId}`
|
||||
+
|
||||
+ if(this.schemaCache.has(cacheKey)) {
|
||||
+ return this.schemaCache.get(cacheKey)
|
||||
@@ -54,7 +55,6 @@ index 1684394..dd0773d 100644
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+
|
||||
+ const mergedSchemas = (0, schema_1.mergeSchemas)({
|
||||
+ schemas,
|
||||
+ });
|
||||
@@ -86,8 +86,9 @@ index 1684394..dd0773d 100644
|
||||
+ const workspaceId = request.req.workspace?.id ?? 'anonymous'
|
||||
+ const workspaceCacheVersion = request.req.workspaceMetadataVersion ?? '0'
|
||||
+ const url = request.req.baseUrl
|
||||
+ const applicationId = request.req.application?.id ?? 'all'
|
||||
+
|
||||
+ const cacheKey = `${workspaceId}-${url}-${workspaceCacheVersion}`
|
||||
+ const cacheKey = `${workspaceId}-${url}-${workspaceCacheVersion}-${applicationId}`
|
||||
+
|
||||
+ if(this.schemaCache.has(cacheKey)) {
|
||||
+ return this.schemaCache.get(cacheKey)
|
||||
@@ -106,7 +107,6 @@ index 1684394..dd0773d 100644
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+
|
||||
+ const mergedSchemas = (0, schema_1.mergeSchemas)({
|
||||
+ schemas,
|
||||
+ });
|
||||
@@ -125,7 +125,7 @@ index 1684394..dd0773d 100644
|
||||
// disable logging by default
|
||||
// however, if `true` use fastify logger
|
||||
diff --git a/dist/esm/index.js b/dist/esm/index.js
|
||||
index 7068c51..8494b69 100644
|
||||
index 7068c51..95b4fbe 100644
|
||||
--- a/dist/esm/index.js
|
||||
+++ b/dist/esm/index.js
|
||||
@@ -1,9 +1,13 @@
|
||||
@@ -154,7 +154,7 @@ index 7068c51..8494b69 100644
|
||||
const app = this.httpAdapterHost.httpAdapter.getInstance();
|
||||
preStartHook?.(app);
|
||||
// nest's logger doesnt have the info method
|
||||
@@ -39,6 +43,45 @@ export class AbstractYogaDriver extends AbstractGraphQLDriver {
|
||||
@@ -39,6 +43,46 @@ export class AbstractYogaDriver extends AbstractGraphQLDriver {
|
||||
}
|
||||
const yoga = createYoga({
|
||||
...options,
|
||||
@@ -162,8 +162,9 @@ index 7068c51..8494b69 100644
|
||||
+ const workspaceId = request.req.workspace?.id ?? 'anonymous'
|
||||
+ const workspaceCacheVersion = request.req.workspaceMetadataVersion ?? '0'
|
||||
+ const url = request.req.baseUrl
|
||||
+ const applicationId = request.req.application?.id ?? 'all'
|
||||
+
|
||||
+ const cacheKey = `${workspaceId}-${url}-${workspaceCacheVersion}`
|
||||
+ const cacheKey = `${workspaceId}-${url}-${workspaceCacheVersion}-${applicationId}`
|
||||
+
|
||||
+ if (this.schemaCache.has(cacheKey)) {
|
||||
+ return this.schemaCache.get(cacheKey)
|
||||
@@ -200,7 +201,7 @@ index 7068c51..8494b69 100644
|
||||
graphqlEndpoint: options.path,
|
||||
// disable logging by default
|
||||
// however, if `true` use nest logger
|
||||
@@ -51,11 +94,50 @@ export class AbstractYogaDriver extends AbstractGraphQLDriver {
|
||||
@@ -51,11 +95,51 @@ export class AbstractYogaDriver extends AbstractGraphQLDriver {
|
||||
this.yoga = yoga;
|
||||
app.use(yoga.graphqlEndpoint, (req, res) => yoga(req, res, { req, res }));
|
||||
}
|
||||
@@ -214,8 +215,9 @@ index 7068c51..8494b69 100644
|
||||
+ const workspaceId = request.req.workspace?.id ?? 'anonymous'
|
||||
+ const workspaceCacheVersion = request.req.workspaceMetadataVersion ?? '0'
|
||||
+ const url = request.req.baseUrl
|
||||
+ const applicationId = request.req.application?.id ?? 'all'
|
||||
+
|
||||
+ const cacheKey = `${workspaceId}-${url}-${workspaceCacheVersion}`
|
||||
+ const cacheKey = `${workspaceId}-${url}-${workspaceCacheVersion}-${applicationId}`
|
||||
+
|
||||
+ if (this.schemaCache.has(cacheKey)) {
|
||||
+ return this.schemaCache.get(cacheKey)
|
||||
@@ -330,7 +332,7 @@ index 2c6a965..fd86dac 100644
|
||||
}): void;
|
||||
subscriptionWithFilter<TPayload, TVariables, TContext>(instanceRef: unknown, filterFn: (payload: TPayload, variables: TVariables, context: TContext) => boolean | Promise<boolean>, createSubscribeContext: Function): (args_0: TPayload, args_1: TVariables, args_2: TContext) => Promise<import("graphql-yoga").Repeater<TPayload, void, unknown>>;
|
||||
diff --git a/src/index.ts b/src/index.ts
|
||||
index ce142f6..d3e7bab 100644
|
||||
index ce142f6..95a1faf 100644
|
||||
--- a/src/index.ts
|
||||
+++ b/src/index.ts
|
||||
@@ -1,9 +1,10 @@
|
||||
@@ -349,7 +351,7 @@ index ce142f6..d3e7bab 100644
|
||||
@@ -11,23 +12,31 @@ import {
|
||||
SubscriptionConfig,
|
||||
} from '@nestjs/graphql';
|
||||
|
||||
|
||||
+export type YogaSchemaDefinition<TContext> =
|
||||
+ | PromiseOrValue<GraphQLSchemaWithContext<TContext>>
|
||||
+ | ((
|
||||
@@ -357,7 +359,7 @@ index ce142f6..d3e7bab 100644
|
||||
+ ) => PromiseOrValue<GraphQLSchemaWithContext<TContext>>);
|
||||
+
|
||||
export type YogaDriverPlatform = 'express' | 'fastify';
|
||||
|
||||
|
||||
export type YogaDriverServerContext<Platform extends YogaDriverPlatform> =
|
||||
Platform extends 'fastify'
|
||||
- ? {
|
||||
@@ -376,7 +378,7 @@ index ce142f6..d3e7bab 100644
|
||||
+ req: ExpressRequest;
|
||||
+ res: ExpressResponse;
|
||||
+ };
|
||||
|
||||
|
||||
export type YogaDriverServerOptions<Platform extends YogaDriverPlatform> = Omit<
|
||||
YogaServerOptions<YogaDriverServerContext<Platform>, never>,
|
||||
'context' | 'schema'
|
||||
@@ -384,7 +386,7 @@ index ce142f6..d3e7bab 100644
|
||||
+> & {
|
||||
+ conditionalSchema?: YogaSchemaDefinition<YogaDriverServerContext<Platform>> | undefined;
|
||||
+};
|
||||
|
||||
|
||||
export type YogaDriverServerInstance<Platform extends YogaDriverPlatform> = YogaServerInstance<
|
||||
YogaDriverServerContext<Platform>,
|
||||
@@ -53,6 +62,8 @@ export type YogaDriverSubscriptionConfig = {
|
||||
@@ -394,27 +396,28 @@ index ce142f6..d3e7bab 100644
|
||||
+ schemaCache = new Map();
|
||||
+
|
||||
protected yoga!: YogaDriverServerInstance<Platform>;
|
||||
|
||||
|
||||
public async start(options: YogaDriverConfig<Platform>) {
|
||||
@@ -78,7 +89,7 @@ export abstract class AbstractYogaDriver<
|
||||
}
|
||||
|
||||
|
||||
protected registerExpress(
|
||||
- options: YogaDriverConfig<'express'>,
|
||||
+ { conditionalSchema, ...options }: YogaDriverConfig<'express'>,
|
||||
{ preStartHook }: { preStartHook?: (app: Express) => void } = {},
|
||||
) {
|
||||
const app: Express = this.httpAdapterHost.httpAdapter.getInstance();
|
||||
@@ -98,6 +109,39 @@ export abstract class AbstractYogaDriver<
|
||||
|
||||
@@ -98,6 +109,40 @@ export abstract class AbstractYogaDriver<
|
||||
|
||||
const yoga = createYoga<YogaDriverServerContext<'express'>>({
|
||||
...options,
|
||||
+ schema: async request => {
|
||||
+ const workspaceId = request.req.workspace.id
|
||||
+ const workspaceCacheVersion = request.req.workspaceMetadataVersion
|
||||
+ const url = request.req.baseUrl
|
||||
+ const applicationId = request.req.application?.id ?? 'all'
|
||||
+
|
||||
+ const cacheKey = `${workspaceId}-${url}-${workspaceCacheVersion}`
|
||||
+ const cacheKey = `${workspaceId}-${url}-${workspaceCacheVersion}-${applicationId}`
|
||||
+
|
||||
+ if (this.schemaCache.has(cacheKey)) {
|
||||
+ return this.schemaCache.get(cacheKey)
|
||||
@@ -445,7 +448,7 @@ index ce142f6..d3e7bab 100644
|
||||
graphqlEndpoint: options.path,
|
||||
// disable logging by default
|
||||
// however, if `true` use nest logger
|
||||
@@ -105,8 +149,8 @@ export abstract class AbstractYogaDriver<
|
||||
@@ -105,8 +150,8 @@ export abstract class AbstractYogaDriver<
|
||||
options.logging == null
|
||||
? false
|
||||
: options.logging
|
||||
@@ -454,27 +457,28 @@ index ce142f6..d3e7bab 100644
|
||||
+ ? new LoggerWithInfo('YogaDriver')
|
||||
+ : options.logging,
|
||||
});
|
||||
|
||||
|
||||
this.yoga = yoga as YogaDriverServerInstance<Platform>;
|
||||
@@ -115,7 +159,7 @@ export abstract class AbstractYogaDriver<
|
||||
@@ -115,7 +160,7 @@ export abstract class AbstractYogaDriver<
|
||||
}
|
||||
|
||||
|
||||
protected registerFastify(
|
||||
- options: YogaDriverConfig<'fastify'>,
|
||||
+ { conditionalSchema, ...options }: YogaDriverConfig<'fastify'>,
|
||||
{ preStartHook }: { preStartHook?: (app: FastifyInstance) => void } = {},
|
||||
) {
|
||||
const app: FastifyInstance = this.httpAdapterHost.httpAdapter.getInstance();
|
||||
@@ -124,6 +168,39 @@ export abstract class AbstractYogaDriver<
|
||||
|
||||
@@ -124,6 +169,40 @@ export abstract class AbstractYogaDriver<
|
||||
|
||||
const yoga = createYoga<YogaDriverServerContext<'fastify'>>({
|
||||
...options,
|
||||
+ schema: async request => {
|
||||
+ const workspaceId = request.req.workspace.id
|
||||
+ const workspaceCacheVersion = request.req.workspaceMetadataVersion
|
||||
+ const url = request.req.baseUrl
|
||||
+ const applicationId = request.req.application?.id ?? 'all'
|
||||
+
|
||||
+ const cacheKey = `${workspaceId}-${url}-${workspaceCacheVersion}`
|
||||
+ const cacheKey = `${workspaceId}-${url}-${workspaceCacheVersion}-${applicationId}`
|
||||
+
|
||||
+ if (this.schemaCache.has(cacheKey)) {
|
||||
+ return this.schemaCache.get(cacheKey)
|
||||
@@ -505,7 +509,7 @@ index ce142f6..d3e7bab 100644
|
||||
graphqlEndpoint: options.path,
|
||||
// disable logging by default
|
||||
// however, if `true` use fastify logger
|
||||
@@ -191,8 +268,8 @@ export class YogaDriver<
|
||||
@@ -191,8 +270,8 @@ export class YogaDriver<
|
||||
const config: SubscriptionConfig =
|
||||
options.subscriptions === true
|
||||
? {
|
||||
@@ -514,5 +518,5 @@ index ce142f6..d3e7bab 100644
|
||||
+ 'graphql-ws': true,
|
||||
+ }
|
||||
: options.subscriptions;
|
||||
|
||||
|
||||
if (config['graphql-ws']) {
|
||||
|
||||
+4
-3
@@ -85,14 +85,14 @@ export class GraphQLConfigService
|
||||
resolverSchemaScope: 'core',
|
||||
buildSchemaOptions: {},
|
||||
conditionalSchema: async (context) => {
|
||||
const { workspace, user } = context.req;
|
||||
const { workspace, user, application } = context.req;
|
||||
|
||||
try {
|
||||
if (!isDefined(workspace)) {
|
||||
return new GraphQLSchema({});
|
||||
}
|
||||
|
||||
return await this.createSchema(context, workspace);
|
||||
return await this.createSchema(context, workspace, application?.id);
|
||||
} catch (error) {
|
||||
if (error instanceof UnauthorizedException) {
|
||||
throw new GraphQLError('Unauthenticated', {
|
||||
@@ -159,6 +159,7 @@ export class GraphQLConfigService
|
||||
async createSchema(
|
||||
context: YogaDriverServerContext<'express'> & YogaInitialContext,
|
||||
workspace: WorkspaceEntity,
|
||||
applicationId?: string,
|
||||
): Promise<GraphQLSchemaWithContext<YogaDriverServerContext<'express'>>> {
|
||||
// Create a new contextId for each request
|
||||
const contextId = ContextIdFactory.create();
|
||||
@@ -177,6 +178,6 @@ export class GraphQLConfigService
|
||||
},
|
||||
);
|
||||
|
||||
return await workspaceFactory.createGraphQLSchema(workspace);
|
||||
return await workspaceFactory.createGraphQLSchema(workspace, applicationId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,14 @@ import {
|
||||
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 {
|
||||
@@ -32,6 +38,7 @@ export class WorkspaceSchemaFactory {
|
||||
|
||||
async createGraphQLSchema(
|
||||
workspace: WorkspaceEntity,
|
||||
applicationId?: string,
|
||||
): Promise<GraphQLSchema> {
|
||||
const dataSourcesMetadata =
|
||||
await this.dataSourceService.getDataSourcesMetadataFromWorkspaceId(
|
||||
@@ -42,32 +49,68 @@ export class WorkspaceSchemaFactory {
|
||||
return new GraphQLSchema({});
|
||||
}
|
||||
|
||||
const { flatObjectMetadataMaps, flatFieldMetadataMaps, flatIndexMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId: workspace.id,
|
||||
flatMapsKeys: [
|
||||
'flatObjectMetadataMaps',
|
||||
'flatFieldMetadataMaps',
|
||||
'flatIndexMaps',
|
||||
],
|
||||
},
|
||||
);
|
||||
const {
|
||||
flatObjectMetadataMaps: allFlatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps: allFlatFieldMetadataMaps,
|
||||
flatIndexMaps: allFlatIndexMaps,
|
||||
flatApplicationMaps,
|
||||
} = await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId: workspace.id,
|
||||
flatMapsKeys: [
|
||||
'flatObjectMetadataMaps',
|
||||
'flatFieldMetadataMaps',
|
||||
'flatIndexMaps',
|
||||
'flatApplicationMaps',
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
if (!isDefined(flatObjectMetadataMaps)) {
|
||||
if (!isDefined(allFlatObjectMetadataMaps)) {
|
||||
throw new FlatEntityMapsException(
|
||||
'Object metadata collection not found',
|
||||
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
if (!isDefined(flatFieldMetadataMaps)) {
|
||||
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,
|
||||
);
|
||||
|
||||
if (isDefined(allFlatIndexMaps)) {
|
||||
flatIndexMaps = this.filterFlatEntityMapsByApplicationIds(
|
||||
allFlatIndexMaps,
|
||||
applicationIds,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let metadataVersion =
|
||||
await this.workspaceCacheStorageService.getMetadataVersion(workspace.id);
|
||||
|
||||
@@ -88,11 +131,13 @@ export class WorkspaceSchemaFactory {
|
||||
let typeDefs = await this.workspaceCacheStorageService.getGraphQLTypeDefs(
|
||||
workspace.id,
|
||||
metadataVersion,
|
||||
applicationId,
|
||||
);
|
||||
let usedScalarNames =
|
||||
await this.workspaceCacheStorageService.getGraphQLUsedScalarNames(
|
||||
workspace.id,
|
||||
metadataVersion,
|
||||
applicationId,
|
||||
);
|
||||
|
||||
if (!typeDefs || !usedScalarNames) {
|
||||
@@ -111,11 +156,13 @@ export class WorkspaceSchemaFactory {
|
||||
workspace.id,
|
||||
metadataVersion,
|
||||
typeDefs,
|
||||
applicationId,
|
||||
);
|
||||
await this.workspaceCacheStorageService.setGraphQLUsedScalarNames(
|
||||
workspace.id,
|
||||
metadataVersion,
|
||||
usedScalarNames,
|
||||
applicationId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -140,4 +187,16 @@ export class WorkspaceSchemaFactory {
|
||||
|
||||
return executableSchema;
|
||||
}
|
||||
|
||||
private filterFlatEntityMapsByApplicationIds<
|
||||
T extends FlatObjectMetadata | FlatFieldMetadata | FlatIndexMetadata,
|
||||
>(
|
||||
flatEntityMaps: FlatEntityMaps<T>,
|
||||
applicationIds: string[],
|
||||
): FlatEntityMaps<T> {
|
||||
return getSubFlatEntityMapsByApplicationIdsOrThrow({
|
||||
applicationIds,
|
||||
flatEntityMaps,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
+13
-7
@@ -3,22 +3,28 @@ import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/typ
|
||||
import { findFlatEntitiesByApplicationId } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entities-by-application-id.util';
|
||||
import { getSubFlatEntityByIdsMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/get-sub-flat-entity-by-ids-maps-or-throw.util';
|
||||
|
||||
export const getSubFlatEntityMapsByApplicationIdOrThrow = <
|
||||
export const getSubFlatEntityMapsByApplicationIdsOrThrow = <
|
||||
T extends SyncableFlatEntity,
|
||||
>({
|
||||
applicationId,
|
||||
applicationIds,
|
||||
flatEntityMaps,
|
||||
}: {
|
||||
applicationId: string;
|
||||
applicationIds: string[];
|
||||
flatEntityMaps: FlatEntityMaps<T>;
|
||||
}) => {
|
||||
const allApplicationFlatEntity = findFlatEntitiesByApplicationId({
|
||||
applicationId,
|
||||
flatEntityMaps,
|
||||
const allFlatEntityIds = applicationIds.flatMap((applicationId) => {
|
||||
const entities = findFlatEntitiesByApplicationId({
|
||||
applicationId,
|
||||
flatEntityMaps,
|
||||
});
|
||||
|
||||
return entities.map((entity) => entity.id);
|
||||
});
|
||||
|
||||
const uniqueFlatEntityIds = [...new Set(allFlatEntityIds)];
|
||||
|
||||
return getSubFlatEntityByIdsMapsOrThrow({
|
||||
flatEntityIds: allApplicationFlatEntity.map((flatEntity) => flatEntity.id),
|
||||
flatEntityIds: uniqueFlatEntityIds,
|
||||
flatEntityMaps,
|
||||
});
|
||||
};
|
||||
+16
-4
@@ -91,9 +91,12 @@ export class WorkspaceCacheStorageService {
|
||||
workspaceId: string,
|
||||
metadataVersion: number,
|
||||
typeDefs: string,
|
||||
applicationId?: string,
|
||||
): Promise<void> {
|
||||
const applicationSuffix = applicationId ? `:${applicationId}` : '';
|
||||
|
||||
return this.cacheStorageService.set<string>(
|
||||
`${METADATA_VERSIONED_WORKSPACE_CACHE_KEY.GraphQLTypeDefs}:${workspaceId}:${metadataVersion}`,
|
||||
`${METADATA_VERSIONED_WORKSPACE_CACHE_KEY.GraphQLTypeDefs}:${workspaceId}:${metadataVersion}${applicationSuffix}`,
|
||||
typeDefs,
|
||||
TTL_ONE_WEEK,
|
||||
);
|
||||
@@ -102,9 +105,12 @@ export class WorkspaceCacheStorageService {
|
||||
getGraphQLTypeDefs(
|
||||
workspaceId: string,
|
||||
metadataVersion: number,
|
||||
applicationId?: string,
|
||||
): Promise<string | undefined> {
|
||||
const applicationSuffix = applicationId ? `:${applicationId}` : '';
|
||||
|
||||
return this.cacheStorageService.get<string>(
|
||||
`${METADATA_VERSIONED_WORKSPACE_CACHE_KEY.GraphQLTypeDefs}:${workspaceId}:${metadataVersion}`,
|
||||
`${METADATA_VERSIONED_WORKSPACE_CACHE_KEY.GraphQLTypeDefs}:${workspaceId}:${metadataVersion}${applicationSuffix}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -112,9 +118,12 @@ export class WorkspaceCacheStorageService {
|
||||
workspaceId: string,
|
||||
metadataVersion: number,
|
||||
usedScalarNames: string[],
|
||||
applicationId?: string,
|
||||
): Promise<void> {
|
||||
const applicationSuffix = applicationId ? `:${applicationId}` : '';
|
||||
|
||||
return this.cacheStorageService.set<string[]>(
|
||||
`${METADATA_VERSIONED_WORKSPACE_CACHE_KEY.GraphQLUsedScalarNames}:${workspaceId}:${metadataVersion}`,
|
||||
`${METADATA_VERSIONED_WORKSPACE_CACHE_KEY.GraphQLUsedScalarNames}:${workspaceId}:${metadataVersion}${applicationSuffix}`,
|
||||
usedScalarNames,
|
||||
TTL_ONE_WEEK,
|
||||
);
|
||||
@@ -123,9 +132,12 @@ export class WorkspaceCacheStorageService {
|
||||
getGraphQLUsedScalarNames(
|
||||
workspaceId: string,
|
||||
metadataVersion: number,
|
||||
applicationId?: string,
|
||||
): Promise<string[] | undefined> {
|
||||
const applicationSuffix = applicationId ? `:${applicationId}` : '';
|
||||
|
||||
return this.cacheStorageService.get<string[]>(
|
||||
`${METADATA_VERSIONED_WORKSPACE_CACHE_KEY.GraphQLUsedScalarNames}:${workspaceId}:${metadataVersion}`,
|
||||
`${METADATA_VERSIONED_WORKSPACE_CACHE_KEY.GraphQLUsedScalarNames}:${workspaceId}:${metadataVersion}${applicationSuffix}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -8,7 +8,7 @@ import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twent
|
||||
import { MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type';
|
||||
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
|
||||
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 { FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
|
||||
@@ -94,10 +94,10 @@ export class TwentyStandardApplicationService {
|
||||
const fromFlatEntityMaps =
|
||||
fromTwentyStandardAllFlatEntityMaps[flatEntityMapsKey];
|
||||
const fromTo = {
|
||||
from: getSubFlatEntityMapsByApplicationIdOrThrow<
|
||||
from: getSubFlatEntityMapsByApplicationIdsOrThrow<
|
||||
MetadataFlatEntity<typeof metadataName>
|
||||
>({
|
||||
applicationId: twentyStandardFlatApplication.id,
|
||||
applicationIds: [twentyStandardFlatApplication.id],
|
||||
flatEntityMaps: fromFlatEntityMaps,
|
||||
}),
|
||||
to: toTwentyStandardAllFlatEntityMaps[flatEntityMapsKey],
|
||||
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
import request from 'supertest';
|
||||
import { findManyApplications } from 'test/integration/graphql/utils/find-many-applications.util';
|
||||
import { generateApplicationToken } from 'test/integration/metadata/suites/application/utils/generate-application-token.util';
|
||||
|
||||
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
|
||||
|
||||
const INTROSPECTION_QUERY = `
|
||||
query IntrospectionQuery {
|
||||
__schema {
|
||||
types {
|
||||
kind
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Custom objects seeded in the dev workspace that should NOT appear
|
||||
// in a schema scoped to the Twenty Standard Application
|
||||
const CUSTOM_OBJECT_TYPE_NAMES = [
|
||||
'Rocket',
|
||||
'Pet',
|
||||
'PetCareAgreement',
|
||||
'SurveyResult',
|
||||
'EmploymentHistory',
|
||||
];
|
||||
|
||||
const makeGraphqlIntrospectionRequest = async (
|
||||
token: string,
|
||||
): Promise<request.Response> => {
|
||||
const client = request(`http://localhost:${APP_PORT}`);
|
||||
|
||||
const response = await client
|
||||
.post('/graphql')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.send({ query: INTROSPECTION_QUERY });
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
describe('Application token schema filtering', () => {
|
||||
let standardAppToken: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const { data: applicationsData } = await findManyApplications({
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const standardApp = applicationsData.findManyApplications.find(
|
||||
(application) =>
|
||||
application.universalIdentifier ===
|
||||
TWENTY_STANDARD_APPLICATION.universalIdentifier,
|
||||
);
|
||||
|
||||
expect(standardApp).toBeDefined();
|
||||
|
||||
const { data: tokenData } = await generateApplicationToken({
|
||||
applicationId: standardApp!.id,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
standardAppToken = tokenData.generateApplicationToken.token;
|
||||
});
|
||||
|
||||
it('should not include custom objects in the schema when using a standard app token', async () => {
|
||||
const response = await makeGraphqlIntrospectionRequest(standardAppToken);
|
||||
|
||||
expect(response.body.errors).toBeUndefined();
|
||||
expect(response.body.data.__schema).toBeDefined();
|
||||
|
||||
const typeNames: string[] = response.body.data.__schema.types.map(
|
||||
(type: { name: string }) => type.name,
|
||||
);
|
||||
|
||||
for (const customTypeName of CUSTOM_OBJECT_TYPE_NAMES) {
|
||||
expect(typeNames).not.toContain(customTypeName);
|
||||
expect(typeNames).not.toContain(`${customTypeName}Edge`);
|
||||
expect(typeNames).not.toContain(`${customTypeName}Connection`);
|
||||
}
|
||||
});
|
||||
|
||||
it('should include standard objects in the schema when using a standard app token', async () => {
|
||||
const response = await makeGraphqlIntrospectionRequest(standardAppToken);
|
||||
|
||||
expect(response.body.errors).toBeUndefined();
|
||||
|
||||
const typeNames: string[] = response.body.data.__schema.types.map(
|
||||
(type: { name: string }) => type.name,
|
||||
);
|
||||
|
||||
expect(typeNames).toContain('Person');
|
||||
expect(typeNames).toContain('Company');
|
||||
expect(typeNames).toContain('Opportunity');
|
||||
});
|
||||
});
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import { findManyApplications } from 'test/integration/graphql/utils/find-many-applications.util';
|
||||
import { generateApplicationToken } from 'test/integration/metadata/suites/application/utils/generate-application-token.util';
|
||||
|
||||
describe('generateApplicationToken', () => {
|
||||
let applicationId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const { data } = await findManyApplications({
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const application = data.findManyApplications[0];
|
||||
|
||||
expect(application).toBeDefined();
|
||||
|
||||
applicationId = application.id;
|
||||
});
|
||||
|
||||
it('should generate an application token with admin access token', async () => {
|
||||
const { data } = await generateApplicationToken({
|
||||
applicationId,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
expect(data.generateApplicationToken).toBeDefined();
|
||||
expect(data.generateApplicationToken.token).toBeDefined();
|
||||
expect(typeof data.generateApplicationToken.token).toBe('string');
|
||||
expect(data.generateApplicationToken.token.length).toBeGreaterThan(0);
|
||||
expect(data.generateApplicationToken.expiresAt).toBeDefined();
|
||||
});
|
||||
|
||||
it('should generate an application token with API key access token', async () => {
|
||||
const { data } = await generateApplicationToken({
|
||||
applicationId,
|
||||
expectToFail: false,
|
||||
token: API_KEY_ACCESS_TOKEN,
|
||||
});
|
||||
|
||||
expect(data.generateApplicationToken).toBeDefined();
|
||||
expect(data.generateApplicationToken.token).toBeDefined();
|
||||
expect(typeof data.generateApplicationToken.token).toBe('string');
|
||||
expect(data.generateApplicationToken.token.length).toBeGreaterThan(0);
|
||||
expect(data.generateApplicationToken.expiresAt).toBeDefined();
|
||||
});
|
||||
|
||||
it('should fail with a non-existent application id', async () => {
|
||||
const { errors } = await generateApplicationToken({
|
||||
applicationId: '00000000-0000-0000-0000-000000000000',
|
||||
expectToFail: true,
|
||||
});
|
||||
|
||||
expect(errors).toBeDefined();
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const generateApplicationTokenQueryFactory = ({
|
||||
applicationId,
|
||||
}: {
|
||||
applicationId: string;
|
||||
}) => ({
|
||||
query: gql`
|
||||
mutation GenerateApplicationToken($applicationId: UUID!) {
|
||||
generateApplicationToken(applicationId: $applicationId) {
|
||||
token
|
||||
expiresAt
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
applicationId,
|
||||
},
|
||||
});
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { generateApplicationTokenQueryFactory } from 'test/integration/metadata/suites/application/utils/generate-application-token-query-factory.util';
|
||||
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
|
||||
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
|
||||
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
|
||||
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
|
||||
|
||||
import { type AuthToken } from 'src/engine/core-modules/auth/dto/auth-token.dto';
|
||||
|
||||
export const generateApplicationToken = async ({
|
||||
applicationId,
|
||||
expectToFail = false,
|
||||
token,
|
||||
}: {
|
||||
applicationId: string;
|
||||
expectToFail?: boolean;
|
||||
token?: string;
|
||||
}): CommonResponseBody<{
|
||||
generateApplicationToken: AuthToken;
|
||||
}> => {
|
||||
const graphqlOperation = generateApplicationTokenQueryFactory({
|
||||
applicationId,
|
||||
});
|
||||
|
||||
const response = await makeMetadataAPIRequest(graphqlOperation, token);
|
||||
|
||||
if (expectToFail === true) {
|
||||
warnIfNoErrorButExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'Generate application token should have failed but did not',
|
||||
});
|
||||
}
|
||||
|
||||
if (expectToFail === false) {
|
||||
warnIfErrorButNotExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'Generate application token has failed but should not',
|
||||
});
|
||||
}
|
||||
|
||||
return { data: response.body.data, errors: response.body.errors };
|
||||
};
|
||||
@@ -7016,14 +7016,14 @@ __metadata:
|
||||
|
||||
"@graphql-yoga/nestjs@patch:@graphql-yoga/nestjs@2.1.0#./patches/@graphql-yoga+nestjs+2.1.0.patch::locator=twenty-server%40workspace%3Apackages%2Ftwenty-server":
|
||||
version: 2.1.0
|
||||
resolution: "@graphql-yoga/nestjs@patch:@graphql-yoga/nestjs@npm%3A2.1.0#./patches/@graphql-yoga+nestjs+2.1.0.patch::version=2.1.0&hash=d6801e&locator=twenty-server%40workspace%3Apackages%2Ftwenty-server"
|
||||
resolution: "@graphql-yoga/nestjs@patch:@graphql-yoga/nestjs@npm%3A2.1.0#./patches/@graphql-yoga+nestjs+2.1.0.patch::version=2.1.0&hash=aafdfe&locator=twenty-server%40workspace%3Apackages%2Ftwenty-server"
|
||||
peerDependencies:
|
||||
"@nestjs/common": ^10.0.0
|
||||
"@nestjs/core": ^10.0.0
|
||||
"@nestjs/graphql": ^12.0.0
|
||||
graphql: ^15.0.0 || ^16.0.0
|
||||
graphql-yoga: ^4.0.4
|
||||
checksum: 10c0/929da1f7265003cb5f43291d6db96e752bd2ac5d81e21fb3af9b245e54e732f5862789d7e5941e893d85255919b4c6a5a37adb16ef6594fab606e8eec742891c
|
||||
checksum: 10c0/7ebb5bd5bf630cbad7ef99ac4a04017c1294a6a40623b0142cf4990d790fb4252efa71abe3a7923f66845320fb9cba536426743eb5d93a1e9353700b730efb28
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user