Add getters factory for attachements (#4567)

* Add getter factory for attachements

* Override guard in test

* Add secret in env variables

* Return custom message on expiration

* Rename to signPayload

---------

Co-authored-by: Thomas Trompette <thomast@twenty.com>
This commit is contained in:
Thomas Trompette
2024-03-19 16:39:53 +01:00
committed by GitHub
parent 9f6c578a46
commit e579554d47
24 changed files with 306 additions and 55 deletions

View File

@ -1,7 +1,9 @@
import { QueryResultGettersFactory } from './query-result-getters.factory';
import { RecordPositionFactory } from './record-position.factory';
import { QueryRunnerArgsFactory } from './query-runner-args.factory';
export const workspaceQueryRunnerFactories = [
QueryRunnerArgsFactory,
RecordPositionFactory,
QueryResultGettersFactory,
];

View File

@ -0,0 +1,75 @@
import { Injectable } from '@nestjs/common';
import { addMilliseconds } from 'date-fns';
import ms from 'ms';
import { ObjectMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/object-metadata.interface';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { TokenService } from 'src/engine/modules/auth/services/token.service';
@Injectable()
export class QueryResultGettersFactory {
constructor(
private readonly tokenService: TokenService,
private readonly environmentService: EnvironmentService,
) {}
async create<Result>(
result: Result,
objectMetadataItem: ObjectMetadataInterface,
): Promise<Result> {
// TODO: look for file type once implemented
switch (objectMetadataItem.nameSingular) {
case 'attachment':
return this.applyAttachmentGetters(result);
default:
return result;
}
}
private async applyAttachmentGetters<Result>(
attachments: any,
): Promise<Result> {
if (!attachments || !attachments.edges) {
return attachments;
}
const fileTokenExpiresIn = this.environmentService.get(
'FILE_TOKEN_EXPIRES_IN',
);
const secret = this.environmentService.get('FILE_TOKEN_SECRET');
const mappedEdges = await Promise.all(
attachments.edges.map(async (attachment: any) => {
if (!attachment.node.id || !attachment?.node?.fullPath) {
return attachment;
}
const expirationDate = addMilliseconds(
new Date(),
ms(fileTokenExpiresIn),
);
const signedPayload = await this.tokenService.encodePayload(
{
expiration_date: expirationDate,
attachment_id: attachment.node.id,
},
{
secret,
},
);
attachment.node.fullPath = `${attachment.node.fullPath}?token=${signedPayload}`;
return attachment;
}),
);
return {
...attachments,
edges: mappedEdges,
} as Result;
}
}

View File

@ -5,11 +5,13 @@ import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/works
import { WorkspacePreQueryHookModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.module';
import { workspaceQueryRunnerFactories } from 'src/engine/api/graphql/workspace-query-runner/factories';
import { RecordPositionListener } from 'src/engine/api/graphql/workspace-query-runner/listeners/record-position.listener';
import { AuthModule } from 'src/engine/modules/auth/auth.module';
import { WorkspaceQueryRunnerService } from './workspace-query-runner.service';
@Module({
imports: [
AuthModule,
WorkspaceQueryBuilderModule,
WorkspaceDataSourceModule,
WorkspacePreQueryHookModule,

View File

@ -45,6 +45,7 @@ import { WorkspacePreQueryHookService } from 'src/engine/api/graphql/workspace-q
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { NotFoundError } from 'src/engine/filters/utils/graphql-errors.util';
import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory';
import { QueryResultGettersFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters.factory';
import { WorkspaceQueryRunnerOptions } from './interfaces/query-runner-option.interface';
import {
@ -61,6 +62,7 @@ export class WorkspaceQueryRunnerService {
private readonly workspaceQueryBuilderFactory: WorkspaceQueryBuilderFactory,
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
private readonly queryRunnerArgsFactory: QueryRunnerArgsFactory,
private readonly queryResultGettersFactory: QueryResultGettersFactory,
@Inject(MessageQueue.webhookQueue)
private readonly messageQueueService: MessageQueueService,
private readonly eventEmitter: EventEmitter2,
@ -133,7 +135,7 @@ export class WorkspaceQueryRunnerService {
);
const result = await this.execute(query, workspaceId);
const parsedResult = this.parseResult<IConnection<Record>>(
const parsedResult = await this.parseResult<IConnection<Record>>(
result,
objectMetadataItem,
'',
@ -174,7 +176,7 @@ export class WorkspaceQueryRunnerService {
workspaceId,
);
const parsedResult = this.parseResult<Record<string, unknown>>(
const parsedResult = await this.parseResult<Record<string, unknown>>(
existingRecordResult,
objectMetadataItem,
'',
@ -227,10 +229,12 @@ export class WorkspaceQueryRunnerService {
const result = await this.execute(query, workspaceId);
const parsedResults = this.parseResult<PGGraphQLMutation<Record>>(
result,
objectMetadataItem,
'insertInto',
const parsedResults = (
await this.parseResult<PGGraphQLMutation<Record>>(
result,
objectMetadataItem,
'insertInto',
)
)?.records;
await this.triggerWebhooks<Record>(
@ -280,10 +284,12 @@ export class WorkspaceQueryRunnerService {
const result = await this.execute(query, workspaceId);
const parsedResults = this.parseResult<PGGraphQLMutation<Record>>(
result,
objectMetadataItem,
'update',
const parsedResults = (
await this.parseResult<PGGraphQLMutation<Record>>(
result,
objectMetadataItem,
'update',
)
)?.records;
await this.triggerWebhooks<Record>(
@ -316,10 +322,12 @@ export class WorkspaceQueryRunnerService {
const result = await this.execute(query, workspaceId);
const parsedResults = this.parseResult<PGGraphQLMutation<Record>>(
result,
objectMetadataItem,
'update',
const parsedResults = (
await this.parseResult<PGGraphQLMutation<Record>>(
result,
objectMetadataItem,
'update',
)
)?.records;
await this.triggerWebhooks<Record>(
@ -349,10 +357,12 @@ export class WorkspaceQueryRunnerService {
const result = await this.execute(query, workspaceId);
const parsedResults = this.parseResult<PGGraphQLMutation<Record>>(
result,
objectMetadataItem,
'deleteFrom',
const parsedResults = (
await this.parseResult<PGGraphQLMutation<Record>>(
result,
objectMetadataItem,
'deleteFrom',
)
)?.records;
await this.triggerWebhooks<Record>(
@ -382,10 +392,12 @@ export class WorkspaceQueryRunnerService {
);
const result = await this.execute(query, workspaceId);
const parsedResults = this.parseResult<PGGraphQLMutation<Record>>(
result,
objectMetadataItem,
'deleteFrom',
const parsedResults = (
await this.parseResult<PGGraphQLMutation<Record>>(
result,
objectMetadataItem,
'deleteFrom',
)
)?.records;
await this.triggerWebhooks<Record>(
@ -445,11 +457,11 @@ export class WorkspaceQueryRunnerService {
return results;
}
private parseResult<Result>(
private async parseResult<Result>(
graphqlResult: PGGraphQLResult | undefined,
objectMetadataItem: ObjectMetadataInterface,
command: string,
): Result {
): Promise<Result> {
const entityKey = `${command}${computeObjectTargetTable(
objectMetadataItem,
)}Collection`;
@ -481,7 +493,12 @@ export class WorkspaceQueryRunnerService {
throw error;
}
return parseResult(result);
const resultWithGetters = await this.queryResultGettersFactory.create(
result,
objectMetadataItem,
);
return parseResult(resultWithGetters);
}
async executeAndParse<Result>(