Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/core/manifest/src/config/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export default (): {
s3Region: string
s3AccessKeyId: string
s3SecretAccessKey: string
s3Provider: string
}
} => {
return {
Expand All @@ -13,7 +14,8 @@ export default (): {
s3Endpoint: process.env.S3_ENDPOINT,
s3Region: process.env.S3_REGION,
s3AccessKeyId: process.env.S3_ACCESS_KEY_ID,
s3SecretAccessKey: process.env.S3_SECRET_ACCESS_KEY
s3SecretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
s3Provider: process.env.S3_PROVIDER,
}
}
}
35 changes: 28 additions & 7 deletions packages/core/manifest/src/storage/services/storage.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,27 @@ export class StorageService {
this.s3Region = this.configService.get('storage.s3Region')
this.s3AccessKeyId = this.configService.get('storage.s3AccessKeyId')
this.s3SecretAccessKey = this.configService.get('storage.s3SecretAccessKey')
this.s3provider = this.s3Endpoint?.includes('amazon')
? 'aws'
: this.s3Endpoint?.includes('digitalocean')
? 'digitalocean'
: 'other'
this.s3provider = this.configService.get('storage.s3Provider')
? this.configService.get('storage.s3Provider')
: this.s3Endpoint?.includes('amazon')
? 'aws'
: this.s3Endpoint?.includes('digitalocean')
? 'digitalocean'
: 'other'

if (this.isS3Enabled) {
let s3Endpoint = this.configService.get('storage.s3Endpoint')
if (this.s3provider === 'supabase') {
s3Endpoint = this.s3Endpoint + '/s3'
}
this.s3Client = new S3Client({
region: this.s3Region,
endpoint: this.s3Endpoint,
endpoint: s3Endpoint,
credentials: {
accessKeyId: this.s3AccessKeyId,
secretAccessKey: this.s3SecretAccessKey
},
forcePathStyle: false
forcePathStyle: this.s3provider === 'supabase'
})
}
}
Expand Down Expand Up @@ -193,6 +199,9 @@ export class StorageService {
* @returns The S3 file URL.
*/
private async uploadToS3(key: string, buffer: Buffer): Promise<string> {
if (this.s3provider === 'supabase') {
return this.uploadToSupabase(key, buffer)
}
await this.s3Client.send(
new PutObjectCommand({
Bucket: this.s3Bucket,
Expand All @@ -204,4 +213,16 @@ export class StorageService {
)
return `${this.s3Endpoint}/${this.s3Bucket}/${STORAGE_PATH}/${key}`
}

private async uploadToSupabase(key: string, buffer: Buffer): Promise<string> {
await this.s3Client.send(
new PutObjectCommand({
Bucket: this.s3Bucket,
Key: `${STORAGE_PATH}/${key}`,
Body: buffer,
})
)

return `${this.s3Endpoint}/object/public/${this.s3Bucket}/${STORAGE_PATH}/${key}`;
}
}
101 changes: 101 additions & 0 deletions packages/core/manifest/src/storage/tests/storage.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { DEFAULT_IMAGE_SIZES } from '../../constants'
import { ImageSizesObject } from '../../../../types/src'
import { ConfigService } from '@nestjs/config'
import { StorageService } from '../services/storage.service'
import { S3Client, PutObjectCommand, PutObjectCommandOutput } from '@aws-sdk/client-s3'
import { Command } from '@smithy/smithy-client';

const fs = require('fs')
const mkdirp = require('mkdirp')
Expand Down Expand Up @@ -192,4 +194,103 @@ describe('StorageService', () => {
expect(filePaths.huge).toBe('s3-image-url')
})
})

describe('Supabase storage', () => {
beforeEach(() => {
file.originalname = 'file.jpg'
image.originalname = 'test.jpg'

jest.clearAllMocks()

jest.spyOn(configService, 'get').mockImplementation((key: string) => {
switch (key) {
case 'storage.s3Bucket':
return 'test-supabase-bucket'
case 'storage.s3Endpoint':
return 'https://xyz.supabase.co'
case 'storage.s3Region':
return 'us-west-1'
case 'storage.s3AccessKeyId':
return 'test-supabase-access-key-id'
case 'storage.s3SecretAccessKey':
return 'test-supabase-secret-access-key'
case 'storage.s3Provider':
return 'supabase'
default:
return null
}
})

// Mock S3Client and its send method
jest.spyOn(S3Client.prototype, 'send').mockImplementation(
async <InputType extends object, OutputType extends object>(
command: Command<InputType, OutputType, any>
): Promise<OutputType> => {
// Check if the command is PutObjectCommand and return a mock PutObjectCommandOutput
if (command instanceof PutObjectCommand) {
return Promise.resolve({
$metadata: { httpStatusCode: 200 },
ETag: 'mock-etag',
} as PutObjectCommandOutput as OutputType);
}
// Return a generic metadata object for unhandled commands
return Promise.resolve({ $metadata: {} } as OutputType);
}
);

service = new StorageService(configService)
})

it('should initialize the S3 client for Supabase', () => {
expect(service['s3Client']).toBeDefined()
// Check if forcePathStyle is true for Supabase
expect(service['s3Client'].config.forcePathStyle).toBe(true)
})

it('should upload a file to Supabase', async () => {
const sendSpy = jest.spyOn(S3Client.prototype, 'send')

const filePath = await service.store(entity, property, file)
expect(sendSpy).toHaveBeenCalledWith(expect.any(PutObjectCommand))
// Expect Supabase URL format with date folder and unique ID
expect(filePath).toMatch(
new RegExp(
`^https://xyz\\.supabase\\.co/object/public/test-supabase-bucket/storage/${entity}/${property}/[A-Za-z]{3}\\d{4}/[a-z0-9]+-file\\.jpg$`
)
)
})

it('should upload an image to Supabase', async () => {
const sendSpy = jest.spyOn(S3Client.prototype, 'send')

const imageSizes: ImageSizesObject = {
tiny: {
height: 100
},
huge: {
width: 1000
}
}

const filePaths = await service.storeImage(
entity,
property,
image,
imageSizes
)
// Expect 2 calls, one for each size
expect(sendSpy).toHaveBeenCalledTimes(2)
// Expect Supabase URL format with date folder and unique ID
expect(filePaths.tiny).toMatch(
new RegExp(
`^https://xyz\\.supabase\\.co/object/public/test-supabase-bucket/storage/${entity}/${property}/[A-Za-z]{3}\\d{4}/[a-z0-9]+-tiny\\.jpg$`
)
)
expect(filePaths.huge).toMatch(
new RegExp(
`^https://xyz\\.supabase\\.co/object/public/test-supabase-bucket/storage/${entity}/${property}/[A-Za-z]{3}\\d{4}/[a-z0-9]+-huge\\.jpg$`
)
)
})
})
})