feat: add new ACTOR field type and createdBy standard fields (#6324)

This pull request introduces a new `FieldMetadataType` called `ACTOR`.
The primary objective of this new type is to add an extra column to the
following objects: `person`, `company`, `opportunity`, `note`, `task`,
and all custom objects.

This composite type contains three properties:

- `source`
    ```typescript
    export enum FieldActorSource {
      EMAIL = 'EMAIL',
      CALENDAR = 'CALENDAR',
      API = 'API',
      IMPORT = 'IMPORT',
      MANUAL = 'MANUAL',
    }
    ```
- `workspaceMemberId`
- This property can be `undefined` in some cases and refers to the
member who created the record.
- `name`
- Serves as a fallback if the `workspaceMember` is deleted and is used
for other source types like `API`.

### Functionality

The pre-hook system has been updated to allow real-time argument
updates. When a record is created, a pre-hook can now compute and update
the arguments accordingly. This enhancement enables the `createdBy`
field to be populated with the correct values based on the
`authContext`.

The `authContext` now includes:
- An optional User entity
- An optional ApiKey entity
- The workspace entity

This provides access to the necessary data for the `createdBy` field.

In the GraphQL API, only the `source` can be specified in the
`createdBy` input. This allows the front-end to specify the source when
creating records from a CSV file.

### Front-End Handling

On the front-end, `orderBy` and `filter` are only applied to the name
property of the `ACTOR` composite type. Currently, we are unable to
apply these operations to the workspace member relation. This means that
if a workspace member changes their first name or last name, there may
be a mismatch because the name will differ from the new one. The name
displayed on the screen is based on the workspace member entity when
available.

### Missing Components

Currently, this PR does not include a `createdBy` value for the `MAIL`
and `CALENDAR` sources. These records are created in a job, and at
present, we only have access to the workspaceId within the job. To
address this, we should use a function similar to
`loadServiceWithContext`, which was recently removed from `TwentyORM`.
This function would allow us to pass the `authContext` to the jobs
without disrupting existing jobs.
Another PR will be created to handle these cases.

### Related Issues

Fixes issue #5155.

### Additional Notes

This PR doesn't include the migrations of the current records and views.
Everything works properly when the database is reset but this part is
still missing for now. We'll add that in another PR.

- There is a minor issue: front-end tests are broken since this commit:
[80c0fc7ff1).

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Jérémy M
2024-08-03 15:43:31 +02:00
committed by GitHub
parent 9cf08d912a
commit 6432ad39b9
152 changed files with 24425 additions and 14968 deletions

View File

@ -15,7 +15,7 @@ import { JsonWebTokenError, TokenExpiredError } from 'jsonwebtoken';
import { useThrottler } from 'src/engine/api/graphql/graphql-config/hooks/use-throttler';
import { WorkspaceSchemaFactory } from 'src/engine/api/graphql/workspace-schema.factory';
import { TokenService } from 'src/engine/core-modules/auth/services/token.service';
import { JwtData } from 'src/engine/core-modules/auth/types/jwt-data.type';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { CoreEngineModule } from 'src/engine/core-modules/core-engine.module';
import { useGraphQLErrorHandlerHook } from 'src/engine/core-modules/graphql/hooks/use-graphql-error-handler.hook';
import { User } from 'src/engine/core-modules/user/user.entity';
@ -65,7 +65,7 @@ export class GraphQLConfigService
autoSchemaFile: true,
include: [CoreEngineModule],
conditionalSchema: async (context) => {
let user: User | undefined;
let user: User | null | undefined;
let workspace: Workspace | undefined;
try {
@ -136,7 +136,7 @@ export class GraphQLConfigService
async createSchema(
context: YogaDriverServerContext<'express'> & YogaInitialContext,
data: JwtData,
data: AuthContext,
): Promise<GraphQLSchemaWithContext<YogaDriverServerContext<'express'>>> {
// Create a new contextId for each request
const contextId = ContextIdFactory.create();
@ -155,9 +155,6 @@ export class GraphQLConfigService
},
);
return await workspaceFactory.createGraphQLSchema(
data.workspace.id,
data.user?.id,
);
return await workspaceFactory.createGraphQLSchema(data);
}
}

View File

@ -19,7 +19,7 @@ describe('QueryRunnerArgsFactory', () => {
{ name: 'testNumber', type: FieldMetadataType.NUMBER },
] as FieldMetadataInterface[],
objectMetadataItem: { isCustom: true, nameSingular: 'test' },
workspaceId,
authContext: { workspace: { id: workspaceId } },
} as WorkspaceQueryRunnerOptions;
let factory: QueryRunnerArgsFactory;
@ -149,6 +149,7 @@ describe('QueryRunnerArgsFactory', () => {
{ name: 'testNumber', type: FieldMetadataType.NUMBER },
] as FieldMetadataInterface[],
objectMetadataItem: { isCustom: true, nameSingular: 'test' },
authContext: { workspace: { id: workspaceId } },
} as WorkspaceQueryRunnerOptions;
const args = {

View File

@ -129,7 +129,7 @@ export class QueryRunnerArgsFactory {
isCustom: options.objectMetadataItem.isCustom,
nameSingular: options.objectMetadataItem.nameSingular,
},
options.workspaceId,
options.authContext.workspace.id,
argPositionBackfillInput.argIndex,
),
];
@ -157,7 +157,7 @@ export class QueryRunnerArgsFactory {
isCustom: options.objectMetadataItem.isCustom,
nameSingular: options.objectMetadataItem.nameSingular,
},
options.workspaceId,
options.authContext.workspace.id,
argPositionBackfillInput.argIndex,
),
],

