Common API - create common find one query (#14720)

closes https://github.com/twentyhq/core-team-issues/issues/1418

Tested : 
- findOne on Rest and Gql

### Vision
#### Common
- Common is kind of renamed Gql base resolver
- Common handles args (filter, values, ...) validation
- Common accepts depth or raw gql selected fields to compute
selectedFields
- Common is directly called by each CommonQueries (findOne, ...)
service, which extend CommonBaseQuery service
- Common sequence : 
| - Parse & Validate args (args-handlers, to create)
| - Build query (query-parsers : currently in gql-query-parsers, to
move)
| - Execute query
| - Fetch relation + format
#### Rest
- Simple parsing (without metadata validation)
- Calling Common API
- Simple rest response formatting
#### Gql
- Calling Common API
This commit is contained in:
Etienne
2025-10-08 12:18:42 +02:00
committed by GitHub
parent 8914ba9fec
commit c693c4d9cf
49 changed files with 1230 additions and 110 deletions
@@ -0,0 +1,3 @@
import { CommonSelectedFieldsHandler } from 'src/engine/api/common/common-args-handlers/common-query-selected-fields/common-selected-fields.handler';
export const CommonArgsHandlers = [CommonSelectedFieldsHandler];
@@ -0,0 +1,140 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { FieldMetadataType, ObjectsPermissions } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { CommonSelectedFieldsResult } from 'src/engine/api/common/types/common-selected-fields-result.type';
import {
Depth,
MAX_DEPTH,
} from 'src/engine/api/rest/input-factories/depth-input.factory';
import { getAllSelectableFields } from 'src/engine/api/utils/get-all-selectable-fields.utils';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
@Injectable()
export class CommonSelectedFieldsHandler {
computeFromDepth = ({
objectsPermissions,
objectMetadataMaps,
objectMetadataMapItem,
depth,
}: {
objectsPermissions: ObjectsPermissions;
objectMetadataMaps: ObjectMetadataMaps;
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
depth: Depth | undefined;
}): CommonSelectedFieldsResult => {
const restrictedFields =
objectsPermissions[objectMetadataMapItem.id].restrictedFields;
const { relations, relationsSelectFields } =
this.getRelationsAndRelationsSelectFields({
objectMetadataMaps,
objectMetadataMapItem,
objectsPermissions,
depth,
});
const selectableFields = getAllSelectableFields({
restrictedFields,
objectMetadata: {
objectMetadataMapItem,
},
});
return {
select: {
...selectableFields,
...relationsSelectFields,
},
relations,
aggregate: {},
};
};
private getRelationsAndRelationsSelectFields({
objectMetadataMaps,
objectMetadataMapItem,
objectsPermissions,
depth,
}: {
objectMetadataMaps: ObjectMetadataMaps;
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
objectsPermissions: ObjectsPermissions;
depth: Depth | undefined;
}) {
if (!isDefined(depth) || depth === 0) {
return {
relations: {},
relationsSelectFields: {},
};
}
let relations: { [key: string]: boolean | { [key: string]: boolean } } = {};
let relationsSelectFields: {
[key: string]:
| boolean
| { [key: string]: boolean | { [key: string]: boolean } };
} = {};
for (const field of Object.values(objectMetadataMapItem.fieldsById)) {
if (!isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION))
continue;
const relationTargetObjectMetadata =
objectMetadataMaps.byId[field.relationTargetObjectMetadataId];
if (!isDefined(relationTargetObjectMetadata)) {
throw new BadRequestException(
`Object metadata relation target not found for relation creation payload`,
);
}
const relationFieldSelectFields = getAllSelectableFields({
restrictedFields:
objectsPermissions[relationTargetObjectMetadata.id].restrictedFields,
objectMetadata: {
objectMetadataMapItem: relationTargetObjectMetadata,
},
});
if (Object.keys(relationFieldSelectFields).length === 0) continue;
if (
depth === MAX_DEPTH &&
isDefined(field.relationTargetObjectMetadataId)
) {
const {
relations: depth2Relations,
relationsSelectFields: depth2RelationsSelectFields,
} = this.getRelationsAndRelationsSelectFields({
objectMetadataMaps,
objectMetadataMapItem: relationTargetObjectMetadata,
objectsPermissions,
depth: 1,
}) as {
relations: { [key: string]: boolean };
relationsSelectFields: {
[key: string]: boolean;
};
};
relations[field.name] = depth2Relations as {
[key: string]: boolean;
};
relationsSelectFields[field.name] = {
...relationFieldSelectFields,
...depth2RelationsSelectFields,
};
} else {
relations[field.name] = true;
relationsSelectFields[field.name] = relationFieldSelectFields;
}
}
return { relations, relationsSelectFields };
}
}
@@ -0,0 +1,214 @@
import { Inject, Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { type ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { type IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
import { type IEdge } from 'src/engine/api/graphql/workspace-query-runner/interfaces/edge.interface';
import { CommonSelectedFieldsHandler } from 'src/engine/api/common/common-args-handlers/common-query-selected-fields/common-selected-fields.handler';
import { CommonQueryNames } from 'src/engine/api/common/types/common-query-args.type';
import { OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS } from 'src/engine/api/graphql/graphql-query-runner/constants/objects-with-settings-permissions-requirements';
import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper';
import { QueryResultGettersFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory';
import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory';
import { WorkspaceQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service';
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/api-key-role.service';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { type PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
import {
PermissionsException,
PermissionsExceptionCode,
PermissionsExceptionMessage,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
@Injectable()
export abstract class CommonBaseQueryRunnerService<
Response extends
| ObjectRecord
| ObjectRecord[]
| IConnection<ObjectRecord, IEdge<ObjectRecord>>
| IConnection<ObjectRecord, IEdge<ObjectRecord>>[],
> {
@Inject()
protected readonly workspaceQueryHookService: WorkspaceQueryHookService;
@Inject()
protected readonly queryRunnerArgsFactory: QueryRunnerArgsFactory;
@Inject()
protected readonly queryResultGettersFactory: QueryResultGettersFactory;
@Inject()
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager;
@Inject()
protected readonly processNestedRelationsHelper: ProcessNestedRelationsHelper;
@Inject()
protected readonly permissionsService: PermissionsService;
@Inject()
protected readonly userRoleService: UserRoleService;
@Inject()
protected readonly apiKeyRoleService: ApiKeyRoleService;
@Inject()
protected readonly selectedFieldsHandler: CommonSelectedFieldsHandler;
@Inject()
protected readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService;
public async prepareQueryRunnerContext({
authContext,
objectMetadataItemWithFieldMaps,
}: {
authContext: WorkspaceAuthContext;
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
}) {
if (objectMetadataItemWithFieldMaps.isSystem === true) {
await this.validateSettingsPermissionsOnObjectOrThrow(
authContext,
objectMetadataItemWithFieldMaps,
);
}
const workspace = authContext.workspace;
const workspaceDataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
workspaceId: workspace.id,
});
const { roleId } = await this.getRoleIdAndObjectsPermissions(
authContext,
workspace.id,
);
const repository = workspaceDataSource.getRepository(
objectMetadataItemWithFieldMaps.nameSingular,
false,
roleId,
authContext,
);
return {
workspaceDataSource,
repository,
isExecutedByApiKey: isDefined(authContext.apiKey),
roleId,
shouldBypassPermissionChecks: false,
};
}
public async enrichResultsWithGettersAndHooks({
results,
operationName,
authContext,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
}: {
results: Response;
operationName: CommonQueryNames;
authContext: WorkspaceAuthContext;
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
objectMetadataMaps: ObjectMetadataMaps;
}) {
const resultWithGetters = await this.queryResultGettersFactory.create(
results,
objectMetadataItemWithFieldMaps,
authContext.workspace.id,
objectMetadataMaps,
);
await this.workspaceQueryHookService.executePostQueryHooks(
authContext,
objectMetadataItemWithFieldMaps.nameSingular,
operationName,
resultWithGetters,
);
return resultWithGetters;
}
private async validateSettingsPermissionsOnObjectOrThrow(
authContext: WorkspaceAuthContext,
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
) {
const workspace = authContext.workspace;
if (
Object.keys(OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS).includes(
objectMetadataItemWithFieldMaps.nameSingular,
)
) {
const permissionRequired: PermissionFlagType =
OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS[
objectMetadataItemWithFieldMaps.nameSingular as keyof typeof OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS
];
const userHasPermission =
await this.permissionsService.userHasWorkspaceSettingPermission({
userWorkspaceId: authContext.userWorkspaceId,
setting: permissionRequired,
workspaceId: workspace.id,
apiKeyId: authContext.apiKey?.id,
});
if (!userHasPermission) {
throw new PermissionsException(
PermissionsExceptionMessage.PERMISSION_DENIED,
PermissionsExceptionCode.PERMISSION_DENIED,
);
}
}
}
private async getRoleIdAndObjectsPermissions(
authContext: AuthContext,
workspaceId: string,
) {
let roleId: string;
if (
!isDefined(authContext.apiKey) &&
!isDefined(authContext.userWorkspaceId)
) {
throw new PermissionsException(
PermissionsExceptionMessage.NO_AUTHENTICATION_CONTEXT,
PermissionsExceptionCode.NO_AUTHENTICATION_CONTEXT,
);
}
if (isDefined(authContext.apiKey)) {
roleId = await this.apiKeyRoleService.getRoleIdForApiKey(
authContext.apiKey.id,
workspaceId,
);
} else {
const userWorkspaceRoleId =
await this.userRoleService.getRoleIdForUserWorkspace({
userWorkspaceId: authContext.userWorkspaceId,
workspaceId,
});
if (!isDefined(userWorkspaceRoleId)) {
throw new PermissionsException(
PermissionsExceptionMessage.NO_ROLE_FOUND_FOR_USER_WORKSPACE,
PermissionsExceptionCode.NO_ROLE_FOUND_FOR_USER_WORKSPACE,
);
}
roleId = userWorkspaceRoleId;
}
const objectMetadataPermissions =
await this.workspacePermissionsCacheService.getObjectRecordPermissionsForRoles(
{
workspaceId: workspaceId,
roleIds: [roleId],
},
);
return { roleId, objectsPermissions: objectMetadataPermissions[roleId] };
}
}
@@ -0,0 +1,185 @@
import { Injectable } from '@nestjs/common';
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { FindOptionsRelations, ObjectLiteral } from 'typeorm';
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import {
ObjectRecord,
ObjectRecordFilter,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { CommonBaseQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-base-query-runner.service';
import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
import {
CommonQueryNames,
FindOneQueryArgs,
} from 'src/engine/api/common/types/common-query-args.type';
import { isWorkspaceAuthContext } from 'src/engine/api/common/utils/is-workspace-auth-context.util';
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
@Injectable()
export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerService<ObjectRecord> {
async run({
args,
authContext: toValidateAuthContext,
objectMetadataMaps,
objectMetadataItemWithFieldMaps,
}: {
args: FindOneQueryArgs;
authContext: AuthContext;
objectMetadataMaps: ObjectMetadataMaps;
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
}): Promise<ObjectRecord> {
const authContext = toValidateAuthContext;
if (!isWorkspaceAuthContext(authContext)) {
throw new CommonQueryRunnerException(
'Invalid auth context',
CommonQueryRunnerExceptionCode.INVALID_AUTH_CONTEXT,
);
}
const {
workspaceDataSource,
repository,
roleId,
shouldBypassPermissionChecks,
} = await this.prepareQueryRunnerContext({
authContext,
objectMetadataItemWithFieldMaps,
});
const processedArgs = await this.processQueryArgs({
authContext,
objectMetadataItemWithFieldMaps,
args,
});
if (
!processedArgs.filter ||
Object.keys(processedArgs.filter).length === 0
) {
throw new CommonQueryRunnerException(
'Missing filter argument',
CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
);
}
const queryBuilder = repository.createQueryBuilder(
objectMetadataItemWithFieldMaps.nameSingular,
);
//TODO : Refacto-common - QueryParser should be common branded service
const commonQueryParser = new GraphqlQueryParser(
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
commonQueryParser.applyFilterToBuilder(
queryBuilder,
objectMetadataItemWithFieldMaps.nameSingular,
processedArgs.filter ?? ({} as ObjectRecordFilter),
);
commonQueryParser.applyDeletedAtToBuilder(
queryBuilder,
processedArgs.filter ?? ({} as ObjectRecordFilter),
);
const columnsToSelect = buildColumnsToSelect({
select: args.selectedFieldsResult.select,
relations: args.selectedFieldsResult.relations,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
});
const objectRecord = await queryBuilder
.setFindOptions({
select: columnsToSelect,
})
.getOne();
if (!objectRecord) {
throw new CommonQueryRunnerException(
'Record not found',
CommonQueryRunnerExceptionCode.RECORD_NOT_FOUND,
);
}
const objectRecords = [objectRecord] as ObjectRecord[];
if (isDefined(args.selectedFieldsResult.relations)) {
await this.processNestedRelationsHelper.processNestedRelations({
objectMetadataMaps,
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
parentObjectRecords: objectRecords,
//TODO : Refacto-common - To fix when switching processNestedRelationsHelper to Common
relations: args.selectedFieldsResult.relations as Record<
string,
FindOptionsRelations<ObjectLiteral>
>,
limit: QUERY_MAX_RECORDS,
authContext,
workspaceDataSource,
roleId,
shouldBypassPermissionChecks,
selectedFields: args.selectedFieldsResult.select,
});
}
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
const results = typeORMObjectRecordsParser.processRecord({
objectRecord: objectRecords[0],
objectName: objectMetadataItemWithFieldMaps.nameSingular,
take: 1,
totalCount: 1,
}) as ObjectRecord;
return this.enrichResultsWithGettersAndHooks({
results,
authContext,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
operationName: CommonQueryNames.findOne,
});
}
async processQueryArgs({
authContext,
objectMetadataItemWithFieldMaps,
args,
}: {
authContext: WorkspaceAuthContext;
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
args: FindOneQueryArgs;
}): Promise<FindOneQueryArgs> {
const hookedArgs =
(await this.workspaceQueryHookService.executePreQueryHooks(
authContext,
objectMetadataItemWithFieldMaps.nameSingular,
CommonQueryNames.findOne,
args,
//TODO : Refacto-common - To fix when updating workspaceQueryHookService, removing gql typing dependency
)) as FindOneQueryArgs;
return {
...hookedArgs,
filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata(
hookedArgs.filter,
objectMetadataItemWithFieldMaps,
),
};
}
}
@@ -0,0 +1,3 @@
import { CommonFindOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-one-query-runner.service';
export const CommonQueryRunners = [CommonFindOneQueryRunnerService];
@@ -0,0 +1,9 @@
import { CustomException } from 'src/utils/custom-exception';
export class CommonQueryRunnerException extends CustomException<CommonQueryRunnerExceptionCode> {}
export enum CommonQueryRunnerExceptionCode {
RECORD_NOT_FOUND = 'RECORD_NOT_FOUND',
INVALID_QUERY_INPUT = 'INVALID_QUERY_INPUT',
INVALID_AUTH_CONTEXT = 'INVALID_AUTH_CONTEXT',
}
@@ -0,0 +1,27 @@
import { assertUnreachable } from 'twenty-shared/utils';
import {
CommonQueryRunnerExceptionCode,
type CommonQueryRunnerException,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
import {
AuthenticationError,
NotFoundError,
UserInputError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
export const commonQueryRunnerToGraphqlApiExceptionHandler = (
error: CommonQueryRunnerException,
) => {
switch (error.code) {
case CommonQueryRunnerExceptionCode.RECORD_NOT_FOUND:
throw new NotFoundError(error);
case CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT:
throw new UserInputError(error);
case CommonQueryRunnerExceptionCode.INVALID_AUTH_CONTEXT:
throw new AuthenticationError(error);
default: {
return assertUnreachable(error.code);
}
}
};
@@ -0,0 +1,28 @@
import {
BadRequestException,
NotFoundException,
UnauthorizedException,
} from '@nestjs/common';
import { assertUnreachable } from 'twenty-shared/utils';
import {
CommonQueryRunnerExceptionCode,
type CommonQueryRunnerException,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
export const commonQueryRunnerToRestApiExceptionHandler = (
error: CommonQueryRunnerException,
): never => {
switch (error.code) {
case CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT:
throw new BadRequestException(error.message);
case CommonQueryRunnerExceptionCode.RECORD_NOT_FOUND:
throw new NotFoundException('Record not found');
case CommonQueryRunnerExceptionCode.INVALID_AUTH_CONTEXT:
throw new UnauthorizedException(error.message);
default: {
return assertUnreachable(error.code);
}
}
};
@@ -0,0 +1,36 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CommonArgsHandlers } from 'src/engine/api/common/common-args-handlers/common-query-selected-fields/common-arg-handlers';
import { CommonQueryRunners } from 'src/engine/api/common/common-query-runners/common-query-runners';
import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper';
import { ProcessNestedRelationsV2Helper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper';
import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper';
import { WorkspaceQueryHookModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.module';
import { WorkspaceQueryRunnerModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module';
import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
@Module({
imports: [
WorkspaceQueryHookModule,
WorkspaceQueryRunnerModule,
PermissionsModule,
TypeOrmModule.forFeature([RoleTargetsEntity]),
UserRoleModule,
ApiKeyModule,
WorkspacePermissionsCacheModule,
],
providers: [
ProcessNestedRelationsHelper,
ProcessNestedRelationsV2Helper,
...CommonArgsHandlers,
ProcessAggregateHelper,
...CommonQueryRunners,
],
exports: [...CommonQueryRunners],
})
export class CoreCommonApiModule {}
@@ -0,0 +1,21 @@
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
interface AuthContextWithDefinedWorkspaceProperties {
user: AuthContext['user'];
workspace: NonNullable<AuthContext['workspace']>;
workspaceMetadataVersion?: string;
workspaceMemberId: AuthContext['workspaceMemberId'];
userWorkspaceId: AuthContext['userWorkspaceId'];
apiKey: AuthContext['apiKey'];
}
interface ApiKeyAuthContext extends Request {
apiKey: NonNullable<AuthContext['apiKey']>;
}
interface UserWorkspaceAuthContext extends Request {
userWorkspaceId: NonNullable<AuthContext['userWorkspaceId']>;
}
export type WorkspaceAuthContext = AuthContextWithDefinedWorkspaceProperties &
(ApiKeyAuthContext | UserWorkspaceAuthContext);
@@ -0,0 +1,14 @@
import { type ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { type CommonSelectedFieldsResult } from 'src/engine/api/common/types/common-selected-fields-result.type';
export enum CommonQueryNames {
findOne = 'findOne',
}
export interface FindOneQueryArgs {
selectedFieldsResult: CommonSelectedFieldsResult;
filter?: ObjectRecordFilter;
}
export type CommonQueryArgs = FindOneQueryArgs;
@@ -0,0 +1,9 @@
interface SelectedFields {
[key: string]: boolean | SelectedFields;
}
export type CommonSelectedFieldsResult = {
select: SelectedFields;
relations: SelectedFields;
aggregate: SelectedFields;
};
@@ -0,0 +1,14 @@
import { isDefined } from 'twenty-shared/utils';
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
export const isWorkspaceAuthContext = (
context: AuthContext,
): context is WorkspaceAuthContext => {
return (
isDefined(context.workspace) &&
(isDefined(context.userWorkspaceId) || isDefined(context.apiKey))
);
};