Skip to content

Commit 5f4b6c7

Browse files
committed
chore: use POST to upload postman attachments
1 parent cedc621 commit 5f4b6c7

File tree

12 files changed

+288
-254
lines changed

12 files changed

+288
-254
lines changed

package-lock.json

Lines changed: 8 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"@apollo/server": "4.9.4",
3232
"@aws-sdk/client-dynamodb": "3.460.0",
3333
"@aws-sdk/client-s3": "3.369.0",
34-
"@aws-sdk/s3-request-presigner": "3.369.0",
34+
"@aws-sdk/s3-presigned-post": "3.369.0",
3535
"@bull-board/express": "5.17.0",
3636
"@graphql-tools/schema": "10.0.0",
3737
"@launchdarkly/node-server-sdk": "9.0.4",

packages/backend/src/graphql/__tests__/mutations/generate-presigned-url.itest.ts renamed to packages/backend/src/graphql/__tests__/mutations/generate-presigned-post.itest.ts

Lines changed: 46 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
22

33
import { ForbiddenError } from '@/errors/graphql-errors'
44
import { generateMockContext } from '@/graphql/__tests__/mutations/tiles/table.mock'
5-
import generatePresignedUrl from '@/graphql/mutations/generate-presigned-url'
5+
import generatePresignedPost from '@/graphql/mutations/generate-presigned-post'
66
import Flow from '@/models/flow'
77
import Context from '@/types/express/context'
88

@@ -12,59 +12,76 @@ const VALID_PARAMS = {
1212
fileType: 'text/plain',
1313
size: 100,
1414
updatedAt: new Date().toISOString(),
15-
manualUpload: true,
1615
}
1716

