Skip to content

Commit

Permalink
Merge pull request #53 from LabO8/feature/prefix-algorithm-setting
Browse files Browse the repository at this point in the history
feat: allow custom prefix algorithm implementation
  • Loading branch information
MartinAndreev authored Jan 5, 2024
2 parents 98e91a3 + 5c4e8ba commit c901b04
Show file tree
Hide file tree
Showing 16 changed files with 205 additions and 25 deletions.
66 changes: 66 additions & 0 deletions site/docs/prefixing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
id: prefixing
title: Prefixing
sidebar_label: Prefixing
slug: /prefixing
---

## Introduction

As our app grows, we might want to store our objects in a more organized way. This is where the prefix comes in.

The prefix is a string that is prepended to the object key. This allows us to organize our objects in a folder-like structure.

For example, if we have a bucket called `my-bucket` and we want to store our objects in a folder called `my-folder`, we can do that by prepending the prefix `my-folder/` to the object key.

## Usage

By default, the prefix is an empty string. This means that the object key is not modified, but if you set a prefix, when you initialize the module, the prefix will be prepended to the object key.

The default algorithm for prefixing will just prepend the prefix to the object key, but you can also specify a custom algorithm.

**All services like the `ObjectService` will use the prefix service by default.**

## Custom prefixing

In order to use a custom prefixing algorithm, you need to specify the `prefixingAlgorithm` when initializing the module.

```typescript
class CustomPrefixService implements IPrefixAlgorithm {
prefix(remote: string, prefix: string, bucket?: string): string {
return `${bucket}/${prefix}${remote}`;
}
}

S3Module.forRoot({
region: 'region',
accessKeyId: '***',
secretAccessKey: '***',
prefix: 'test/',
prefixAlgorithm: new CustomPrefixService(),
})
```

you can also use injectables

