Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Api gateway create bucket #149

Merged
merged 14 commits into from
Nov 19, 2024
2 changes: 2 additions & 0 deletions packages/api-gateway/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { UserModule } from './modules/user/user.module';
import { EmailModule } from './modules/email/email.module';
import { SentryModule } from '@sentry/nestjs/setup';
import { FileProviderModule } from './modules/file_provider/file_provider.module';
import { FileBucketModule } from './modules/file_bucket/file_bucket.module';

@Module({
imports: [
Expand All @@ -14,6 +15,7 @@ import { FileProviderModule } from './modules/file_provider/file_provider.module
UserModule,
EmailModule,
FileProviderModule,
FileBucketModule,
],
})
export class AppModule {}
79 changes: 79 additions & 0 deletions packages/api-gateway/src/models/file_bucket.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { IsNotEmpty, IsString, IsInt } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { IdentifierProto } from 'juno-proto';

export class RegisterFileBucketModel {
@ApiProperty({
type: 'string',
description: 'The unique name of the bucket',
})
@IsNotEmpty()
@IsString()
name: string;

@ApiProperty({
type: 'number',
description: 'Configuration ID for the bucket',
})
@IsNotEmpty()
@IsInt()
configId: number;

@ApiProperty({
type: 'string',
description: 'The file provider name associated with the bucket',
})
@IsNotEmpty()
@IsString()
fileProviderName: string;

@ApiProperty({
type: [],
description: 'The file identifiers linked to this bucket',
})
FileServiceFile: Array<IdentifierProto.FileIdentifier>;
}

export class FileBucketResponse {
@ApiProperty({
type: 'string',
description: 'The unique name of the registered bucket',
})
@IsNotEmpty()
@IsString()
name: string;

@ApiProperty({
type: 'number',
description: 'Configuration ID associated with the registered bucket',
})
@IsNotEmpty()
@IsInt()
configId: number;

@ApiProperty({
type: 'string',
description: 'The name of the file provider associated with the bucket',
})
@IsNotEmpty()
@IsString()
fileProviderName: string;

@ApiProperty({
type: [],
description: 'The list of file identifiers associated with the bucket',
})
FileServiceFile: Array<IdentifierProto.FileIdentifier>;

constructor(bucketData: {
name: string;
configId: number;
fileProviderName: string;
FileServiceFile: Array<IdentifierProto.FileIdentifier>;
}) {
this.name = bucketData.name;
this.configId = bucketData.configId;
this.fileProviderName = bucketData.fileProviderName;
this.FileServiceFile = bucketData.FileServiceFile;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import {
Body,
Controller,
HttpStatus,
Inject,
OnModuleInit,
Post,
} from '@nestjs/common';
import { ClientGrpc } from '@nestjs/microservices';
import { lastValueFrom } from 'rxjs';
import { FileBucketProto } from 'juno-proto';
import {
ApiBearerAuth,
ApiOperation,
ApiResponse,
ApiTags,
} from '@nestjs/swagger';
import { RegisterFileBucketModel } from 'src/models/file_bucket.dto';
import { FileBucketResponse } from 'src/models/file_bucket.dto';

const { BUCKET_FILE_SERVICE_NAME } = FileBucketProto;

@ApiBearerAuth('api_key')
@ApiTags('file_bucket')
@Controller('file-bucket')
export class FileBucketController implements OnModuleInit {
private fileBucketService: FileBucketProto.BucketFileServiceClient;

constructor(
@Inject(BUCKET_FILE_SERVICE_NAME)
private fileBucketClient: ClientGrpc,
) {}

onModuleInit() {
this.fileBucketService =
this.fileBucketClient.getService<FileBucketProto.BucketFileServiceClient>(
BUCKET_FILE_SERVICE_NAME,
);
}

@Post('bucket')
@ApiOperation({ summary: 'Registers a File Bucket.' })
@ApiResponse({
status: HttpStatus.BAD_REQUEST,
description: 'Parameters are invalid',
})
@ApiResponse({
status: HttpStatus.OK,
description: 'Returned the file bucket associated with the given data',
type: FileBucketResponse,
})
async registerFileBucket(
@Body() params: RegisterFileBucketModel,
): Promise<FileBucketResponse> {
console.log(`params: ${JSON.stringify(params)}`);

const fileBucket = this.fileBucketService.registerBucket({
name: params.name,
configId: params.configId,
fileProviderName: params.fileProviderName,
FileServiceFile: params.FileServiceFile,
});

console.log(`bucket: ${JSON.stringify(fileBucket)}`);

return new FileBucketResponse(await lastValueFrom(fileBucket));
}
}
58 changes: 58 additions & 0 deletions packages/api-gateway/src/modules/file_bucket/file_bucket.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {
Module,
NestModule,
RequestMethod,
MiddlewareConsumer,
} from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { join } from 'path';
import { FileBucketController } from './file_bucket.controller';
import { ApiKeyMiddleware } from 'src/middleware/api_key.middleware';

import { ClientsModule, Transport } from '@nestjs/microservices';
import {
ApiKeyProtoFile,
FileBucketProto,
FileBucketProtoFile,
} from 'juno-proto';
import { JUNO_FILE_SERVICE_BUCKET_PACKAGE_NAME } from 'juno-proto/dist/gen/file_bucket';
import {
API_KEY_SERVICE_NAME,
JUNO_API_KEY_PACKAGE_NAME,
} from 'juno-proto/dist/gen/api_key';

@Module({
imports: [
ConfigModule.forRoot({
envFilePath: join(__dirname, '../../../../../.env.local'),
}),
ClientsModule.register([
{
name: API_KEY_SERVICE_NAME,
transport: Transport.GRPC,
options: {
url: process.env.AUTH_SERVICE_ADDR,
package: JUNO_API_KEY_PACKAGE_NAME,
protoPath: ApiKeyProtoFile,
},
},
{
name: JUNO_FILE_SERVICE_BUCKET_PACKAGE_NAME,
transport: Transport.GRPC,
options: {
url: process.env.BUCKET_SERVICE_ADDR,
package: FileBucketProto.JUNO_FILE_SERVICE_BUCKET_PACKAGE_NAME,
protoPath: FileBucketProtoFile,
},
},
]),
],
controllers: [FileBucketController],
})
export class FileBucketModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(ApiKeyMiddleware)
.forRoutes({ path: '/bucket/register', method: RequestMethod.POST });
}
}
156 changes: 156 additions & 0 deletions packages/api-gateway/test/file_bucket.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { Test, TestingModule } from '@nestjs/testing';
import {
ClassSerializerInterceptor,
INestApplication,
ValidationPipe,
} from '@nestjs/common';
import { AppModule } from './../src/app.module';
import { Reflector } from '@nestjs/core';
import * as request from 'supertest';
import { FileBucketProto } from 'juno-proto';
import * as GRPC from '@grpc/grpc-js';
import * as ProtoLoader from '@grpc/proto-loader';

