Skip to content

Commit 57cfed3

Browse files
committed
create common find one query
1 parent 1fdfac4 commit 57cfed3

22 files changed

+737
-77
lines changed
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import { Inject, Injectable } from '@nestjs/common';
2+
3+
import {
4+
assertIsDefinedOrThrow,
5+
capitalize,
6+
isDefined,
7+
} from 'twenty-shared/utils';
8+
9+
import { CommonQueryRunnerOptions } from 'src/engine/api/common/interfaces/common-query-runner-options.interface';
10+
import { type ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
11+
import { type IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
12+
import { type IEdge } from 'src/engine/api/graphql/workspace-query-runner/interfaces/edge.interface';
13+
import { ResolverArgsType } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
14+
15+
import { CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type';
16+
import {
17+
CommonQueryArgs,
18+
CommonQueryNames,
19+
} from 'src/engine/api/common/types/common-query-args.type';
20+
import { OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS } from 'src/engine/api/graphql/graphql-query-runner/constants/objects-with-settings-permissions-requirements';
21+
import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper';
22+
import { QueryResultGettersFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory';
23+
import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory';
24+
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
25+
import { WorkspaceQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service';
26+
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/api-key-role.service';
27+
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
28+
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
29+
import { type PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
30+
import {
31+
PermissionsException,
32+
PermissionsExceptionCode,
33+
PermissionsExceptionMessage,
34+
} from 'src/engine/metadata-modules/permissions/permissions.exception';
35+
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
36+
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
37+
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
38+
39+
@Injectable()
40+
export abstract class CommonBaseQueryRunnerService<
41+
Input extends CommonQueryArgs,
42+
Response extends
43+
| ObjectRecord
44+
| ObjectRecord[]
45+
| IConnection<ObjectRecord, IEdge<ObjectRecord>>
46+
| IConnection<ObjectRecord, IEdge<ObjectRecord>>[],
47+
> {
48+
@Inject()
49+
protected readonly workspaceQueryHookService: WorkspaceQueryHookService;
50+
@Inject()
51+
protected readonly queryRunnerArgsFactory: QueryRunnerArgsFactory;
52+
@Inject()
53+
protected readonly queryResultGettersFactory: QueryResultGettersFactory;
54+
@Inject()
55+
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager;
56+
@Inject()
57+
protected readonly processNestedRelationsHelper: ProcessNestedRelationsHelper;
58+
@Inject()
59+
protected readonly permissionsService: PermissionsService;
60+
@Inject()
61+
protected readonly userRoleService: UserRoleService;
62+
@Inject()
63+
protected readonly apiKeyRoleService: ApiKeyRoleService;
64+
65+
public async execute(
66+
args: Input,
67+
options: CommonQueryRunnerOptions,
68+
operationName: CommonQueryNames,
69+
): Promise<Response | undefined> {
70+
try {
71+
const { authContext, objectMetadataItemWithFieldMaps } = options;
72+
const workspace = authContext.workspace;
73+
74+
assertIsDefinedOrThrow(workspace);
75+
76+
await this.validate(args, options);
77+
78+
const workspaceDataSource =
79+
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
80+
workspaceId: workspace.id,
81+
});
82+
83+
if (objectMetadataItemWithFieldMaps.isSystem === true) {
84+
await this.validateSettingsPermissionsOnObjectOrThrow(options);
85+
}
86+
87+
const hookedArgs =
88+
await this.workspaceQueryHookService.executePreQueryHooks(
89+
authContext,
90+
objectMetadataItemWithFieldMaps.nameSingular,
91+
operationName,
92+
args,
93+
);
94+
95+
const computedArgs = (await this.queryRunnerArgsFactory.create(
96+
hookedArgs,
97+
options,
98+
ResolverArgsType[
99+
//TODO : Refacto-common
100+
capitalize(operationName) as keyof typeof ResolverArgsType
101+
],
102+
//TODO : Refacto-common
103+
)) as Input;
104+
105+
const roleId = await this.getRoleId(authContext, workspace.id);
106+
107+
const repository = workspaceDataSource.getRepository(
108+
objectMetadataItemWithFieldMaps.nameSingular,
109+
false,
110+
roleId,
111+
authContext,
112+
);
113+
114+
const commonBaseMethodExecutionArgs = {
115+
args: computedArgs,
116+
options,
117+
workspaceDataSource,
118+
repository,
119+
selectedFieldsResult: args.selectedFieldsResult,
120+
isExecutedByApiKey: isDefined(authContext.apiKey),
121+
roleId,
122+
shouldBypassPermissionChecks: false,
123+
};
124+
125+
const results = await this.run(
126+
commonBaseMethodExecutionArgs,
127+
workspaceDataSource.featureFlagMap,
128+
);
129+
130+
const resultWithGetters = await this.queryResultGettersFactory.create(
131+
results,
132+
objectMetadataItemWithFieldMaps,
133+
workspace.id,
134+
options.objectMetadataMaps,
135+
);
136+
137+
await this.workspaceQueryHookService.executePostQueryHooks(
138+
authContext,
139+
objectMetadataItemWithFieldMaps.nameSingular,
140+
operationName,
141+
resultWithGetters,
142+
);
143+
144+
return resultWithGetters;
145+
} catch (error) {
146+
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
147+
}
148+
}
149+
150+
private async validateSettingsPermissionsOnObjectOrThrow(
151+
options: CommonQueryRunnerOptions,
152+
) {
153+
const { authContext, objectMetadataItemWithFieldMaps } = options;
154+
155+
const workspace = authContext.workspace;
156+
157+
assertIsDefinedOrThrow(workspace);
158+
159+
if (
160+
Object.keys(OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS).includes(
161+
objectMetadataItemWithFieldMaps.nameSingular,
162+
)
163+
) {
164+
const permissionRequired: PermissionFlagType =
165+
// @ts-expect-error legacy noImplicitAny
166+
OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS[
167+
objectMetadataItemWithFieldMaps.nameSingular
168+
];
169+
170+
const userHasPermission =
171+
await this.permissionsService.userHasWorkspaceSettingPermission({
172+
userWorkspaceId: authContext.userWorkspaceId,
173+
setting: permissionRequired,
174+
workspaceId: workspace.id,
175+
apiKeyId: authContext.apiKey?.id,
176+
});
177+
178+
if (!userHasPermission) {
179+
throw new PermissionsException(
180+
PermissionsExceptionMessage.PERMISSION_DENIED,
181+
PermissionsExceptionCode.PERMISSION_DENIED,
182+
);
183+
}
184+
}
185+
}
186+
187+
private async getRoleId(authContext: AuthContext, workspaceId: string) {
188+
let roleId: string;
189+
190+
if (
191+
!isDefined(authContext.apiKey) &&
192+
!isDefined(authContext.userWorkspaceId)
193+
) {
194+
throw new PermissionsException(
195+
PermissionsExceptionMessage.NO_AUTHENTICATION_CONTEXT,
196+
PermissionsExceptionCode.NO_AUTHENTICATION_CONTEXT,
197+
);
198+
}
199+
200+
if (isDefined(authContext.apiKey)) {
201+
roleId = await this.apiKeyRoleService.getRoleIdForApiKey(
202+
authContext.apiKey.id,
203+
workspaceId,
204+
);
205+
} else {
206+
const userWorkspaceRoleId =
207+
await this.userRoleService.getRoleIdForUserWorkspace({
208+
userWorkspaceId: authContext.userWorkspaceId,
209+
workspaceId,
210+
});
211+
212+
if (!isDefined(userWorkspaceRoleId)) {
213+
throw new PermissionsException(
214+
PermissionsExceptionMessage.NO_ROLE_FOUND_FOR_USER_WORKSPACE,
215+
PermissionsExceptionCode.NO_ROLE_FOUND_FOR_USER_WORKSPACE,
216+
);
217+
}
218+
219+
roleId = userWorkspaceRoleId;
220+
}
221+
222+
return roleId;
223+
}
224+
225+
protected abstract run(
226+
executionArgs: CommonBaseQueryRunnerContext<Input>,
227+
featureFlagsMap: Record<FeatureFlagKey, boolean>,
228+
): Promise<Response>;
229+
230+
protected abstract validate(
231+
args: Input,
232+
options: CommonQueryRunnerOptions,
233+
): Promise<void>;
234+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { Injectable } from '@nestjs/common';
2+
3+
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
4+
import { FindOptionsRelations, ObjectLiteral } from 'typeorm';
5+
6+
import {
7+
ObjectRecord,
8+
ObjectRecordFilter,
9+
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
10+
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
11+
import { FindOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
12+
13+
import { CommonBaseQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-base-query-runner.service';
14+
import { CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type';
15+
import { FindOneQueryArgs } from 'src/engine/api/common/types/common-query-args.type';
16+
import {
17+
GraphqlQueryRunnerException,
18+
GraphqlQueryRunnerExceptionCode,
19+
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
20+
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
21+
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
22+
import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select';
23+
import {
24+
WorkspaceQueryRunnerException,
25+
WorkspaceQueryRunnerExceptionCode,
26+
} from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception';
27+
28+
@Injectable()
29+
export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerService<
30+
FindOneQueryArgs,
31+
ObjectRecord
32+
> {
33+
async run(
34+
executionContext: CommonBaseQueryRunnerContext<FindOneQueryArgs>,
35+
): Promise<ObjectRecord> {
36+
const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } =
37+
executionContext.options;
38+
39+
const { roleId } = executionContext;
40+
41+
const queryBuilder = executionContext.repository.createQueryBuilder(
42+
objectMetadataItemWithFieldMaps.nameSingular,
43+
);
44+
45+
//TODO : Refacto-common - QueryParser should be common branded service
46+
const commonQueryParser = new GraphqlQueryParser(
47+
objectMetadataItemWithFieldMaps,
48+
objectMetadataMaps,
49+
);
50+
51+
commonQueryParser.applyFilterToBuilder(
52+
queryBuilder,
53+
objectMetadataItemWithFieldMaps.nameSingular,
54+
executionContext.args.filter ?? ({} as ObjectRecordFilter),
55+
);
56+
57+
commonQueryParser.applyDeletedAtToBuilder(
58+
queryBuilder,
59+
executionContext.args.filter ?? ({} as ObjectRecordFilter),
60+
);
61+
62+
const columnsToSelect = buildColumnsToSelect({
63+
select: executionContext.selectedFieldsResult.select,
64+
relations: executionContext.selectedFieldsResult.relations,
65+
objectMetadataItemWithFieldMaps,
66+
objectMetadataMaps,
67+
});
68+
69+
const objectRecord = await queryBuilder
70+
.setFindOptions({
71+
select: columnsToSelect,
72+
})
73+
.getOne();
74+
75+
if (!objectRecord) {
76+
//TODO : Refacto-common - Exception handler should be Common
77+
throw new GraphqlQueryRunnerException(
78+
'Record not found',
79+
GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND,
80+
);
81+
}
82+
83+
const objectRecords = [objectRecord] as ObjectRecord[];
84+
85+
if (executionContext.selectedFieldsResult.relations) {
86+
await this.processNestedRelationsHelper.processNestedRelations({
87+
objectMetadataMaps,
88+
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
89+
parentObjectRecords: objectRecords,
90+
//TODO : Refacto-common - To fix when switching processNestedRelationsHelper to Common
91+
relations: executionContext.selectedFieldsResult.relations as Record<
92+
string,
93+
FindOptionsRelations<ObjectLiteral>
94+
>,
95+
limit: QUERY_MAX_RECORDS,
96+
authContext,
97+
workspaceDataSource: executionContext.workspaceDataSource,
98+
roleId,
99+
shouldBypassPermissionChecks:
100+
executionContext.shouldBypassPermissionChecks,
101+
selectedFields: executionContext.selectedFieldsResult.select,
102+
});
103+
}
104+
105+
const typeORMObjectRecordsParser =
106+
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
107+
108+
return typeORMObjectRecordsParser.processRecord({
109+
objectRecord: objectRecords[0],
110+
objectName: objectMetadataItemWithFieldMaps.nameSingular,
111+
take: 1,
112+
totalCount: 1,
113+
}) as ObjectRecord;
114+
}
115+
116+
async validate(
117+
args: FindOneResolverArgs<ObjectRecordFilter>,
118+
_options: WorkspaceQueryRunnerOptions,
119+
): Promise<void> {
120+
if (!args.filter || Object.keys(args.filter).length === 0) {
121+
throw new WorkspaceQueryRunnerException(
122+
'Missing filter argument',
123+
WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
124+
);
125+
}
126+
}
127+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { CommonFindOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-one-query-runner.service';
2+
3+
export const CommonQueryRunners = [CommonFindOneQueryRunnerService];

0 commit comments

Comments
 (0)