18-
const mocks = vi.hoisted(() => ({
19-
getSignedUrl: vi.fn(),
17+
// Mock the s3 helpers module
18+
vi.mock('@/helpers/s3', () => ({
19+
getPresignedPost: vi.fn(),
20+
COMMON_S3_BUCKET: 'test-bucket',
21+
COMMON_S3_MOCK_FOLDER_PREFIX: 's3:test-bucket:mock/',
22+
parseS3Id: vi.fn(),
23+
MAX_FILE_SIZE: 1024 * 1024 * 2,
24+
ACCEPTED_FILE_TYPES: ['text/plain'],
25+
validateObjectKey: vi.fn((objectKey) => {
26+
const invalidCharacters = /[\\{}^`%~#<>|[\]]/
27+
if (invalidCharacters.test(objectKey)) {
28+
return false
29+
}
30+
31+
// validate length of object key
32+
const byteLength = Buffer.byteLength(objectKey, 'utf-8')
33+
return byteLength <= 1024
34+
}),
2035
}))
2136

22-
vi.mock('@aws-sdk/s3-request-presigner', () => ({
23-
getSignedUrl: mocks.getSignedUrl,
24-
}))
37+
import { COMMON_S3_BUCKET, getPresignedPost } from '@/helpers/s3'
2538

26-
describe('generatePresignedUrl', () => {
39+
describe('generatePresignedPost', () => {
2740
let context: Context
2841
beforeEach(async () => {
2942
context = await generateMockContext()
43+
vi.clearAllMocks()
3044
})
3145

3246
it('should generate a presigned url', async () => {
33-
const expectedUrl = 'https://presigned-url.example.com'
34-
mocks.getSignedUrl.mockResolvedValueOnce(expectedUrl)
35-
3647
await Flow.query().insert({
3748
id: VALID_PARAMS.flowId,
3849
name: 'Test Flow',
3950
userId: context.currentUser.id,
4051
})
4152

42-
const result = await generatePresignedUrl(
43-
null,
44-
{ input: VALID_PARAMS },
45-
context,
53+
await generatePresignedPost(null, { input: VALID_PARAMS }, context)
54+
expect(getPresignedPost).toHaveBeenCalledWith(
55+
COMMON_S3_BUCKET,
56+
expect.stringMatching(
57+
new RegExp(
58+
`^${VALID_PARAMS.flowId}/[a-f0-9-]+/${VALID_PARAMS.filename}$`,
59+
),
60+
),
61+
VALID_PARAMS.fileType,
62+
{
63+
flowId: VALID_PARAMS.flowId,
64+
filename: VALID_PARAMS.filename,
65+
size: VALID_PARAMS.size.toString(),
66+
updatedAt: VALID_PARAMS.updatedAt,
67+
},
4668
)
47-
const expectedKeys = ['url', 's3Id']
48-
expect(Object.keys(result)).toEqual(expectedKeys)
49-
expect(result.s3Id).toContain(VALID_PARAMS.flowId)
5069
})
5170

5271
it('should throw an error if the user does not have access to the flow', async () => {
53-
vi.fn()
54-
.mockRejectedValue(Flow.hasAccess)
55-
.mockRejectedValue(
56-
new ForbiddenError('You do not have access to this flow'),
57-
)
72+
const otherUserContext = await generateMockContext()
73+
await Flow.query()
74+
.patch({
75+
userId: otherUserContext.currentUser.id,
76+
})
77+
.where('id', VALID_PARAMS.flowId)
5878

5979
await expect(
60-
generatePresignedUrl(null, { input: VALID_PARAMS }, context),
80+
generatePresignedPost(null, { input: VALID_PARAMS }, context),
6181
).rejects.toThrow(ForbiddenError)
6282
})
6383

6484
it('should throw an error if the file size is too large', async () => {
65-
const expectedUrl = 'https://presigned-url.example.com'
66-
mocks.getSignedUrl.mockResolvedValueOnce(expectedUrl)
67-
6885
await Flow.query().insert({
6986
id: VALID_PARAMS.flowId,
7087
name: 'Test Flow',
@@ -73,7 +90,7 @@ describe('generatePresignedUrl', () => {
7390

7491
const tooLargeParams = { ...VALID_PARAMS, size: 2 * 1024 * 1024 + 1 }
7592
await expect(
76-
generatePresignedUrl(null, { input: tooLargeParams }, context),
93+
generatePresignedPost(null, { input: tooLargeParams }, context),
7794
).rejects.toThrow('Size of attachment exceeds 2MB')
7895
})
7996

@@ -85,9 +102,6 @@ describe('generatePresignedUrl', () => {
85102
])(
86103
'should throw an error if the file type is not supported: %s',
87104
async (fileType) => {
88-
const expectedUrl = 'https://presigned-url.example.com'
89-
mocks.getSignedUrl.mockResolvedValueOnce(expectedUrl)
90-
91105
await Flow.query().insert({
92106
id: VALID_PARAMS.flowId,
93107
name: 'Test Flow',
@@ -100,7 +114,7 @@ describe('generatePresignedUrl', () => {
100114
}
101115

102116
await expect(
103-
generatePresignedUrl(null, { input: unsupportedParams }, context),
117+
generatePresignedPost(null, { input: unsupportedParams }, context),
104118
).rejects.toThrow('Unsupported file type')
105119
},
106120
)

packages/backend/src/graphql/mutation-resolvers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import deleteUploadedFile from './mutations/delete-uploaded-file'
1313
import duplicateFlow from './mutations/duplicate-flow'
1414
import executeStep from './mutations/execute-step'
1515
import generateAuthUrl from './mutations/generate-auth-url'
16-
import generatePresignedUrl from './mutations/generate-presigned-url'
16+
import generatePresignedPost from './mutations/generate-presigned-post'
1717
import loginWithSelectedSgid from './mutations/login-with-selected-sgid'
1818
import loginWithSgid from './mutations/login-with-sgid'
1919
import loginWithSso from './mutations/login-with-sso'
@@ -82,7 +82,7 @@ export default {
8282
updateFlowTransferStatus,
8383
duplicateFlow,
8484
deleteUploadedFile,
85-
generatePresignedUrl,
85+
generatePresignedPost,
8686
...tilesMutationResolvers,
8787

8888
// This is a special stub that enables us to group all our admin-related
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { PresignedPost } from '@aws-sdk/s3-presigned-post'
2+
import { randomUUID } from 'crypto'
3+
4+
import {
5+
ACCEPTED_FILE_TYPES,
6+
COMMON_S3_BUCKET,
7+
getPresignedPost,
8+
MAX_FILE_SIZE,
9+
validateObjectKey,
10+
} from '@/helpers/s3'
11+
import Flow from '@/models/flow'
12+
13+
import { MutationResolvers } from '../__generated__/types.generated'
14+
15+
const generatePresignedPost: MutationResolvers['generatePresignedPost'] =
16+
async (_parent, params, context) => {
17+
const { flowId, filename, fileType, size, updatedAt } = params.input
18+
19+
if (size > MAX_FILE_SIZE) {
20+
throw new Error('Size of attachment exceeds 2MB')
21+
}
22+
if (!ACCEPTED_FILE_TYPES.includes(fileType)) {
23+
throw new Error('Unsupported file type')
24+
}
25+
26+
const uuid = randomUUID()
27+
const objectKey = `${flowId}/${uuid}/${filename}`
28+
if (!validateObjectKey(objectKey)) {
29+
throw new Error('File path cannot be longer than 1024 bytes')
30+
}
31+
32+
await Flow.hasAccess(context.currentUser.id, flowId)
33+
34+
const presignedPost = await getPresignedPost(
35+
COMMON_S3_BUCKET,
36+
objectKey,
37+
fileType,
38+
{
39+
flowId,
40+
filename,
41+
size: size.toString(),
42+
updatedAt,
43+
},
44+
)
45+
46+
return {
47+
presignedPost: presignedPost as PresignedPost,
48+
s3Id: `s3:${COMMON_S3_BUCKET}:${objectKey}`,
49+
}
50+
}
51+
52+
export default generatePresignedPost

packages/backend/src/graphql/mutations/generate-presigned-url.ts

Lines changed: 0 additions & 79 deletions
This file was deleted.

packages/backend/src/graphql/schema.graphql

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ type AdminQuery {
6363
getPausedGroups(appKey: String!): [String!]!
6464
}
6565

66-
6766
type Mutation {
6867
createConnection(input: CreateConnectionInput): Connection
6968
generateAuthUrl(input: GenerateAuthUrlInput): AuthLink
@@ -101,7 +100,7 @@ type Mutation {
101100
): LoginWithSelectedSgidResult!
102101
duplicateFlow(input: DuplicateFlowInput): Flow
103102
# S3 operations
104-
generatePresignedUrl(input: GeneratePresignedUrlInput): PresignedUrl
103+
generatePresignedPost(input: GeneratePresignedPostInput): PreSignedPostObject
105104
deleteUploadedFile(id: String!): Boolean
106105
# Tiles
107106
createTable(input: CreateTableInput!): TableMetadata!
@@ -123,7 +122,6 @@ type Mutation {
123122
admin: AdminMutation!
124123
}
125124

126-
127125
type AdminMutation {
128126
pauseGroup(input: PauseGroupInput!): Boolean
129127
resumeGroup(input: ResumeGroupInput!): Boolean
@@ -523,13 +521,12 @@ input DuplicateFlowInput {
523521
id: String!
524522
}
525523

526-
input GeneratePresignedUrlInput {
524+
input GeneratePresignedPostInput {
527525
flowId: String!
528526
filename: String!
529527
fileType: String!
530528
size: Int!
531529
updatedAt: String!
532-
manualUpload: Boolean
533530
}
534531

535532
input CreateStepInput {
@@ -601,8 +598,13 @@ input PreviousStepInput {
601598
id: String
602599
}
603600

604-
type PresignedUrl {
601+
type PresignedPost {
605602
url: String!
603+
fields: JSONObject!
604+
}
605+
606+
type PreSignedPostObject {
607+
presignedPost: PresignedPost!
606608
s3Id: String!
607609
}
608610

0 commit comments

Comments
 (0)