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:
@@ -55,7 +55,7 @@ jobs:
|
||||
ports:
|
||||
- 6379:6379
|
||||
clickhouse:
|
||||
image: clickhouse/clickhouse-server:latest
|
||||
image: clickhouse/clickhouse-server:25.8.8
|
||||
env:
|
||||
CLICKHOUSE_PASSWORD: clickhousePassword
|
||||
CLICKHOUSE_URL: "http://default:clickhousePassword@localhost:8123/twenty"
|
||||
|
||||
@@ -191,7 +191,7 @@ jobs:
|
||||
ports:
|
||||
- 6379:6379
|
||||
clickhouse:
|
||||
image: clickhouse/clickhouse-server:latest
|
||||
image: clickhouse/clickhouse-server:25.8.8
|
||||
env:
|
||||
CLICKHOUSE_PASSWORD: clickhousePassword
|
||||
CLICKHOUSE_URL: "http://default:clickhousePassword@localhost:8123/twenty"
|
||||
|
||||
@@ -1195,6 +1195,7 @@ export enum FeatureFlagKey {
|
||||
IS_AIRTABLE_INTEGRATION_ENABLED = 'IS_AIRTABLE_INTEGRATION_ENABLED',
|
||||
IS_AI_ENABLED = 'IS_AI_ENABLED',
|
||||
IS_CALENDAR_VIEW_ENABLED = 'IS_CALENDAR_VIEW_ENABLED',
|
||||
IS_COMMON_API_ENABLED = 'IS_COMMON_API_ENABLED',
|
||||
IS_CORE_VIEW_ENABLED = 'IS_CORE_VIEW_ENABLED',
|
||||
IS_CORE_VIEW_SYNCING_ENABLED = 'IS_CORE_VIEW_SYNCING_ENABLED',
|
||||
IS_DYNAMIC_SEARCH_FIELDS_ENABLED = 'IS_DYNAMIC_SEARCH_FIELDS_ENABLED',
|
||||
|
||||
@@ -1159,6 +1159,7 @@ export enum FeatureFlagKey {
|
||||
IS_AIRTABLE_INTEGRATION_ENABLED = 'IS_AIRTABLE_INTEGRATION_ENABLED',
|
||||
IS_AI_ENABLED = 'IS_AI_ENABLED',
|
||||
IS_CALENDAR_VIEW_ENABLED = 'IS_CALENDAR_VIEW_ENABLED',
|
||||
IS_COMMON_API_ENABLED = 'IS_COMMON_API_ENABLED',
|
||||
IS_CORE_VIEW_ENABLED = 'IS_CORE_VIEW_ENABLED',
|
||||
IS_CORE_VIEW_SYNCING_ENABLED = 'IS_CORE_VIEW_SYNCING_ENABLED',
|
||||
IS_DYNAMIC_SEARCH_FIELDS_ENABLED = 'IS_DYNAMIC_SEARCH_FIELDS_ENABLED',
|
||||
|
||||
+3
@@ -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];
|
||||
+140
@@ -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 };
|
||||
}
|
||||
}
|
||||
+214
@@ -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] };
|
||||
}
|
||||
}
|
||||
+185
@@ -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,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
import { CommonFindOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-one-query-runner.service';
|
||||
|
||||
export const CommonQueryRunners = [CommonFindOneQueryRunnerService];
|
||||
+9
@@ -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',
|
||||
}
|
||||
+27
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
+28
@@ -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 {}
|
||||
+21
@@ -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;
|
||||
+9
@@ -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))
|
||||
);
|
||||
};
|
||||
+1
@@ -1,3 +1,4 @@
|
||||
//TODO : Refacto-common - To delete
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
|
||||
|
||||
+1
@@ -1,3 +1,4 @@
|
||||
//TODO : Refacto-common - Should be moved to common api layer
|
||||
export interface ObjectRecord {
|
||||
id: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
+3
-3
@@ -7,7 +7,7 @@ import {
|
||||
type ObjectRecord,
|
||||
type ObjectRecordFilter,
|
||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import {
|
||||
type CreateManyResolverArgs,
|
||||
type CreateOneResolverArgs,
|
||||
@@ -23,9 +23,9 @@ import {
|
||||
|
||||
import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service';
|
||||
import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service';
|
||||
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
|
||||
import { type FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
|
||||
|
||||
@Injectable()
|
||||
export class QueryRunnerArgsFactory {
|
||||
@@ -214,7 +214,7 @@ export class QueryRunnerArgsFactory {
|
||||
return allOverriddenRecords;
|
||||
}
|
||||
|
||||
private overrideFilterByFieldMetadata(
|
||||
public overrideFilterByFieldMetadata(
|
||||
filter: ObjectRecordFilter | undefined,
|
||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
|
||||
) {
|
||||
|
||||
+2
-2
@@ -6,7 +6,7 @@ import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/objec
|
||||
|
||||
export interface WorkspaceQueryRunnerOptions {
|
||||
authContext: AuthContext;
|
||||
info: GraphQLResolveInfo;
|
||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
|
||||
info: GraphQLResolveInfo;
|
||||
}
|
||||
|
||||
+5
@@ -1,5 +1,7 @@
|
||||
import { type QueryFailedError } from 'typeorm';
|
||||
|
||||
import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
|
||||
import { commonQueryRunnerToGraphqlApiExceptionHandler } from 'src/engine/api/common/common-query-runners/utils/common-query-runner-to-graphql-api-exception-handler.util';
|
||||
import { GraphqlQueryRunnerException } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
import { graphqlQueryRunnerExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/graphql-query-runner-exception-handler.util';
|
||||
import { workspaceExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-exception-handler.util';
|
||||
@@ -18,6 +20,7 @@ import { twentyORMGraphqlApiExceptionHandler } from 'src/engine/twenty-orm/utils
|
||||
interface QueryFailedErrorWithCode extends QueryFailedError {
|
||||
code: string;
|
||||
}
|
||||
//TODO : Refacto-common - Should be handle first in common api layer
|
||||
|
||||
export const workspaceQueryRunnerGraphqlApiExceptionHandler = (
|
||||
error: QueryFailedErrorWithCode,
|
||||
@@ -33,6 +36,8 @@ export const workspaceQueryRunnerGraphqlApiExceptionHandler = (
|
||||
return graphqlQueryRunnerExceptionHandler(error);
|
||||
case error instanceof TwentyORMException:
|
||||
return twentyORMGraphqlApiExceptionHandler(error);
|
||||
case error instanceof CommonQueryRunnerException:
|
||||
return commonQueryRunnerToGraphqlApiExceptionHandler(error);
|
||||
case error instanceof AuthException:
|
||||
return authGraphqlApiExceptionHandler(error);
|
||||
case error instanceof ApiKeyException:
|
||||
|
||||
+3
-1
@@ -5,6 +5,7 @@ import merge from 'lodash.merge';
|
||||
import { type QueryResultFieldValue } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-field-value';
|
||||
import { type WorkspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import { CommonQueryNames } from 'src/engine/api/common/types/common-query-args.type';
|
||||
import { type 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 { type WorkspacePreQueryHookPayload } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
|
||||
@@ -18,8 +19,9 @@ export class WorkspaceQueryHookService {
|
||||
private readonly workspaceQueryHookExplorer: WorkspaceQueryHookExplorer,
|
||||
) {}
|
||||
|
||||
//TODO : Refacto-common - Should be Common
|
||||
public async executePreQueryHooks<
|
||||
T extends WorkspaceResolverBuilderMethodNames,
|
||||
T extends WorkspaceResolverBuilderMethodNames | CommonQueryNames,
|
||||
>(
|
||||
authContext: AuthContext,
|
||||
// TODO: We should allow wildcard for object name
|
||||
|
||||
+1
-1
@@ -13,8 +13,8 @@ import { FileModule } from 'src/engine/core-modules/file/file.module';
|
||||
import { RecordPositionModule } from 'src/engine/core-modules/record-position/record-position.module';
|
||||
import { RecordTransformerModule } from 'src/engine/core-modules/record-transformer/record-transformer.module';
|
||||
import { TelemetryModule } from 'src/engine/core-modules/telemetry/telemetry.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
import { SubscriptionsModule } from 'src/engine/subscriptions/subscriptions.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
|
||||
import { EntityEventsToDbListener } from './listeners/entity-events-to-db.listener';
|
||||
|
||||
|
||||
+46
-10
@@ -1,15 +1,21 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import graphqlFields from 'graphql-fields';
|
||||
|
||||
import { type WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
|
||||
import {
|
||||
type FindOneResolverArgs,
|
||||
type Resolver,
|
||||
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||
|
||||
import { CommonFindOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-one-query-runner.service';
|
||||
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
|
||||
import { GraphqlQueryFindOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service';
|
||||
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
|
||||
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
|
||||
@Injectable()
|
||||
export class FindOneResolverFactory
|
||||
@@ -18,6 +24,8 @@ export class FindOneResolverFactory
|
||||
public static methodName = RESOLVER_METHOD_NAMES.FIND_ONE;
|
||||
|
||||
constructor(
|
||||
private readonly commonFindOneQueryRunnerService: CommonFindOneQueryRunnerService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly graphqlQueryRunnerService: GraphqlQueryFindOneResolverService,
|
||||
) {}
|
||||
|
||||
@@ -27,17 +35,45 @@ export class FindOneResolverFactory
|
||||
const internalContext = context;
|
||||
|
||||
return async (_source, args, _context, info) => {
|
||||
const options: WorkspaceQueryRunnerOptions = {
|
||||
authContext: internalContext.authContext,
|
||||
info,
|
||||
objectMetadataMaps: internalContext.objectMetadataMaps,
|
||||
objectMetadataItemWithFieldMaps:
|
||||
internalContext.objectMetadataItemWithFieldMaps,
|
||||
};
|
||||
const isCommonApiEnabled = await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_COMMON_API_ENABLED,
|
||||
internalContext.authContext.workspace?.id as string,
|
||||
);
|
||||
|
||||
if (isCommonApiEnabled) {
|
||||
try {
|
||||
const graphqlQueryParser = new GraphqlQueryParser(
|
||||
internalContext.objectMetadataItemWithFieldMaps,
|
||||
internalContext.objectMetadataMaps,
|
||||
);
|
||||
|
||||
const selectedFieldsResult = graphqlQueryParser.parseSelectedFields(
|
||||
internalContext.objectMetadataItemWithFieldMaps,
|
||||
graphqlFields(info),
|
||||
internalContext.objectMetadataMaps,
|
||||
);
|
||||
|
||||
return await this.commonFindOneQueryRunnerService.run({
|
||||
args: { ...args, selectedFieldsResult },
|
||||
authContext: internalContext.authContext,
|
||||
objectMetadataMaps: internalContext.objectMetadataMaps,
|
||||
objectMetadataItemWithFieldMaps:
|
||||
internalContext.objectMetadataItemWithFieldMaps,
|
||||
});
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
return await this.graphqlQueryRunnerService.execute(
|
||||
args,
|
||||
options,
|
||||
{
|
||||
authContext: internalContext.authContext,
|
||||
info,
|
||||
objectMetadataMaps: internalContext.objectMetadataMaps,
|
||||
objectMetadataItemWithFieldMaps:
|
||||
internalContext.objectMetadataItemWithFieldMaps,
|
||||
},
|
||||
FindOneResolverFactory.methodName,
|
||||
);
|
||||
};
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { CoreCommonApiModule } from 'src/engine/api/common/core-common-api.module';
|
||||
import { GraphqlQueryRunnerModule } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.module';
|
||||
import { WorkspaceResolverBuilderService } from 'src/engine/api/graphql/workspace-resolver-builder/workspace-resolver-builder.service';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
@@ -9,7 +10,7 @@ import { WorkspaceResolverFactory } from './workspace-resolver.factory';
|
||||
import { workspaceResolverBuilderFactories } from './factories/factories';
|
||||
|
||||
@Module({
|
||||
imports: [GraphqlQueryRunnerModule, FeatureFlagModule],
|
||||
imports: [GraphqlQueryRunnerModule, FeatureFlagModule, CoreCommonApiModule],
|
||||
providers: [
|
||||
...workspaceResolverBuilderFactories,
|
||||
WorkspaceResolverFactory,
|
||||
|
||||
+30
-8
@@ -12,10 +12,11 @@ import {
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { Request, Response } from 'express';
|
||||
import { Response } from 'express';
|
||||
|
||||
import { RestApiCoreService } from 'src/engine/api/rest/core/services/rest-api-core.service';
|
||||
import { RestApiExceptionFilter } from 'src/engine/api/rest/rest-api-exception.filter';
|
||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
|
||||
@@ -27,7 +28,10 @@ export class RestApiCoreController {
|
||||
constructor(private readonly restApiCoreService: RestApiCoreService) {}
|
||||
|
||||
@Post('batch/*')
|
||||
async handleApiPostBatch(@Req() request: Request, @Res() res: Response) {
|
||||
async handleApiPostBatch(
|
||||
@Req() request: AuthenticatedRequest,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
this.logger.log(
|
||||
`[REST API] Processing BATCH request to ${request.path} on workspace ${request.workspaceId}`,
|
||||
);
|
||||
@@ -37,7 +41,10 @@ export class RestApiCoreController {
|
||||
}
|
||||
|
||||
@Post('*/duplicates')
|
||||
async handleApiFindDuplicates(@Req() request: Request, @Res() res: Response) {
|
||||
async handleApiFindDuplicates(
|
||||
@Req() request: AuthenticatedRequest,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
this.logger.log(
|
||||
`[REST API] Processing DUPLICATES request to ${request.path} on workspace ${request.workspaceId}`,
|
||||
);
|
||||
@@ -47,7 +54,10 @@ export class RestApiCoreController {
|
||||
}
|
||||
|
||||
@Post('*')
|
||||
async handleApiPost(@Req() request: Request, @Res() res: Response) {
|
||||
async handleApiPost(
|
||||
@Req() request: AuthenticatedRequest,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
this.logger.log(
|
||||
`[REST API] Processing POST request to ${request.path} on workspace ${request.workspaceId}`,
|
||||
);
|
||||
@@ -57,7 +67,10 @@ export class RestApiCoreController {
|
||||
}
|
||||
|
||||
@Get('*')
|
||||
async handleApiGet(@Req() request: Request, @Res() res: Response) {
|
||||
async handleApiGet(
|
||||
@Req() request: AuthenticatedRequest,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
this.logger.log(
|
||||
`[REST API] Processing GET request to ${request.path} on workspace ${request.workspaceId}`,
|
||||
);
|
||||
@@ -67,7 +80,10 @@ export class RestApiCoreController {
|
||||
}
|
||||
|
||||
@Delete('*')
|
||||
async handleApiDelete(@Req() request: Request, @Res() res: Response) {
|
||||
async handleApiDelete(
|
||||
@Req() request: AuthenticatedRequest,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
this.logger.log(
|
||||
`[REST API] Processing DELETE request to ${request.path} on workspace ${request.workspaceId}`,
|
||||
);
|
||||
@@ -77,7 +93,10 @@ export class RestApiCoreController {
|
||||
}
|
||||
|
||||
@Patch('*')
|
||||
async handleApiPatch(@Req() request: Request, @Res() res: Response) {
|
||||
async handleApiPatch(
|
||||
@Req() request: AuthenticatedRequest,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
this.logger.log(
|
||||
`[REST API] Processing PATCH request to ${request.path} on workspace ${request.workspaceId}`,
|
||||
);
|
||||
@@ -90,7 +109,10 @@ export class RestApiCoreController {
|
||||
// We keep it to avoid a breaking change since it initially used PUT instead
|
||||
// of PATCH, and because the PUT verb is often used as a PATCH.
|
||||
@Put('*')
|
||||
async handleApiPut(@Req() request: Request, @Res() res: Response) {
|
||||
async handleApiPut(
|
||||
@Req() request: AuthenticatedRequest,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
this.logger.log(
|
||||
`[REST API] Processing PUT request to ${request.path} on workspace ${request.workspaceId}`,
|
||||
);
|
||||
|
||||
+2
-2
@@ -4,17 +4,17 @@ import {
|
||||
InternalServerErrorException,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { type Request } from 'express';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
import { getAllSelectableFields } from 'src/engine/api/utils/get-all-selectable-fields.utils';
|
||||
|
||||
@Injectable()
|
||||
export class RestApiCreateManyHandler extends RestApiBaseHandler {
|
||||
async handle(request: Request) {
|
||||
async handle(request: AuthenticatedRequest) {
|
||||
const { objectMetadata, repository, restrictedFields } =
|
||||
await this.getRepositoryAndMetadataOrFail(request);
|
||||
|
||||
|
||||
+2
-2
@@ -4,17 +4,17 @@ import {
|
||||
InternalServerErrorException,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { type Request } from 'express';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
import { getAllSelectableFields } from 'src/engine/api/utils/get-all-selectable-fields.utils';
|
||||
|
||||
@Injectable()
|
||||
export class RestApiCreateOneHandler extends RestApiBaseHandler {
|
||||
async handle(request: Request) {
|
||||
async handle(request: AuthenticatedRequest) {
|
||||
const { objectMetadata, repository, restrictedFields } =
|
||||
await this.getRepositoryAndMetadataOrFail(request);
|
||||
|
||||
|
||||
+2
-3
@@ -1,15 +1,14 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
|
||||
import { type Request } from 'express';
|
||||
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
|
||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
import { getAllSelectableFields } from 'src/engine/api/utils/get-all-selectable-fields.utils';
|
||||
|
||||
@Injectable()
|
||||
export class RestApiDeleteOneHandler extends RestApiBaseHandler {
|
||||
async handle(request: Request) {
|
||||
async handle(request: AuthenticatedRequest) {
|
||||
const { id: recordId } = parseCorePath(request);
|
||||
|
||||
if (!recordId) {
|
||||
|
||||
+2
-1
@@ -10,11 +10,12 @@ import {
|
||||
RestApiBaseHandler,
|
||||
} from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
import { buildDuplicateConditions } from 'src/engine/api/utils/build-duplicate-conditions.utils';
|
||||
|
||||
@Injectable()
|
||||
export class RestApiFindDuplicatesHandler extends RestApiBaseHandler {
|
||||
async handle(request: Request) {
|
||||
async handle(request: AuthenticatedRequest) {
|
||||
this.validate(request);
|
||||
|
||||
const {
|
||||
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type Request } from 'express';
|
||||
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
|
||||
@Injectable()
|
||||
export class RestApiFindManyHandler extends RestApiBaseHandler {
|
||||
async handle(request: Request) {
|
||||
async handle(request: AuthenticatedRequest) {
|
||||
const {
|
||||
repository,
|
||||
objectMetadata,
|
||||
|
||||
+76
-2
@@ -1,15 +1,24 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
|
||||
import { type Request } from 'express';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
import { CommonFindOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-one-query-runner.service';
|
||||
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
|
||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
import { workspaceQueryRunnerRestApiExceptionHandler } from 'src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util';
|
||||
|
||||
@Injectable()
|
||||
export class RestApiFindOneHandler extends RestApiBaseHandler {
|
||||
async handle(request: Request) {
|
||||
constructor(
|
||||
private readonly commonFindOneQueryRunnerService: CommonFindOneQueryRunnerService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async handle(request: AuthenticatedRequest) {
|
||||
const { id: recordId } = parseCorePath(request);
|
||||
|
||||
if (!isDefined(recordId)) {
|
||||
@@ -46,4 +55,69 @@ export class RestApiFindOneHandler extends RestApiBaseHandler {
|
||||
data: record,
|
||||
});
|
||||
}
|
||||
|
||||
async commonHandle(request: AuthenticatedRequest) {
|
||||
try {
|
||||
const { filter, depth } = await this.parseRequestArgs(request);
|
||||
const {
|
||||
authContext,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps,
|
||||
} = await this.buildCommonOptions(request);
|
||||
|
||||
const selectedFieldsResult = await this.computeSelectedFields({
|
||||
depth,
|
||||
objectMetadataMapItem: objectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps,
|
||||
authContext,
|
||||
});
|
||||
|
||||
const record = await this.commonFindOneQueryRunnerService.run({
|
||||
args: { filter, selectedFieldsResult },
|
||||
authContext,
|
||||
objectMetadataMaps,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
});
|
||||
|
||||
return this.formatRestResponse(
|
||||
record,
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
);
|
||||
} catch (error) {
|
||||
return workspaceQueryRunnerRestApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
private formatRestResponse(record: ObjectRecord, objectNameSingular: string) {
|
||||
return { data: { [objectNameSingular]: record } };
|
||||
}
|
||||
|
||||
private async parseRequestArgs(request: AuthenticatedRequest) {
|
||||
const { id: recordId } = parseCorePath(request);
|
||||
const filter = { id: { eq: recordId } };
|
||||
const depth = this.depthInputFactory.create(request);
|
||||
|
||||
return {
|
||||
filter,
|
||||
depth,
|
||||
};
|
||||
}
|
||||
|
||||
private async buildCommonOptions(request: AuthenticatedRequest) {
|
||||
const { object: parsedObject } = parseCorePath(request);
|
||||
|
||||
const { objectMetadataMaps, objectMetadataMapItem } =
|
||||
await this.coreQueryBuilderFactory.getObjectMetadata(
|
||||
request,
|
||||
parsedObject,
|
||||
);
|
||||
|
||||
const authContext = this.getAuthContextFromRequest(request);
|
||||
|
||||
return {
|
||||
authContext: authContext,
|
||||
objectMetadataItemWithFieldMaps: objectMetadataMapItem,
|
||||
objectMetadataMaps: objectMetadataMaps,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -4,18 +4,18 @@ import {
|
||||
InternalServerErrorException,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { type Request } from 'express';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
|
||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
import { getAllSelectableFields } from 'src/engine/api/utils/get-all-selectable-fields.utils';
|
||||
|
||||
@Injectable()
|
||||
export class RestApiUpdateOneHandler extends RestApiBaseHandler {
|
||||
async handle(request: Request) {
|
||||
async handle(request: AuthenticatedRequest) {
|
||||
const { id: recordId } = parseCorePath(request);
|
||||
|
||||
if (!recordId) {
|
||||
|
||||
+116
-52
@@ -10,6 +10,7 @@ import {
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
import { In, type ObjectLiteral } from 'typeorm';
|
||||
|
||||
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
|
||||
import {
|
||||
type ObjectRecord,
|
||||
type ObjectRecordFilter,
|
||||
@@ -20,17 +21,18 @@ import { encodeCursor } from 'src/engine/api/graphql/graphql-query-runner/utils/
|
||||
import { CoreQueryBuilderFactory } from 'src/engine/api/rest/core/query-builder/core-query-builder.factory';
|
||||
import { GetVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/get-variables.factory';
|
||||
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
|
||||
import { RestToCommonSelectedFieldsHandler } from 'src/engine/api/rest/core/rest-to-common-args-handlers/selected-fields-handler';
|
||||
import { type QueryVariables } from 'src/engine/api/rest/core/types/query-variables.type';
|
||||
import {
|
||||
type Depth,
|
||||
DepthInputFactory,
|
||||
MAX_DEPTH,
|
||||
type Depth,
|
||||
} from 'src/engine/api/rest/input-factories/depth-input.factory';
|
||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
import { computeCursorArgFilter } from 'src/engine/api/utils/compute-cursor-arg-filter.utils';
|
||||
import { getAllSelectableFields } from 'src/engine/api/utils/get-all-selectable-fields.utils';
|
||||
import { CreatedByFromAuthContextService } from 'src/engine/core-modules/actor/services/created-by-from-auth-context.service';
|
||||
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/api-key-role.service';
|
||||
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { InternalServerError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service';
|
||||
import {
|
||||
@@ -40,13 +42,16 @@ import {
|
||||
} from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util';
|
||||
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
|
||||
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
||||
import { type WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
|
||||
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||
import { formatResult as formatGetManyData } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
|
||||
export interface PageInfo {
|
||||
hasNextPage?: boolean;
|
||||
@@ -94,13 +99,21 @@ export abstract class RestApiBaseHandler {
|
||||
@Inject()
|
||||
protected readonly createdByFromAuthContextService: CreatedByFromAuthContextService;
|
||||
@Inject()
|
||||
protected readonly workspaceCacheStorageService: WorkspaceCacheStorageService;
|
||||
@Inject()
|
||||
protected readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService;
|
||||
@Inject()
|
||||
protected readonly apiKeyRoleService: ApiKeyRoleService;
|
||||
@Inject()
|
||||
protected readonly restToCommonSelectedFieldsHandler: RestToCommonSelectedFieldsHandler;
|
||||
@Inject()
|
||||
protected readonly userRoleService: UserRoleService;
|
||||
|
||||
protected abstract handle(
|
||||
request: Request,
|
||||
request: AuthenticatedRequest,
|
||||
): Promise<FormatResult | { data: FormatResult[] }>;
|
||||
|
||||
public async getRepositoryAndMetadataOrFail(request: Request) {
|
||||
public async getRepositoryAndMetadataOrFail(request: AuthenticatedRequest) {
|
||||
const { workspace, apiKey, userWorkspaceId } = request;
|
||||
const { object: parsedObject } = parseCorePath(request);
|
||||
|
||||
@@ -109,14 +122,14 @@ export abstract class RestApiBaseHandler {
|
||||
parsedObject,
|
||||
);
|
||||
|
||||
if (!objectMetadata) {
|
||||
throw new BadRequestException('Object metadata not found');
|
||||
}
|
||||
|
||||
if (!workspace?.id) {
|
||||
throw new BadRequestException('Workspace not found');
|
||||
}
|
||||
|
||||
if (!objectMetadata) {
|
||||
throw new BadRequestException('Object metadata not found');
|
||||
}
|
||||
|
||||
const workspaceDataSource = await this.twentyORMManager.getDatasource();
|
||||
|
||||
const objectMetadataNameSingular =
|
||||
@@ -134,18 +147,22 @@ export abstract class RestApiBaseHandler {
|
||||
);
|
||||
}
|
||||
|
||||
let roleId: string | undefined = undefined;
|
||||
let shouldBypassPermissionChecks = false;
|
||||
let roleId: string;
|
||||
|
||||
if (isDefined(apiKey)) {
|
||||
roleId = await this.apiKeyRoleService.getRoleIdForApiKey(
|
||||
apiKey.id,
|
||||
workspace.id,
|
||||
);
|
||||
}
|
||||
|
||||
if (isDefined(userWorkspaceId)) {
|
||||
roleId =
|
||||
if (!isDefined(roleId)) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.API_KEY_ROLE_NOT_FOUND,
|
||||
PermissionsExceptionCode.API_KEY_ROLE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const userWorkspaceRoleId =
|
||||
await this.workspacePermissionsCacheService.getRoleIdFromUserWorkspaceId(
|
||||
{
|
||||
workspaceId: workspace.id,
|
||||
@@ -153,60 +170,53 @@ export abstract class RestApiBaseHandler {
|
||||
},
|
||||
);
|
||||
|
||||
if (!roleId) {
|
||||
if (!isDefined(userWorkspaceRoleId)) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.NO_ROLE_FOUND_FOR_USER_WORKSPACE,
|
||||
PermissionsExceptionCode.NO_ROLE_FOUND_FOR_USER_WORKSPACE,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isDefined(apiKey) && !isDefined(userWorkspaceId)) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.NO_AUTHENTICATION_CONTEXT,
|
||||
PermissionsExceptionCode.NO_AUTHENTICATION_CONTEXT,
|
||||
);
|
||||
roleId = userWorkspaceRoleId;
|
||||
}
|
||||
|
||||
const repository = workspaceDataSource.getRepository<ObjectRecord>(
|
||||
objectMetadataNameSingular,
|
||||
shouldBypassPermissionChecks,
|
||||
false,
|
||||
roleId,
|
||||
);
|
||||
|
||||
let restrictedFields: RestrictedFieldsPermissions = {};
|
||||
const objectMetadataPermissions =
|
||||
await this.workspacePermissionsCacheService.getObjectRecordPermissionsForRoles(
|
||||
{
|
||||
workspaceId: workspace.id,
|
||||
roleIds: roleId ? [roleId] : undefined,
|
||||
},
|
||||
);
|
||||
|
||||
if (roleId) {
|
||||
const objectMetadataPermissions =
|
||||
await this.workspacePermissionsCacheService.getObjectRecordPermissionsForRoles(
|
||||
{
|
||||
workspaceId: workspace.id,
|
||||
roleIds: roleId ? [roleId] : undefined,
|
||||
},
|
||||
);
|
||||
|
||||
if (
|
||||
!isDefined(
|
||||
objectMetadataPermissions?.[roleId]?.[
|
||||
objectMetadata.objectMetadataMapItem.id
|
||||
]?.restrictedFields,
|
||||
)
|
||||
) {
|
||||
throw new InternalServerError('Fields permissions not found for role');
|
||||
}
|
||||
|
||||
restrictedFields =
|
||||
objectMetadataPermissions[roleId][
|
||||
if (
|
||||
!isDefined(
|
||||
objectMetadataPermissions?.[roleId]?.[
|
||||
objectMetadata.objectMetadataMapItem.id
|
||||
].restrictedFields;
|
||||
]?.restrictedFields,
|
||||
)
|
||||
) {
|
||||
throw new InternalServerError('Fields permissions not found for role');
|
||||
}
|
||||
|
||||
const restrictedFields =
|
||||
objectMetadataPermissions[roleId][objectMetadata.objectMetadataMapItem.id]
|
||||
.restrictedFields;
|
||||
|
||||
return {
|
||||
objectMetadata,
|
||||
repository,
|
||||
workspaceDataSource,
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
restrictedFields,
|
||||
isExecutedByApiKey: isDefined(apiKey),
|
||||
authContext: this.getAuthContextFromRequest(request),
|
||||
objectsPermissions: objectMetadataPermissions[roleId],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -325,14 +335,10 @@ export abstract class RestApiBaseHandler {
|
||||
return orderedRecords;
|
||||
}
|
||||
|
||||
public getAuthContextFromRequest(request: Request): AuthContext {
|
||||
return {
|
||||
user: request.user,
|
||||
workspace: request.workspace,
|
||||
apiKey: request.apiKey,
|
||||
workspaceMemberId: request.workspaceMemberId,
|
||||
userWorkspaceId: request.userWorkspaceId,
|
||||
};
|
||||
public getAuthContextFromRequest(
|
||||
request: AuthenticatedRequest,
|
||||
): WorkspaceAuthContext {
|
||||
return request;
|
||||
}
|
||||
|
||||
public formatResult<T>({
|
||||
@@ -546,4 +552,62 @@ export abstract class RestApiBaseHandler {
|
||||
throw new BadRequestException(`Invalid cursor: ${cursor}`);
|
||||
}
|
||||
};
|
||||
|
||||
private getObjectsPermissions = async (authContext: WorkspaceAuthContext) => {
|
||||
let roleId: string;
|
||||
|
||||
if (isDefined(authContext.apiKey)) {
|
||||
roleId = await this.apiKeyRoleService.getRoleIdForApiKey(
|
||||
authContext.apiKey.id,
|
||||
authContext.workspace.id,
|
||||
);
|
||||
} else {
|
||||
const userWorkspaceRoleId =
|
||||
await this.userRoleService.getRoleIdForUserWorkspace({
|
||||
userWorkspaceId: authContext.userWorkspaceId,
|
||||
workspaceId: authContext.workspace.id,
|
||||
});
|
||||
|
||||
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: authContext.workspace.id,
|
||||
roleIds: [roleId],
|
||||
},
|
||||
);
|
||||
|
||||
return { objectsPermissions: objectMetadataPermissions[roleId] };
|
||||
};
|
||||
|
||||
async computeSelectedFields({
|
||||
authContext,
|
||||
depth,
|
||||
objectMetadataMapItem,
|
||||
objectMetadataMaps,
|
||||
}: {
|
||||
authContext: WorkspaceAuthContext;
|
||||
depth: Depth;
|
||||
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
}) {
|
||||
const { objectsPermissions } =
|
||||
await this.getObjectsPermissions(authContext);
|
||||
|
||||
return this.restToCommonSelectedFieldsHandler.computeFromDepth({
|
||||
objectsPermissions,
|
||||
objectMetadataMaps,
|
||||
objectMetadataMapItem,
|
||||
depth,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -2,19 +2,23 @@ import { Module } from '@nestjs/common';
|
||||
|
||||
import { CoreQueryBuilderFactory } from 'src/engine/api/rest/core/query-builder/core-query-builder.factory';
|
||||
import { coreQueryBuilderFactories } from 'src/engine/api/rest/core/query-builder/factories/factories';
|
||||
import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module';
|
||||
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module';
|
||||
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
AuthModule,
|
||||
ApiKeyModule,
|
||||
DomainManagerModule,
|
||||
FeatureFlagModule,
|
||||
WorkspaceCacheStorageModule,
|
||||
WorkspaceMetadataCacheModule,
|
||||
WorkspacePermissionsCacheModule,
|
||||
],
|
||||
providers: [...coreQueryBuilderFactories, CoreQueryBuilderFactory],
|
||||
exports: [CoreQueryBuilderFactory],
|
||||
|
||||
+1
-1
@@ -1,8 +1,8 @@
|
||||
import { CreateManyQueryFactory } from 'src/engine/api/rest/core/query-builder/factories/create-many-query.factory';
|
||||
import { CreateVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/create-variables.factory';
|
||||
import { FindDuplicatesQueryFactory } from 'src/engine/api/rest/core/query-builder/factories/find-duplicates-query.factory';
|
||||
import { FindDuplicatesVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/find-duplicates-variables.factory';
|
||||
import { GetVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/get-variables.factory';
|
||||
import { CreateVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/create-variables.factory';
|
||||
import { inputFactories } from 'src/engine/api/rest/input-factories/factories';
|
||||
|
||||
export const coreQueryBuilderFactories = [
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type Request } from 'express';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type QueryVariables } from 'src/engine/api/rest/core/types/query-variables.type';
|
||||
import { EndingBeforeInputFactory } from 'src/engine/api/rest/input-factories/ending-before-input.factory';
|
||||
@@ -29,7 +30,7 @@ export class GetVariablesFactory {
|
||||
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
|
||||
},
|
||||
): QueryVariables {
|
||||
if (id) {
|
||||
if (isDefined(id)) {
|
||||
return { filter: { id: { eq: id } } };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { CoreCommonApiModule } from 'src/engine/api/common/core-common-api.module';
|
||||
import { RestApiCoreController } from 'src/engine/api/rest/core/controllers/rest-api-core.controller';
|
||||
import { RestApiCreateManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-create-many.handler';
|
||||
import { RestApiCreateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-create-one.handler';
|
||||
@@ -11,13 +12,17 @@ import { RestApiFindOneHandler } from 'src/engine/api/rest/core/handlers/rest-ap
|
||||
import { RestApiUpdateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-update-one.handler';
|
||||
import { CoreQueryBuilderModule } from 'src/engine/api/rest/core/query-builder/core-query-builder.module';
|
||||
import { coreQueryBuilderFactories } from 'src/engine/api/rest/core/query-builder/factories/factories';
|
||||
import { restToCommonArgsHandlers } from 'src/engine/api/rest/core/rest-to-common-args-handlers/rest-to-common-args-handlers';
|
||||
import { RestApiCoreService } from 'src/engine/api/rest/core/services/rest-api-core.service';
|
||||
import { RestApiService } from 'src/engine/api/rest/rest-api.service';
|
||||
import { ActorModule } from 'src/engine/core-modules/actor/actor.module';
|
||||
import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module';
|
||||
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { RecordTransformerModule } from 'src/engine/core-modules/record-transformer/record-transformer.module';
|
||||
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
|
||||
import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module';
|
||||
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
|
||||
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
@@ -38,12 +43,16 @@ const restApiCoreResolvers = [
|
||||
WorkspaceCacheStorageModule,
|
||||
AuthModule,
|
||||
ApiKeyModule,
|
||||
UserRoleModule,
|
||||
HttpModule,
|
||||
TwentyORMModule,
|
||||
RecordTransformerModule,
|
||||
WorkspacePermissionsCacheModule,
|
||||
WorkspaceMetadataCacheModule,
|
||||
ActorModule,
|
||||
FeatureFlagModule,
|
||||
CoreCommonApiModule,
|
||||
DomainManagerModule,
|
||||
],
|
||||
controllers: [RestApiCoreController],
|
||||
providers: [
|
||||
@@ -51,6 +60,7 @@ const restApiCoreResolvers = [
|
||||
RestApiCoreService,
|
||||
...coreQueryBuilderFactories,
|
||||
...restApiCoreResolvers,
|
||||
...restToCommonArgsHandlers,
|
||||
],
|
||||
})
|
||||
export class RestApiCoreModule {}
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
import { RestToCommonSelectedFieldsHandler } from 'src/engine/api/rest/core/rest-to-common-args-handlers/selected-fields-handler';
|
||||
|
||||
export const restToCommonArgsHandlers = [RestToCommonSelectedFieldsHandler];
|
||||
+140
@@ -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 RestToCommonSelectedFieldsHandler {
|
||||
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 };
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,18 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type Request } from 'express';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
|
||||
import { RestApiDeleteOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-delete-one.handler';
|
||||
import { RestApiCreateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-create-one.handler';
|
||||
import { RestApiUpdateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-update-one.handler';
|
||||
import { RestApiFindOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-one.handler';
|
||||
import { RestApiFindManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-many.handler';
|
||||
import { RestApiCreateManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-create-many.handler';
|
||||
import { RestApiCreateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-create-one.handler';
|
||||
import { RestApiDeleteOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-delete-one.handler';
|
||||
import { RestApiFindDuplicatesHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-duplicates.handler';
|
||||
import { RestApiFindManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-many.handler';
|
||||
import { RestApiFindOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-one.handler';
|
||||
import { RestApiUpdateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-update-one.handler';
|
||||
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
|
||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
|
||||
@Injectable()
|
||||
export class RestApiCoreService {
|
||||
@@ -22,32 +24,42 @@ export class RestApiCoreService {
|
||||
private readonly restApiFindOneHandler: RestApiFindOneHandler,
|
||||
private readonly restApiFindManyHandler: RestApiFindManyHandler,
|
||||
private readonly restApiFindDuplicatesHandler: RestApiFindDuplicatesHandler,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
) {}
|
||||
|
||||
async delete(request: Request) {
|
||||
async delete(request: AuthenticatedRequest) {
|
||||
return await this.restApiDeleteOneHandler.handle(request);
|
||||
}
|
||||
|
||||
async createOne(request: Request) {
|
||||
async createOne(request: AuthenticatedRequest) {
|
||||
return await this.restApiCreateOneHandler.handle(request);
|
||||
}
|
||||
|
||||
async createMany(request: Request) {
|
||||
async createMany(request: AuthenticatedRequest) {
|
||||
return await this.restApiCreateManyHandler.handle(request);
|
||||
}
|
||||
|
||||
async findDuplicates(request: Request) {
|
||||
async findDuplicates(request: AuthenticatedRequest) {
|
||||
return await this.restApiFindDuplicatesHandler.handle(request);
|
||||
}
|
||||
|
||||
async update(request: Request) {
|
||||
async update(request: AuthenticatedRequest) {
|
||||
return await this.restApiUpdateOneHandler.handle(request);
|
||||
}
|
||||
|
||||
async get(request: Request) {
|
||||
async get(request: AuthenticatedRequest) {
|
||||
const { id: recordId } = parseCorePath(request);
|
||||
|
||||
if (isDefined(recordId)) {
|
||||
const isCommonApiEnabled = await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_COMMON_API_ENABLED,
|
||||
request.workspace.id,
|
||||
);
|
||||
|
||||
if (isCommonApiEnabled) {
|
||||
return await this.restApiFindOneHandler.commonHandle(request);
|
||||
}
|
||||
|
||||
return await this.restApiFindOneHandler.handle(request);
|
||||
} else {
|
||||
return await this.restApiFindManyHandler.handle(request);
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { type Request } from 'express';
|
||||
|
||||
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
|
||||
|
||||
export type AuthenticatedRequest = Request & WorkspaceAuthContext;
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { type QueryFailedError } from 'typeorm';
|
||||
|
||||
import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
|
||||
import { commonQueryRunnerToRestApiExceptionHandler } from 'src/engine/api/common/common-query-runners/utils/common-query-runner-to-rest-api-exception-handler.util';
|
||||
|
||||
interface QueryFailedErrorWithCode extends QueryFailedError {
|
||||
code: string;
|
||||
}
|
||||
|
||||
export const workspaceQueryRunnerRestApiExceptionHandler = (
|
||||
error: QueryFailedErrorWithCode,
|
||||
): never => {
|
||||
switch (true) {
|
||||
case error instanceof CommonQueryRunnerException:
|
||||
return commonQueryRunnerToRestApiExceptionHandler(error);
|
||||
default:
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
+1
@@ -19,4 +19,5 @@ export enum FeatureFlagKey {
|
||||
IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED',
|
||||
IS_EMAILING_DOMAIN_ENABLED = 'IS_EMAILING_DOMAIN_ENABLED',
|
||||
IS_DYNAMIC_SEARCH_FIELDS_ENABLED = 'IS_DYNAMIC_SEARCH_FIELDS_ENABLED',
|
||||
IS_COMMON_API_ENABLED = 'IS_COMMON_API_ENABLED',
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import {
|
||||
Injectable,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
|
||||
@@ -26,6 +28,10 @@ export class JwtAuthGuard implements CanActivate {
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (!isDefined(data.apiKey) && !isDefined(data.userWorkspaceId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
request.user = data.user;
|
||||
request.apiKey = data.apiKey;
|
||||
request.workspace = data.workspace;
|
||||
|
||||
+1
@@ -142,6 +142,7 @@ describe('WorkspaceEntityManager', () => {
|
||||
IS_PUBLIC_DOMAIN_ENABLED: false,
|
||||
IS_EMAILING_DOMAIN_ENABLED: false,
|
||||
IS_DYNAMIC_SEARCH_FIELDS_ENABLED: false,
|
||||
IS_COMMON_API_ENABLED: false,
|
||||
},
|
||||
eventEmitterService: {
|
||||
emitMutationEvent: jest.fn(),
|
||||
|
||||
+5
@@ -56,6 +56,11 @@ export const seedFeatureFlags = async (
|
||||
workspaceId: workspaceId,
|
||||
value: workspaceId === SEED_APPLE_WORKSPACE_ID,
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IS_COMMON_API_ENABLED,
|
||||
workspaceId: workspaceId,
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IS_PAGE_LAYOUT_ENABLED,
|
||||
workspaceId: workspaceId,
|
||||
|
||||
+2
@@ -62,9 +62,11 @@ describe('Core REST API Find One endpoint', () => {
|
||||
method: 'get',
|
||||
path: `/people/${NOT_EXISTING_TEST_PERSON_ID}`,
|
||||
})
|
||||
//TODO : Refacto-common - This should be a 404
|
||||
.expect(400)
|
||||
.expect((res) => {
|
||||
expect(res.body.messages[0]).toContain('Record not found');
|
||||
//TODO : Refacto-common - This should be a NotFoundException
|
||||
expect(res.body.error).toBe('BadRequestException');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user