View File

@ -3,9 +3,10 @@ import { GraphQLResolveInfo } from 'graphql';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
export interface WorkspaceQueryRunnerOptions {
workspaceId: string;
userId: string | undefined;
authContext: AuthContext;
info: GraphQLResolveInfo;
objectMetadataItem: ObjectMetadataInterface;
fieldMetadataCollection: FieldMetadataInterface[];

View File

@ -1,9 +1,11 @@
import { ResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
export interface WorkspaceQueryHookInstance {
execute(
userId: string | undefined,
workspaceId: string,
authContext: AuthContext,
objectName: string,
payload: ResolverArgs,
): Promise<void>;
): Promise<ResolverArgs>;
}

View File

@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common';
import { Module } from '@nestjs/core/injector/module';
import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { WorkspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceQueryHookKey } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
@ -36,8 +37,23 @@ export class WorkspaceQueryHookStorage {
getWorkspaceQueryPreHookInstances(
key: WorkspaceQueryHookKey,
): WorkspaceQueryHookData<WorkspaceQueryHookInstance>[] | undefined {
return this.preHookInstances.get(key);
): WorkspaceQueryHookData<WorkspaceQueryHookInstance>[] {
const methodName = key.split('.')?.[1] as
| WorkspaceResolverBuilderMethodNames
| undefined;
let wildcardInstances: WorkspaceQueryHookData<WorkspaceQueryHookInstance>[] =
[];
if (!methodName) {
throw new Error(`Can't split workspace query hook key: ${key}`);
}
// Retrive wildcard pre-hook instances
if (this.preHookInstances.has(`*.${methodName}`)) {
wildcardInstances = this.preHookInstances.get(`*.${methodName}`)!;
}
return [...wildcardInstances, ...(this.preHookInstances.get(key) ?? [])];
}
registerWorkspaceQueryPostHookInstance(

View File

@ -1,14 +1,14 @@
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { DiscoveryService, ModuleRef, createContextId } from '@nestjs/core';
import { Module } from '@nestjs/core/injector/module';
import { Injector } from '@nestjs/core/injector/injector';
import { Module } from '@nestjs/core/injector/module';
import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { WorkspaceQueryHookMetadataAccessor } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook-metadata.accessor';
import { WorkspaceQueryHookStorage } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/storage/workspace-query-hook.storage';
import { WorkspaceQueryHookKey } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import { WorkspaceQueryHookStorage } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/storage/workspace-query-hook.storage';
import { WorkspaceQueryHookType } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
import { WorkspaceQueryHookMetadataAccessor } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook-metadata.accessor';
@Injectable()
export class WorkspaceQueryHookExplorer implements OnModuleInit {
@ -73,7 +73,7 @@ export class WorkspaceQueryHookExplorer implements OnModuleInit {
instance: object,
host: Module,
isRequestScoped: boolean,
) {
): Promise<ReturnType<WorkspaceQueryHookInstance['execute']>> {
const methodName = 'execute';
if (isRequestScoped) {
@ -83,7 +83,7 @@ export class WorkspaceQueryHookExplorer implements OnModuleInit {
this.moduleRef.registerRequestByContextId(
{
req: {
workspaceId: payload?.[1],
workspaceId: payload?.[0].workspace.id,
},
},
contextId,
@ -97,9 +97,9 @@ export class WorkspaceQueryHookExplorer implements OnModuleInit {
contextId,
);
await contextInstance[methodName].call(contextInstance, ...payload);
return contextInstance[methodName].call(contextInstance, ...payload);
} else {
await instance[methodName].call(instance, ...payload);
return instance[methodName].call(instance, ...payload);
}
}

View File

@ -1,11 +1,14 @@
import { Injectable } from '@nestjs/common';
import merge from 'lodash.merge';
import { WorkspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceQueryHookStorage } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/storage/workspace-query-hook.storage';
import { WorkspaceQueryHookKey } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import { WorkspaceQueryHookExplorer } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.explorer';
import { WorkspacePreQueryHookPayload } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
@Injectable()
export class WorkspaceQueryHookService {
@ -17,27 +20,33 @@ export class WorkspaceQueryHookService {
public async executePreQueryHooks<
T extends WorkspaceResolverBuilderMethodNames,
>(
userId: string | undefined,
workspaceId: string,
authContext: AuthContext,
// TODO: We should allow wildcard for object name
objectName: string,
methodName: T,
payload: WorkspacePreQueryHookPayload<T>,
): Promise<void> {
): Promise<WorkspacePreQueryHookPayload<T>> {
const key: WorkspaceQueryHookKey = `${objectName}.${methodName}`;
const preHookInstances =
this.workspaceQueryHookStorage.getWorkspaceQueryPreHookInstances(key);
if (!preHookInstances) {
return;
return payload;
}
for (const preHookInstance of preHookInstances) {
await this.workspaceQueryHookExplorer.handleHook(
[userId, workspaceId, payload],
// Deep merge all return of handleHook into payload before returning it
const hookPayload = await this.workspaceQueryHookExplorer.handleHook(
[authContext, objectName, payload],
preHookInstance.instance,
preHookInstance.host,
preHookInstance.isRequestScoped,
);
// TODO: Is it really a good idea ?
payload = merge(payload, hookPayload);
}
return payload;
}
}

View File

@ -52,7 +52,6 @@ import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
import { isQueryTimeoutError } from 'src/engine/utils/query-timeout.util';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
import {
PGGraphQLMutation,
@ -90,11 +89,19 @@ export class WorkspaceQueryRunnerService {
args: FindManyResolverArgs<Filter, OrderBy>,
options: WorkspaceQueryRunnerOptions,
): Promise<IConnection<Record> | undefined> {
const { workspaceId, userId, objectMetadataItem } = options;
const { authContext, objectMetadataItem } = options;
const start = performance.now();
const hookedArgs =
await this.workspaceQueryHookService.executePreQueryHooks(
authContext,
objectMetadataItem.nameSingular,
'findMany',
args,
);
const computedArgs = (await this.queryRunnerArgsFactory.create(
args,
hookedArgs,
options,
ResolverArgsType.FindMany,
)) as FindManyResolverArgs<Filter, OrderBy>;
@ -104,15 +111,7 @@ export class WorkspaceQueryRunnerService {
options,
);
await this.workspaceQueryHookService.executePreQueryHooks(
userId,
workspaceId,
objectMetadataItem.nameSingular,
'findMany',
args,
);
const result = await this.execute(query, workspaceId);
const result = await this.execute(query, authContext.workspace.id);
const end = performance.now();
this.logger.log(
@ -125,7 +124,7 @@ export class WorkspaceQueryRunnerService {
result,
objectMetadataItem,
'',
workspaceId,
authContext.workspace.id,
);
}
@ -142,10 +141,18 @@ export class WorkspaceQueryRunnerService {
WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
);
}
const { workspaceId, userId, objectMetadataItem } = options;
const { authContext, objectMetadataItem } = options;
const hookedArgs =
await this.workspaceQueryHookService.executePreQueryHooks(
authContext,
objectMetadataItem.nameSingular,
'findOne',
args,
);
const computedArgs = (await this.queryRunnerArgsFactory.create(
args,
hookedArgs,
options,
ResolverArgsType.FindOne,
)) as FindOneResolverArgs<Filter>;
@ -155,20 +162,12 @@ export class WorkspaceQueryRunnerService {
options,
);
await this.workspaceQueryHookService.executePreQueryHooks(
userId,
workspaceId,
objectMetadataItem.nameSingular,
'findOne',
args,
);
const result = await this.execute(query, workspaceId);
const result = await this.execute(query, authContext.workspace.id);
const parsedResult = await this.parseResult<IConnection<Record>>(
result,
objectMetadataItem,
'',
workspaceId,
authContext.workspace.id,
);
return parsedResult?.edges?.[0]?.node;
@ -192,10 +191,18 @@ export class WorkspaceQueryRunnerService {
);
}
const { workspaceId, userId, objectMetadataItem } = options;
const { authContext, objectMetadataItem } = options;
const hookedArgs =
await this.workspaceQueryHookService.executePreQueryHooks(
authContext,
objectMetadataItem.nameSingular,
'findDuplicates',
args,
);
const computedArgs = (await this.queryRunnerArgsFactory.create(
args,
hookedArgs,
options,
ResolverArgsType.FindDuplicates,
)) as FindDuplicatesResolverArgs<TRecord>;
@ -206,7 +213,7 @@ export class WorkspaceQueryRunnerService {
existingRecords = await this.duplicateService.findExistingRecords(
computedArgs.ids,
objectMetadataItem,
workspaceId,
authContext.workspace.id,
);
if (!existingRecords || existingRecords.length === 0) {
@ -223,21 +230,13 @@ export class WorkspaceQueryRunnerService {
existingRecords,
);
await this.workspaceQueryHookService.executePreQueryHooks(
userId,
workspaceId,
objectMetadataItem.nameSingular,
'findDuplicates',
computedArgs,
);
const result = await this.execute(query, workspaceId);
const result = await this.execute(query, authContext.workspace.id);
return this.parseResult<IConnection<TRecord>>(
result,
objectMetadataItem,
'',
workspaceId,
authContext.workspace.id,
true,
);
}
@ -246,7 +245,7 @@ export class WorkspaceQueryRunnerService {
args: CreateManyResolverArgs<Partial<Record>>,
options: WorkspaceQueryRunnerOptions,
): Promise<Record[] | undefined> {
const { workspaceId, userId, objectMetadataItem } = options;
const { authContext, objectMetadataItem } = options;
assertMutationNotOnRemoteObject(objectMetadataItem);
@ -260,33 +259,33 @@ export class WorkspaceQueryRunnerService {
}
});
const hookedArgs =
await this.workspaceQueryHookService.executePreQueryHooks(
authContext,
objectMetadataItem.nameSingular,
'createMany',
args,
);
const computedArgs = (await this.queryRunnerArgsFactory.create(
args,
hookedArgs,
options,
ResolverArgsType.CreateMany,
)) as CreateManyResolverArgs<Record>;
await this.workspaceQueryHookService.executePreQueryHooks(
userId,
workspaceId,
objectMetadataItem.nameSingular,
'createMany',
args,
);
const query = await this.workspaceQueryBuilderFactory.createMany(
computedArgs,
options,
);
const result = await this.execute(query, workspaceId);
const result = await this.execute(query, authContext.workspace.id);
const parsedResults = (
await this.parseResult<PGGraphQLMutation<Record>>(
result,
objectMetadataItem,
'insertInto',
workspaceId,
authContext.workspace.id,
)
)?.records;
@ -299,8 +298,8 @@ export class WorkspaceQueryRunnerService {
parsedResults.forEach((record) => {
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.created`, {
name: `${objectMetadataItem.nameSingular}.created`,
workspaceId,
userId,
workspaceId: authContext.workspace.id,
userId: authContext.user?.id,
recordId: record.id,
objectMetadata: objectMetadataItem,
properties: {
@ -325,7 +324,7 @@ export class WorkspaceQueryRunnerService {
? await this.duplicateService.findExistingRecords(
ids as string[],
options.objectMetadataItem,
options.workspaceId,
options.authContext.workspace.id,
)
: [];
@ -381,10 +380,10 @@ export class WorkspaceQueryRunnerService {
args: UpdateOneResolverArgs<Partial<Record>>,
options: WorkspaceQueryRunnerOptions,
): Promise<Record | undefined> {
const { workspaceId, userId, objectMetadataItem } = options;
const { authContext, objectMetadataItem } = options;
const repository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
authContext.workspace.id,
objectMetadataItem.nameSingular,
);
@ -402,27 +401,27 @@ export class WorkspaceQueryRunnerService {
);
}
const hookedArgs =
await this.workspaceQueryHookService.executePreQueryHooks(
authContext,
objectMetadataItem.nameSingular,
'updateOne',
args,
);
const query = await this.workspaceQueryBuilderFactory.updateOne(
args,
hookedArgs,
options,
);
await this.workspaceQueryHookService.executePreQueryHooks(
userId,
workspaceId,
objectMetadataItem.nameSingular,
'updateOne',
args,
);
const result = await this.execute(query, workspaceId);
const result = await this.execute(query, authContext.workspace.id);
const parsedResults = (
await this.parseResult<PGGraphQLMutation<Record>>(
result,
objectMetadataItem,
'update',
workspaceId,
authContext.workspace.id,
)
)?.records;
@ -434,8 +433,8 @@ export class WorkspaceQueryRunnerService {
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.updated`, {
name: `${objectMetadataItem.nameSingular}.updated`,
workspaceId,
userId,
workspaceId: authContext.workspace.id,
userId: authContext.user?.id,
recordId: existingRecord.id,
objectMetadata: objectMetadataItem,
properties: {
@ -452,10 +451,10 @@ export class WorkspaceQueryRunnerService {
args: UpdateManyResolverArgs<Partial<Record>>,
options: WorkspaceQueryRunnerOptions,
): Promise<Record[] | undefined> {
const { userId, workspaceId, objectMetadataItem } = options;
const { authContext, objectMetadataItem } = options;
const repository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
authContext.workspace.id,
objectMetadataItem.nameSingular,
);
@ -471,27 +470,31 @@ export class WorkspaceQueryRunnerService {
const maximumRecordAffected = this.environmentService.get(
'MUTATION_MAXIMUM_AFFECTED_RECORDS',
);
const query = await this.workspaceQueryBuilderFactory.updateMany(args, {
...options,
atMost: maximumRecordAffected,
});
await this.workspaceQueryHookService.executePreQueryHooks(
userId,
workspaceId,
objectMetadataItem.nameSingular,
'updateMany',
args,
const hookedArgs =
await this.workspaceQueryHookService.executePreQueryHooks(
authContext,
objectMetadataItem.nameSingular,
'updateMany',
args,
);
const query = await this.workspaceQueryBuilderFactory.updateMany(
hookedArgs,
{
...options,
atMost: maximumRecordAffected,
},
);
const result = await this.execute(query, workspaceId);
const result = await this.execute(query, authContext.workspace.id);
const parsedResults = (
await this.parseResult<PGGraphQLMutation<Record>>(
result,
objectMetadataItem,
'update',
workspaceId,
authContext.workspace.id,
)
)?.records;
@ -514,8 +517,8 @@ export class WorkspaceQueryRunnerService {
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.updated`, {
name: `${objectMetadataItem.nameSingular}.updated`,
workspaceId,
userId,
workspaceId: authContext.workspace.id,
userId: authContext.user?.id,
recordId: existingRecord.id,
objectMetadata: objectMetadataItem,
properties: {
@ -536,34 +539,38 @@ export class WorkspaceQueryRunnerService {
args: DeleteManyResolverArgs<Filter>,
options: WorkspaceQueryRunnerOptions,
): Promise<Record[] | undefined> {
const { workspaceId, userId, objectMetadataItem } = options;
const { authContext, objectMetadataItem } = options;
assertMutationNotOnRemoteObject(objectMetadataItem);
const maximumRecordAffected = this.environmentService.get(
'MUTATION_MAXIMUM_AFFECTED_RECORDS',
);
const query = await this.workspaceQueryBuilderFactory.deleteMany(args, {
...options,
atMost: maximumRecordAffected,
});
await this.workspaceQueryHookService.executePreQueryHooks(
userId,
workspaceId,
objectMetadataItem.nameSingular,
'deleteMany',
args,
const hookedArgs =
await this.workspaceQueryHookService.executePreQueryHooks(
authContext,
objectMetadataItem.nameSingular,
'deleteMany',
args,
);
const query = await this.workspaceQueryBuilderFactory.deleteMany(
hookedArgs,
{
...options,
atMost: maximumRecordAffected,
},
);
const result = await this.execute(query, workspaceId);
const result = await this.execute(query, authContext.workspace.id);
const parsedResults = (
await this.parseResult<PGGraphQLMutation<Record>>(
result,
objectMetadataItem,
'deleteFrom',
workspaceId,
authContext.workspace.id,
)
)?.records;
@ -576,8 +583,8 @@ export class WorkspaceQueryRunnerService {
parsedResults.forEach((record) => {
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.deleted`, {
name: `${objectMetadataItem.nameSingular}.deleted`,
workspaceId,
userId,
workspaceId: authContext.workspace.id,
userId: authContext.user?.id,
recordId: record.id,
objectMetadata: objectMetadataItem,
properties: {
@ -593,18 +600,26 @@ export class WorkspaceQueryRunnerService {
args: DeleteOneResolverArgs,
options: WorkspaceQueryRunnerOptions,
): Promise<Record | undefined> {
const { workspaceId, userId, objectMetadataItem } = options;
const { authContext, objectMetadataItem } = options;
const repository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
authContext.workspace.id,
objectMetadataItem.nameSingular,
);
assertMutationNotOnRemoteObject(objectMetadataItem);
assertIsValidUuid(args.id);
const hookedArgs =
await this.workspaceQueryHookService.executePreQueryHooks(
authContext,
objectMetadataItem.nameSingular,
'deleteOne',
args,
);
const query = await this.workspaceQueryBuilderFactory.deleteOne(
args,
hookedArgs,
options,
);
@ -612,22 +627,14 @@ export class WorkspaceQueryRunnerService {
where: { id: args.id },
});
await this.workspaceQueryHookService.executePreQueryHooks(
userId,
workspaceId,
objectMetadataItem.nameSingular,
'deleteOne',
args,
);
const result = await this.execute(query, workspaceId);
const result = await this.execute(query, authContext.workspace.id);
const parsedResults = (
await this.parseResult<PGGraphQLMutation<Record>>(
result,
objectMetadataItem,
'deleteFrom',
workspaceId,
authContext.workspace.id,
)
)?.records;
@ -639,8 +646,8 @@ export class WorkspaceQueryRunnerService {
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.deleted`, {
name: `${objectMetadataItem.nameSingular}.deleted`,
workspaceId,
userId,
workspaceId: authContext.workspace.id,
userId: authContext.user?.id,
recordId: args.id,
objectMetadata: objectMetadataItem,
properties: {
@ -808,7 +815,7 @@ export class WorkspaceQueryRunnerService {
CallWebhookJobsJob.name,
{
record: jobData,
workspaceId: options.workspaceId,
workspaceId: options.authContext.workspace.id,
operation,
objectMetadataItem: options.objectMetadataItem,
},
@ -816,65 +823,4 @@ export class WorkspaceQueryRunnerService {
);
});
}
async handleDeleteWorkspaceMember(
id: string,
workspaceId: string,
objectMetadataItem: ObjectMetadataInterface,
) {
if (objectMetadataItem.nameSingular !== 'workspaceMember') {
return;
}
const workspaceMemberResult = await this.executeAndParse<IRecord>(
`
query {
workspaceMemberCollection(filter: {id: {eq: "${id}"}}) {
edges {
node {
userId: userId
}
}
}
}
`,
objectMetadataItem,
'',
workspaceId,
);
return workspaceMemberResult.edges?.[0]?.node;
}
async handleDeleteBlocklistItem(
id: string,
workspaceId: string,
objectMetadataItem: ObjectMetadataInterface,
) {
if (objectMetadataItem.standardId !== STANDARD_OBJECT_IDS.blocklist) {
return;
}
const blocklistItemResult = await this.executeAndParse<IRecord>(
`
query {
blocklistCollection(filter: {id: {eq: "${id}"}}) {
edges {
node {
handle
workspaceMember {
id
}
}
}
}
}
`,
objectMetadataItem,
'',
workspaceId,
);
return blocklistItemResult.edges?.[0]?.node;
}
}

View File

@ -25,12 +25,11 @@ export class CreateManyResolverFactory
): Resolver<CreateManyResolverArgs> {
const internalContext = context;
return async (_source, args, context, info) => {
return (_source, args, context, info) => {
try {
return await this.workspaceQueryRunnerService.createMany(args, {
return this.workspaceQueryRunnerService.createMany(args, {
authContext: internalContext.authContext,
objectMetadataItem: internalContext.objectMetadataItem,
workspaceId: internalContext.workspaceId,
userId: internalContext.userId,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,

View File

@ -25,12 +25,11 @@ export class CreateOneResolverFactory
): Resolver<CreateOneResolverArgs> {
const internalContext = context;
return async (_source, args, context, info) => {
return (_source, args, context, info) => {
try {
return await this.workspaceQueryRunnerService.createOne(args, {
return this.workspaceQueryRunnerService.createOne(args, {
authContext: internalContext.authContext,
objectMetadataItem: internalContext.objectMetadataItem,
workspaceId: internalContext.workspaceId,
userId: internalContext.userId,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,

View File

@ -25,12 +25,11 @@ export class DeleteManyResolverFactory
): Resolver<DeleteManyResolverArgs> {
const internalContext = context;
return async (_source, args, context, info) => {
return (_source, args, context, info) => {
try {
return await this.workspaceQueryRunnerService.deleteMany(args, {
return this.workspaceQueryRunnerService.deleteMany(args, {
authContext: internalContext.authContext,
objectMetadataItem: internalContext.objectMetadataItem,
workspaceId: internalContext.workspaceId,
userId: internalContext.userId,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,

View File

@ -25,12 +25,11 @@ export class DeleteOneResolverFactory
): Resolver<DeleteOneResolverArgs> {
const internalContext = context;
return async (_source, args, context, info) => {
return (_source, args, context, info) => {
try {
return await this.workspaceQueryRunnerService.deleteOne(args, {
return this.workspaceQueryRunnerService.deleteOne(args, {
authContext: internalContext.authContext,
objectMetadataItem: internalContext.objectMetadataItem,
workspaceId: internalContext.workspaceId,
userId: internalContext.userId,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,

View File

@ -31,12 +31,11 @@ export class ExecuteQuickActionOnOneResolverFactory
): Resolver<ExecuteQuickActionOnOneResolverArgs> {
const internalContext = context;
return async (_source, args, context, info) => {
return (_source, args, context, info) => {
try {
return await this.executeQuickActionOnOne(args, {
return this.executeQuickActionOnOne(args, {
authContext: internalContext.authContext,
objectMetadataItem: internalContext.objectMetadataItem,
userId: internalContext.userId,
workspaceId: internalContext.workspaceId,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,
@ -55,7 +54,7 @@ export class ExecuteQuickActionOnOneResolverFactory
case 'company': {
await this.quickActionsService.executeQuickActionOnCompany(
args.id,
options.workspaceId,
options.authContext.workspace.id,
options.objectMetadataItem,
);
break;
@ -63,7 +62,7 @@ export class ExecuteQuickActionOnOneResolverFactory
case 'person': {
await this.quickActionsService.createCompanyFromPerson(
args.id,
options.workspaceId,
options.authContext.workspace.id,
options.objectMetadataCollection,
);
break;

View File

@ -25,12 +25,11 @@ export class FindDuplicatesResolverFactory
): Resolver<FindDuplicatesResolverArgs> {
const internalContext = context;
return async (_source, args, context, info) => {
return (_source, args, context, info) => {
try {
return await this.workspaceQueryRunnerService.findDuplicates(args, {
return this.workspaceQueryRunnerService.findDuplicates(args, {
authContext: internalContext.authContext,
objectMetadataItem: internalContext.objectMetadataItem,
workspaceId: internalContext.workspaceId,
userId: internalContext.userId,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,

View File

@ -25,12 +25,11 @@ export class FindManyResolverFactory
): Resolver<FindManyResolverArgs> {
const internalContext = context;
return async (_source, args, context, info) => {
return (_source, args, context, info) => {
try {
return await this.workspaceQueryRunnerService.findMany(args, {
return this.workspaceQueryRunnerService.findMany(args, {
authContext: internalContext.authContext,
objectMetadataItem: internalContext.objectMetadataItem,
workspaceId: internalContext.workspaceId,
userId: internalContext.userId,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,

View File

@ -25,12 +25,11 @@ export class FindOneResolverFactory
): Resolver<FindOneResolverArgs> {
const internalContext = context;
return async (_source, args, context, info) => {
return (_source, args, context, info) => {
try {
return await this.workspaceQueryRunnerService.findOne(args, {
return this.workspaceQueryRunnerService.findOne(args, {
authContext: internalContext.authContext,
objectMetadataItem: internalContext.objectMetadataItem,
workspaceId: internalContext.workspaceId,
userId: internalContext.userId,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,

View File

@ -25,12 +25,11 @@ export class UpdateManyResolverFactory
): Resolver<UpdateManyResolverArgs> {
const internalContext = context;
return async (_source, args, context, info) => {
return (_source, args, context, info) => {
try {
return await this.workspaceQueryRunnerService.updateMany(args, {
return this.workspaceQueryRunnerService.updateMany(args, {
authContext: internalContext.authContext,
objectMetadataItem: internalContext.objectMetadataItem,
workspaceId: internalContext.workspaceId,
userId: internalContext.userId,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,

View File

@ -25,12 +25,11 @@ export class UpdateOneResolverFactory
): Resolver<UpdateOneResolverArgs> {
const internalContext = context;
return async (_source, args, context, info) => {
return (_source, args, context, info) => {
try {
return await this.workspaceQueryRunnerService.updateOne(args, {
return this.workspaceQueryRunnerService.updateOne(args, {
authContext: internalContext.authContext,
objectMetadataItem: internalContext.objectMetadataItem,
workspaceId: internalContext.workspaceId,
userId: internalContext.userId,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,

View File

@ -8,6 +8,7 @@ import { getResolverName } from 'src/engine/utils/get-resolver-name.util';
import { UpdateManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory';
import { DeleteManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory';
import { ExecuteQuickActionOnOneResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/execute-quick-action-on-one-resolver.factory';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { FindDuplicatesResolverFactory } from './factories/find-duplicates-resolver.factory';
import { FindManyResolverFactory } from './factories/find-many-resolver.factory';
@ -40,8 +41,7 @@ export class WorkspaceResolverFactory {
) {}
async create(
workspaceId: string,
userId: string | undefined,
authContext: AuthContext,
objectMetadataCollection: ObjectMetadataInterface[],
workspaceResolverBuilderMethods: WorkspaceResolverBuilderMethods,
): Promise<IResolvers> {
@ -82,8 +82,7 @@ export class WorkspaceResolverFactory {
}
resolvers.Query[resolverName] = resolverFactory.create({
workspaceId,
userId,
authContext,
objectMetadataItem: objectMetadata,
fieldMetadataCollection: objectMetadata.fields,
objectMetadataCollection: objectMetadataCollection,
@ -106,8 +105,7 @@ export class WorkspaceResolverFactory {
}
resolvers.Mutation[resolverName] = resolverFactory.create({
workspaceId,
userId,
authContext,
objectMetadataItem: objectMetadata,
fieldMetadataCollection: objectMetadata.fields,
objectMetadataCollection: objectMetadataCollection,

View File

@ -0,0 +1,99 @@
import { Injectable, Logger } from '@nestjs/common';
import { GraphQLEnumType } from 'graphql';
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
import {
CompositeProperty,
CompositeType,
} from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface';
import { pascalCase } from 'src/utils/pascal-case';
import {
FieldMetadataComplexOption,
FieldMetadataDefaultOption,
} from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util';
import { EnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/enum-type-definition.factory';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { computeCompositePropertyTarget } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-composite-property-target.util';
export interface EnumTypeDefinition {
target: string;
type: GraphQLEnumType;
}
@Injectable()
export class CompositeEnumTypeDefinitionFactory {
private readonly logger = new Logger(EnumTypeDefinitionFactory.name);
public create(
compositeType: CompositeType,
options: WorkspaceBuildSchemaOptions,
): EnumTypeDefinition[] {
const enumTypeDefinitions: EnumTypeDefinition[] = [];
for (const compositeProperty of compositeType.properties) {
if (!isEnumFieldMetadataType(compositeProperty.type)) {
continue;
}
const target = computeCompositePropertyTarget(
compositeType.type,
compositeProperty,
);
enumTypeDefinitions.push({
target,
type: this.generateEnum(compositeType.type, compositeProperty, options),
});
}
return enumTypeDefinitions;
}
private generateEnum(
compositeType: FieldMetadataType,
compositeProperty: CompositeProperty,
options: WorkspaceBuildSchemaOptions,
): GraphQLEnumType {
// FixMe: It's a hack until Typescript get fixed on union types for reduce function
// https://github.com/microsoft/TypeScript/issues/36390
const enumOptions = compositeProperty.options as Array<
FieldMetadataDefaultOption | FieldMetadataComplexOption
>;
if (!enumOptions) {
this.logger.error(
`Enum options are not defined for ${compositeProperty.name}`,
{
compositeProperty,
options,
},
);
throw new Error(
`Enum options are not defined for ${compositeProperty.name}`,
);
}
return new GraphQLEnumType({
name: `${pascalCase(compositeType.toString())}${pascalCase(
compositeProperty.name,
)}Enum`,
description: compositeProperty.description,
values: enumOptions.reduce(
(acc, enumOption) => {
// Key must match this regex: /^[_A-Za-z][_0-9A-Za-z]+$/
acc[enumOption.value] = {
value: enumOption.value,
description: enumOption.label,
};
return acc;
},
{} as { [key: string]: { value: string; description: string } },
),
});
}
}

View File

@ -12,9 +12,15 @@ import {
InputTypeDefinition,
InputTypeDefinitionKind,
} from 'src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory';
import { computeCompositePropertyTarget } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-composite-property-target.util';
import { InputTypeFactory } from './input-type.factory';
const hiddenAllowListKind = [
InputTypeDefinitionKind.Create,
InputTypeDefinitionKind.Update,
];
@Injectable()
export class CompositeInputTypeDefinitionFactory {
private readonly logger = new Logger(
@ -58,12 +64,19 @@ export class CompositeInputTypeDefinitionFactory {
}
// Skip hidden fields
if (property.hidden === true || property.hidden === 'input') {
if (
property.hidden === true ||
(property.hidden === 'input' && hiddenAllowListKind.includes(kind))
) {
continue;
}
const target = computeCompositePropertyTarget(
compositeType.type,
property,
);
const type = this.inputTypeFactory.create(
property.name,
target,
property.type,
kind,
options,

View File

@ -13,6 +13,7 @@ import {
ObjectTypeDefinitionKind,
} from 'src/engine/api/graphql/workspace-schema-builder/factories/object-type-definition.factory';
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
import { computeCompositePropertyTarget } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-composite-property-target.util';
@Injectable()
export class CompositeObjectTypeDefinitionFactory {
@ -62,8 +63,12 @@ export class CompositeObjectTypeDefinitionFactory {
continue;
}
const target = computeCompositePropertyTarget(
compositeType.type,
property,
);
const type = this.outputTypeFactory.create(
property.name,
target,
property.type,
kind,
options,

View File

@ -1,6 +1,7 @@
import { EnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/enum-type-definition.factory';
import { CompositeObjectTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-object-type-definition.factory';
import { CompositeInputTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-input-type-definition.factory';
import { CompositeEnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-enum-type-definition.factory';
import { ArgsFactory } from './args.factory';
import { InputTypeFactory } from './input-type.factory';
@ -27,6 +28,7 @@ export const workspaceSchemaBuilderFactories = [
ObjectTypeDefinitionFactory,
CompositeObjectTypeDefinitionFactory,
EnumTypeDefinitionFactory,
CompositeEnumTypeDefinitionFactory,
RelationTypeFactory,
ExtendObjectTypeDefinitionFactory,
ConnectionTypeFactory,

View File

@ -1,9 +1,10 @@
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
export interface WorkspaceSchemaBuilderContext {
workspaceId: string;
userId: string | undefined;
authContext: AuthContext;
objectMetadataItem: ObjectMetadataInterface;
fieldMetadataCollection: FieldMetadataInterface[];
objectMetadataCollection: ObjectMetadataInterface[];

View File

@ -7,6 +7,7 @@ import { EnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-sche
import { compositeTypeDefintions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { CompositeObjectTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-object-type-definition.factory';
import { CompositeInputTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-input-type-definition.factory';
import { CompositeEnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-enum-type-definition.factory';
import { TypeDefinitionsStorage } from './storages/type-definitions.storage';
import {
@ -32,6 +33,7 @@ export class TypeDefinitionsGenerator {
private readonly objectTypeDefinitionFactory: ObjectTypeDefinitionFactory,
private readonly compositeObjectTypeDefinitionFactory: CompositeObjectTypeDefinitionFactory,
private readonly enumTypeDefinitionFactory: EnumTypeDefinitionFactory,
private readonly compositeEnumTypeDefinitionFactory: CompositeEnumTypeDefinitionFactory,
private readonly inputTypeDefinitionFactory: InputTypeDefinitionFactory,
private readonly compositeInputTypeDefinitionFactory: CompositeInputTypeDefinitionFactory,
private readonly edgeTypeDefinitionFactory: EdgeTypeDefinitionFactory,
@ -62,10 +64,24 @@ export class TypeDefinitionsGenerator {
);
// Generate composite types first because they can be used in metadata objects
this.generateCompositeEnumTypeDefs(compositeTypeCollection, options);
this.generateCompositeObjectTypeDefs(compositeTypeCollection, options);
this.generateCompositeInputTypeDefs(compositeTypeCollection, options);
}
private generateCompositeEnumTypeDefs(
compositeTypes: CompositeType[],
options: WorkspaceBuildSchemaOptions,
) {
const enumTypeDefs = compositeTypes
.map((compositeType) =>
this.compositeEnumTypeDefinitionFactory.create(compositeType, options),
)
.flat();
this.typeDefinitionsStorage.addEnumTypes(enumTypeDefs);
}
private generateCompositeObjectTypeDefs(
compositeTypes: CompositeType[],
options: WorkspaceBuildSchemaOptions,

View File

@ -0,0 +1,10 @@
import { CompositeProperty } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
export const computeCompositePropertyTarget = (
type: FieldMetadataType,
compositeProperty: CompositeProperty,
): string => {
return `${type.toString()}->${compositeProperty.name}`;
};

View File

@ -11,6 +11,7 @@ import { ScalarsExplorerService } from 'src/engine/api/graphql/services/scalars-
import { WorkspaceGraphQLSchemaFactory } from 'src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory';
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 { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
@Injectable()
export class WorkspaceSchemaFactory {
@ -23,17 +24,14 @@ export class WorkspaceSchemaFactory {
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
) {}
async createGraphQLSchema(
workspaceId: string | undefined,
userId: string | undefined,
): Promise<GraphQLSchema> {
if (!workspaceId) {
async createGraphQLSchema(authContext: AuthContext): Promise<GraphQLSchema> {
if (!authContext.workspace?.id) {
return new GraphQLSchema({});
}
const dataSourcesMetadata =
await this.dataSourceService.getDataSourcesMetadataFromWorkspaceId(
workspaceId,
authContext.workspace.id,
);
// Can'f find any data sources for this workspace
@ -42,30 +40,37 @@ export class WorkspaceSchemaFactory {
}
// Validate cache version
await this.workspaceCacheStorageService.validateCacheVersion(workspaceId);
await this.workspaceCacheStorageService.validateCacheVersion(
authContext.workspace.id,
);
// Get object metadata from cache
let objectMetadataCollection =
await this.workspaceCacheStorageService.getObjectMetadataCollection(
workspaceId,
authContext.workspace.id,
);
// If object metadata is not cached, get it from the database
if (!objectMetadataCollection) {
objectMetadataCollection =
await this.objectMetadataService.findManyWithinWorkspace(workspaceId);
await this.objectMetadataService.findManyWithinWorkspace(
authContext.workspace.id,
);
await this.workspaceCacheStorageService.setObjectMetadataCollection(
workspaceId,
authContext.workspace.id,
objectMetadataCollection,
);
}
// Get typeDefs from cache
let typeDefs =
await this.workspaceCacheStorageService.getTypeDefs(workspaceId);
let typeDefs = await this.workspaceCacheStorageService.getTypeDefs(
authContext.workspace.id,
);
let usedScalarNames =
await this.workspaceCacheStorageService.getUsedScalarNames(workspaceId);
await this.workspaceCacheStorageService.getUsedScalarNames(
authContext.workspace.id,
);
// If typeDefs are not cached, generate them
if (!typeDefs || !usedScalarNames) {
@ -80,18 +85,17 @@ export class WorkspaceSchemaFactory {
typeDefs = printSchema(autoGeneratedSchema);
await this.workspaceCacheStorageService.setTypeDefs(
workspaceId,
authContext.workspace.id,
typeDefs,
);
await this.workspaceCacheStorageService.setUsedScalarNames(
workspaceId,
authContext.workspace.id,
usedScalarNames,
);
}
const autoGeneratedResolvers = await this.workspaceResolverFactory.create(
workspaceId,
userId,
authContext,
objectMetadataCollection,
workspaceResolverBuilderMethodNames,
);