```typescript
class CustomPrefixWithDIService implements IPrefixAlgorithm {
public constructor(private readonly globalPrefix: string) {}

prefix(remote: string, prefix: string, bucket?: string): string {
return `${bucket}/${this.globalPrefix}${prefix}${remote}`;
}
}

S3Module.forRootAsync({
imports: [SomeModuleThatProvidesTheGlobalPrefix],
prefixAlgorithmInject: ['GLOBAL_PREFIX'],
prefixAlgorithmFactory: (globalPrefix: string) => new CustomPrefixWithDIService(globalPrefix),
useFactory: () => ({
region: 'region',
accessKeyId: '***',
secretAccessKey: '***',
prefix: 'test/',
}),
})
```
2 changes: 1 addition & 1 deletion site/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
const sidebars = {
docsSidebar: {
Introduction: ['getting-started'],
Services: ['buckets', 'objects', 'signed-url', 'download-helper', 'deletion-helper'],
Services: ['buckets', 'objects', 'signed-url', 'download-helper', 'prefix', 'deletion-helper'],
API: [
{
type: 'autogenerated',
Expand Down
2 changes: 1 addition & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const S3_CONFIG = 's3.config';
export const S3_SERVICE = 's3.service';

export const PREFIX_ALGORITHM = 'prefix.algorithm';
export const DEFAULT_EXPIRES_IN = 3600;
1 change: 1 addition & 0 deletions src/interfaces/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './prefix-algorithm.interface';
3 changes: 3 additions & 0 deletions src/interfaces/prefix-algorithm.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface IPrefixAlgorithm {
prefix(remote: string, prefix?: string, bucket?: string): string;
}
21 changes: 19 additions & 2 deletions src/s3.module.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { HttpModule } from '@nestjs/axios';
import { DynamicModule, Global, Module, Provider } from '@nestjs/common';
import { S3_CONFIG } from './constants';
import { PREFIX_ALGORITHM, S3_CONFIG } from './constants';
import { createS3ServiceProvider } from './s3-service.factory';
import { BucketsService, ObjectsService, PrefixService, SignedUrlService } from './services';
import {
BucketsService,
DefaultPrefixAlgorithmService,
ObjectsService,
PrefixService,
SignedUrlService,
} from './services';
import { S3AsyncConfig, S3Config } from './types';
import { DeletionService, DownloadService } from './utils';

Expand All @@ -21,6 +27,10 @@ const createSharedProviders = (config: S3Config): Provider[] => [
provide: S3_CONFIG,
useValue: config,
},
{
provide: PREFIX_ALGORITHM,
useValue: config.prefixAlgorithm ? (config.prefixAlgorithm as any) : new DefaultPrefixAlgorithmService(),
},
...providers,
];

Expand All @@ -30,6 +40,13 @@ const createSharedProvidersAsync = (provider: S3AsyncConfig): Provider[] => [
useFactory: provider.useFactory,
inject: provider.inject || [],
},
{
provide: PREFIX_ALGORITHM,
useFactory: provider.prefixAlgorithmFactory
? provider.prefixAlgorithmFactory
: () => new DefaultPrefixAlgorithmService(),
inject: provider.prefixAlgorithmInject || [],
},
...providers,
];

Expand Down
11 changes: 11 additions & 0 deletions src/services/default-prefix-algorithm.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { IPrefixAlgorithm } from '../interfaces';

export class DefaultPrefixAlgorithmService implements IPrefixAlgorithm {
prefix(remote: string, prefix?: string, bucket?: string): string {

Check warning on line 4 in src/services/default-prefix-algorithm.service.ts

View workflow job for this annotation

GitHub Actions / jest

'bucket' is defined but never used
if (!prefix) {
return remote;
}

return `${prefix}${remote}`;
}
}
1 change: 1 addition & 0 deletions src/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './buckers.service';
export * from './objects.service';
export * from './prefix.service';
export * from './signed-url.service';
export * from './default-prefix-algorithm.service';
8 changes: 4 additions & 4 deletions src/services/objects.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export class ObjectsService {
new PutObjectCommand({
Bucket: bucket,
Body: body,
Key: disableAutoPrefix ? remote : this.prefixService.prefix(remote),
Key: disableAutoPrefix ? remote : this.prefixService.prefix(remote, bucket),
...preparedOptions,
}),
);
Expand All @@ -73,7 +73,7 @@ export class ObjectsService {
return this.client.send(
new DeleteObjectCommand({
Bucket: bucket,
Key: disableAutoPrefix ? remote : this.prefixService.prefix(remote),
Key: disableAutoPrefix ? remote : this.prefixService.prefix(remote, bucket),
...preparedOptions,
}),
);
Expand All @@ -90,7 +90,7 @@ export class ObjectsService {
new DeleteObjectsCommand({
Bucket: bucket,
Delete: {
Objects: remotes.map((r) => ({ Key: disableAutoPrefix ? r : this.prefixService.prefix(r) })),
Objects: remotes.map((r) => ({ Key: disableAutoPrefix ? r : this.prefixService.prefix(r, bucket) })),
},
...preparedOptions,
}),
Expand All @@ -103,7 +103,7 @@ export class ObjectsService {
return this.client.send(
new GetObjectCommand({
Bucket: bucket,
Key: disableAutoPrefix ? remote : this.prefixService.prefix(remote),
Key: disableAutoPrefix ? remote : this.prefixService.prefix(remote, bucket),
...preparedOptions,
}),
);
Expand Down
16 changes: 8 additions & 8 deletions src/services/prefix.service.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { Inject, Injectable } from '@nestjs/common';
import { S3_CONFIG } from '../constants';
import { PREFIX_ALGORITHM, S3_CONFIG } from '../constants';
import { S3Config } from '../types';
import { IPrefixAlgorithm } from '../interfaces';

@Injectable()
export class PrefixService {
public constructor(@Inject(S3_CONFIG) private readonly config: S3Config) {}
public constructor(
@Inject(S3_CONFIG) private readonly config: S3Config,
@Inject(PREFIX_ALGORITHM) private readonly prefixAlgorithm: IPrefixAlgorithm,
) {}

public prefix(remote: string): string {
public prefix(remote: string, bucket?: string): string {
const { prefix } = this.config;

if (!prefix) {
return remote;
}

return `${prefix}${remote}`;
return this.prefixAlgorithm.prefix(remote, prefix, bucket);
}
}
8 changes: 4 additions & 4 deletions src/services/signed-url.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class SignedUrlService {
options?: PutObjectOptions,
): Promise<PutSignedUrl> {
const { disableAutoPrefix, options: preparedOptions } = prepareOptions(options);
const key = disableAutoPrefix ? remote : this.prefixService.prefix(remote);
const key = disableAutoPrefix ? remote : this.prefixService.prefix(remote, bucket);

const command = new PutObjectCommand({
Bucket: bucket,
Expand Down Expand Up @@ -54,7 +54,7 @@ export class SignedUrlService {

const command = new GetObjectCommand({
Bucket: bucket,
Key: disableAutoPrefix ? remote : this.prefixService.prefix(remote),
Key: disableAutoPrefix ? remote : this.prefixService.prefix(remote, bucket),
...preparedOptions,
});

Expand All @@ -73,7 +73,7 @@ export class SignedUrlService {

const command = new DeleteObjectCommand({
Bucket: bucket,
Key: disableAutoPrefix ? remote : this.prefixService.prefix(remote),
Key: disableAutoPrefix ? remote : this.prefixService.prefix(remote, bucket),
...preparedOptions,
});

Expand All @@ -93,7 +93,7 @@ export class SignedUrlService {
const command = new DeleteObjectsCommand({
Bucket: bucket,
Delete: {
Objects: remotes.map((r) => ({ Key: disableAutoPrefix ? r : this.prefixService.prefix(r) })),
Objects: remotes.map((r) => ({ Key: disableAutoPrefix ? r : this.prefixService.prefix(r, bucket) })),
},
...preparedOptions,
});
Expand Down
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './object-command-options.type';
export * from './s3-config.type';
export * from './signed-url.type';
export * from './disable-auto-prefix.type';
export * from './prefix-algorithm.type';
1 change: 1 addition & 0 deletions src/types/prefix-algorithm.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type PrefixAlgorithm = (remote: string, prefix?: string, bucket?: string) => string;
7 changes: 6 additions & 1 deletion src/types/s3-config.type.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import { Abstract, Type } from '@nestjs/common';
import { ModuleMetadata } from '@nestjs/common/interfaces';
import { PrefixAlgorithm } from './prefix-algorithm.type';

Check warning on line 3 in src/types/s3-config.type.ts

View workflow job for this annotation

GitHub Actions / jest

'PrefixAlgorithm' is defined but never used
import { IPrefixAlgorithm } from '../interfaces';

export type S3Config = {
region: string;
accessKeyId: string;
secretAccessKey: string;
prefix?: string;
endPoint?: string;
prefixAlgorithm?: IPrefixAlgorithm;
};

export type S3AsyncConfig = Pick<ModuleMetadata, 'imports' | 'providers'> & {
useFactory: (...args: any[]) => Promise<S3Config> | S3Config;
useFactory: (...args: any[]) => Promise<Omit<S3Config, 'prefixAlgorithm'>> | Omit<S3Config, 'prefixAlgorithm'>;
inject?: Array<Type<unknown> | string | symbol | Abstract<unknown>>;
prefixAlgorithmFactory?: (...args: any[]) => Promise<IPrefixAlgorithm> | IPrefixAlgorithm;
prefixAlgorithmInject?: Array<Type<unknown> | string | symbol | Abstract<unknown>>;
};
6 changes: 3 additions & 3 deletions src/utils/deletion.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { DeleteObjectOutput, DeleteObjectsCommand, ListObjectsV2Output, S3Client } from '@aws-sdk/client-s3';
import { Inject, Injectable } from '@nestjs/common';
import { S3_SERVICE } from '../constants';
import { PREFIX_ALGORITHM, S3_SERVICE } from '../constants';
import { DeleteObjectsOptions, ListObjectsV2Options } from '../types';
import { ObjectsService, PrefixService } from '../services';
import { prepareOptions } from '../helpers';
Expand All @@ -10,7 +10,7 @@ export class DeletionService {
public constructor(
@Inject(S3_SERVICE) private readonly client: S3Client,
private readonly objectsService: ObjectsService,
private readonly prefixService: PrefixService,
@Inject(PREFIX_ALGORITHM) private readonly prefixService: PrefixService,
) {}

/**
Expand All @@ -32,7 +32,7 @@ export class DeletionService {

do {
data = await this.objectsService.listObjectsV2(bucket, {
Prefix: disableAutoPrefix ? prefix : this.prefixService.prefix(prefix),
Prefix: disableAutoPrefix ? prefix : this.prefixService.prefix(prefix, bucket),
ContinuationToken: continuationToken,
...listOptions,
});
Expand Down
76 changes: 75 additions & 1 deletion tests/unit-tests/prefix-service.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Test, TestingModule } from '@nestjs/testing';
import { S3Module } from '../../src';
import { PrefixService } from '../../src/services';
import { IPrefixAlgorithm } from '../../src/interfaces';

describe('Prefix service', () => {
describe('Prefix service default implementation', () => {
let testingModule!: TestingModule;
let prefixService!: PrefixService;

Expand All @@ -25,3 +26,76 @@ describe('Prefix service', () => {
expect(prefixService.prefix('test.txt')).toEqual('test/test.txt');
});
});

describe('Prefix service custom implementation', () => {
let testingModule!: TestingModule;
let prefixService!: PrefixService;

beforeAll(async () => {
class CustomPrefixService implements IPrefixAlgorithm {
prefix(remote: string, prefix: string, bucket?: string): string {
return `${bucket}/${prefix}${remote}`;
}
}

testingModule = await Test.createTestingModule({
imports: [
S3Module.forRoot({
region: process.env.AWS_REGION,
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
prefix: 'test/',
prefixAlgorithm: new CustomPrefixService(),
}),
],
}).compile();

prefixService = testingModule.get(PrefixService);
});

it('should use the custom implementation', async () => {
expect(prefixService.prefix('test.txt', 'test-bucket')).toEqual('test-bucket/test/test.txt');
});
});

describe('Prefix service custom implementation with injection', () => {
let testingModule!: TestingModule;
let prefixService!: PrefixService;

beforeAll(async () => {
class CustomPrefixWithDIService implements IPrefixAlgorithm {
public constructor(private readonly globalPrefix: string) {}

prefix(remote: string, prefix: string, bucket?: string): string {
return `${bucket}/${this.globalPrefix}${prefix}${remote}`;
}
}

testingModule = await Test.createTestingModule({
imports: [
S3Module.forRootAsync({
providers: [
{
provide: 'GLOBAL_PREFIX',
useValue: 'global-prefix/',
},
],
prefixAlgorithmInject: ['GLOBAL_PREFIX'],
prefixAlgorithmFactory: (globalPrefix: string) => new CustomPrefixWithDIService(globalPrefix),
useFactory: () => ({
region: process.env.AWS_REGION,
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
prefix: 'test/',
}),
}),
],
}).compile();

prefixService = testingModule.get(PrefixService);
});

it('should use the custom implementation', async () => {
expect(prefixService.prefix('test.txt', 'test-bucket')).toEqual('test-bucket/global-prefix/test/test.txt');
});
});

0 comments on commit c901b04

Please sign in to comment.