let app: INestApplication;
let apiKey: string | undefined = undefined;
const ADMIN_EMAIL = 'test-superadmin@test.com';
const ADMIN_PASSWORD = 'test-password';

async function APIKeyForProjectName(projectName: string): Promise<string> {
const key = await request(app.getHttpServer())
.post('/auth/key')
.set('X-User-Email', ADMIN_EMAIL)
.set('X-User-Password', ADMIN_PASSWORD)
.send({
environment: 'prod',
project: {
name: projectName,
},
});

return key.body['apiKey'];
}

beforeAll(async () => {
const proto = ProtoLoader.loadSync(['reset.proto']) as any;
const protoGRPC = GRPC.loadPackageDefinition(proto) as any;
const resetClient = new protoGRPC.juno.reset_db.DatabaseReset(
process.env.DB_SERVICE_ADDR,
GRPC.credentials.createInsecure(),
);
await new Promise((resolve) => {
resetClient.resetDb({}, () => {
resolve(0);
});
});
});

afterAll((done) => {
app.close();
done();
});

beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();

app = moduleFixture.createNestApplication();
app.useGlobalPipes(
new ValidationPipe({
transform: true,
}),
);
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
await app.init();

if (!apiKey) {
apiKey = await APIKeyForProjectName('test-seed-project');
}
});

describe('File Bucket Routes', () => {
it('Creating a bucket successfully', () => {
const fileBucketBody: FileBucketProto.RegisterBucketRequest = {
name: 'Test Bucket',
configId: 1,
fileProviderName: 'Test Provider',
FileServiceFile: [],
};

return request(app.getHttpServer())
.post('/file/bucket')
.set('Authorization', 'Bearer ' + apiKey)
.send(fileBucketBody)
.expect(201)
.expect((res) => {
expect(res.body).toHaveProperty('name', 'Test Bucket');
expect(res.body).toHaveProperty('configId', 1);
expect(res.body).toHaveProperty('fileProviderName', 'Test Provider');
});
});

it('Unsuccessful creation due to missing bucket name', () => {
const fileBucketBody: FileBucketProto.RegisterBucketRequest = {
name: '',
configId: 1,
fileProviderName: 'Test Provider',
FileServiceFile: [],
};

return request(app.getHttpServer())
.post('/file/bucket')
.set('Authorization', 'Bearer ' + apiKey)
.send(fileBucketBody)
.expect(400);
});

it('Unsuccessful creation due to missing config ID', () => {
const fileBucketBody: FileBucketProto.RegisterBucketRequest = {
name: 'Test Bucket',
configId: undefined,
fileProviderName: 'Test Provider',
FileServiceFile: [],
};

return request(app.getHttpServer())
.post('/file/bucket')
.set('Authorization', 'Bearer ' + apiKey)
.send(fileBucketBody)
.expect(400);
});

it('Unsuccessful creation due to missing file provider name', () => {
const fileBucketBody: FileBucketProto.RegisterBucketRequest = {
name: 'Test Bucket',
configId: 1,
fileProviderName: '',
FileServiceFile: [],
};

return request(app.getHttpServer())
.post('/file/bucket')
.set('Authorization', 'Bearer ' + apiKey)
.send(fileBucketBody)
.expect(400);
});

it('Creating an existing bucket (should fail)', async () => {
const fileBucketBody: FileBucketProto.RegisterBucketRequest = {
name: 'Test Bucket',
configId: 1,
fileProviderName: 'Test Provider',
FileServiceFile: [],
};

await request(app.getHttpServer())
.post('/file/bucket')
.set('Authorization', 'Bearer ' + apiKey)
.send(fileBucketBody);

return request(app.getHttpServer())
.post('/file/bucket')
.set('Authorization', 'Bearer ' + apiKey)
.send(fileBucketBody);
});
});
Loading
Loading