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

feat(server): create endpoint to store email #25

Merged
merged 23 commits into from
Mar 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion .github/workflows/release-apps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:

- run: pnpm install --frozen-lockfile

# - run: npm install semver
# - run: npm install semver

- uses: aws-actions/configure-aws-credentials@50ac8dd1e1b10d09dac7b8727528b91bed831ac0 # v3.0.2
with:
Expand Down
12 changes: 8 additions & 4 deletions apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,20 @@
"migration:dev": "npx prisma migrate dev",
"playground:start": "docker-compose rm -fsv && docker-compose -f ./docker-compose.yml --env-file ./.env up",
"prepare": "npx prisma generate",
"test": "jest",
"test": "jest --maxWorkers=4 --coverage --forceExit --passWithNoTests",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
"test:e2e": "jest --selectProjects test:e2e",
"test:i9n": "jest --selectProjects test:i9n",
"test:unit": "jest --selectProjects test:unit"
},
"prettier": "@waveshq/standard-prettier",
"dependencies": {
"@prisma/client": "^5.6.0",
"@stickyjs/testcontainers": "^1.3.10",
"@waveshq/standard-api-fastify": "^3.0.1",
"class-validator": "^0.14.1",
"joi": "^17.12.2",
"light-my-request": "^5.12.0",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.2.0"
},
Expand Down
20 changes: 20 additions & 0 deletions apps/server/src/AppConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as Joi from "joi";

export const DATABASE_URL = "DATABASE_URL";

export function appConfig() {
return {
dbUrl: process.env.DATABASE_URL,
};
}

export type DeepPartial<T> = T extends object
? {
[P in keyof T]?: DeepPartial<T[P]>;
}
: T;
export type AppConfig = DeepPartial<ReturnType<typeof appConfig>>;

export const ENV_VALIDATION_SCHEMA = Joi.object({
DATABASE_URL: Joi.string(),
});
16 changes: 11 additions & 5 deletions apps/server/src/AppModule.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { Module } from "@nestjs/common";
import { AppController } from "./AppController";
import { AppService } from "./AppService";
import { appConfig, ENV_VALIDATION_SCHEMA } from "./AppConfig";
import { UserModule } from "./user/UserModule";
import { ConfigModule } from "@nestjs/config";

@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [appConfig],
validationSchema: ENV_VALIDATION_SCHEMA,
}),
UserModule,
],
})
export class AppModule {}
21 changes: 17 additions & 4 deletions apps/server/src/MarbleFiLsdServerApp.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { Request, Response, NextFunction } from "express";
import { INestApplication } from "@nestjs/common";
import { NextFunction, Request, Response } from "express";
import { INestApplication, NestApplicationOptions } from "@nestjs/common";
import { NestFactory } from "@nestjs/core";
import { NestFastifyApplication } from "@nestjs/platform-fastify";
import {
FastifyAdapter,
NestFastifyApplication,
} from "@nestjs/platform-fastify";

import { AppModule } from "./AppModule";

Expand All @@ -15,6 +18,16 @@ export class MarbleFiLsdServerApp<

constructor(protected readonly module: any) {}

get nestApplicationOptions(): NestApplicationOptions {
return {
bufferLogs: true,
};
}

get fastifyAdapter(): FastifyAdapter {
return new FastifyAdapter();
}

async createNestApp(): Promise<App> {
const app = await NestFactory.create(AppModule);
await this.configureApp(app);
Expand Down Expand Up @@ -53,7 +66,7 @@ export class MarbleFiLsdServerApp<
async start(): Promise<App> {
const app = await this.init();

const PORT = process.env.PORT || 3001;
const PORT = process.env.PORT || 5741;
await app.listen(PORT).then(() => {
// eslint-disable-next-line no-console
console.log(`Started server on port ${PORT}`);
Expand Down
32 changes: 32 additions & 0 deletions apps/server/src/modules/BaseModule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { DynamicModule, Global, Module, ModuleMetadata } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { LoggerModule } from "nestjs-pino";

/**
* Baseline module for any Bridge nest applications.
*
* - `@nestjs/config`, nestjs ConfigModule
* - `nestjs-pino`, the Pino logger for NestJS
* - `joi`, for validation of environment variables
*/
@Global()
@Module({
imports: [
LoggerModule.forRoot({
exclude: ["/health", "/version", "/settings"],
}),
ConfigModule.forRoot({
isGlobal: true,
cache: true,
}),
],
})
export class BaseModule {
static with(metadata: ModuleMetadata): DynamicModule {
return {
module: BaseModule,
global: true,
...metadata,
};
}
}
24 changes: 24 additions & 0 deletions apps/server/src/pipes/MultiEnumValidationPipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { BadRequestException, Injectable, PipeTransform } from "@nestjs/common";

@Injectable()
export class MultiEnumValidationPipe<T extends Record<string, string>>
implements PipeTransform
{
constructor(private enumType: T) {}

transform(value: any): any | undefined {
if (!value) return undefined;
const doesStatusExist = this.enumType[value];
if (!doesStatusExist) {
throw new BadRequestException(
`Invalid query parameter value. See the acceptable values: ${Object.keys(
this.enumType,
)
.map((key) => this.enumType[key])
.join(", ")}`,
);
}

return value;
}
}
75 changes: 75 additions & 0 deletions apps/server/src/user/UserController.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { HttpStatus } from "@nestjs/common";
import {
PostgreSqlContainer,
StartedPostgreSqlContainer,
} from "@stickyjs/testcontainers";
import { UserController } from "./UserController";
import { UserService } from "./UserService";
import { PrismaService } from "../PrismaService";

import { buildTestConfig, TestingModule } from "../../test/TestingModule";
import { MarbleFiLsdServerTestingApp } from "../../test/MarbleFiLsdServerTestingApp";

describe("UserController", () => {
let testing: MarbleFiLsdServerTestingApp;
let userController: UserController;
let userService: UserService;
let prismaService: PrismaService;
let startedPostgresContainer: StartedPostgreSqlContainer;

beforeAll(async () => {
startedPostgresContainer = await new PostgreSqlContainer().start();
testing = new MarbleFiLsdServerTestingApp(
TestingModule.register(buildTestConfig({ startedPostgresContainer })),
);
const app = await testing.start();

// init postgres database
prismaService = app.get<PrismaService>(PrismaService);
userService = new UserService(prismaService);
userController = new UserController(userService);
});

describe("create user", () => {
it("should create an active user in db", async () => {
const res = await userController.create("[email protected]", "ACTIVE");

expect(res).toEqual({
id: 1,
email: "[email protected]",
status: "ACTIVE",
});
});

it("should create an inactive user in db", async () => {
const res = await userController.create("[email protected]", "INACTIVE");

expect(res).toEqual({
id: 2,
email: "[email protected]",
status: "INACTIVE",
});
});

it("should create an active user by default", async () => {
const res = await userController.create("[email protected]");

expect(res).toEqual({
id: 3,
email: "[email protected]",
status: "ACTIVE",
});
});

it("should not create a user with same email", async () => {
try {
await userController.create("[email protected]");
} catch (e) {
expect(e.response.statusCode).toStrictEqual(HttpStatus.BAD_REQUEST);
expect(e.response.message).toStrictEqual(
`Duplicate email '[email protected]' found in database`,
);
}
});
});
});
18 changes: 18 additions & 0 deletions apps/server/src/user/UserController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Body, Controller, Post } from "@nestjs/common";
import { SubscriptionStatus } from "@prisma/client";
import { UserService } from "./UserService";
import { MultiEnumValidationPipe } from "../pipes/MultiEnumValidationPipe";

@Controller("user")
export class UserController {
constructor(private readonly userService: UserService) {}

@Post()
async create(
@Body("email") email: string,
@Body("status", new MultiEnumValidationPipe(SubscriptionStatus))
status?: SubscriptionStatus,
) {
return this.userService.createUser({ email, status });
}
}
11 changes: 11 additions & 0 deletions apps/server/src/user/UserModule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Module } from "@nestjs/common";
import { UserController } from "./UserController";
import { UserService } from "./UserService";
import { PrismaService } from "../PrismaService";

@Module({
controllers: [UserController],
providers: [UserService, PrismaService],
exports: [UserService],
})
export class UserModule {}
45 changes: 45 additions & 0 deletions apps/server/src/user/UserService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {
Injectable,
BadRequestException,
HttpException,
HttpStatus,
} from "@nestjs/common";
import { PrismaService } from "../PrismaService";
import { User } from "@prisma/client";
import { createUserParams } from "./model/User";
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";

@Injectable()
export class UserService {
constructor(private readonly prismaService: PrismaService) {}

async createUser({ email, status }: createUserParams): Promise<User> {
try {
return await this.prismaService.user.create({
data: {
email: email,
status: status,
},
});
} catch (e) {
if (e instanceof PrismaClientKnownRequestError) {
throw new BadRequestException(
`Duplicate email '${email}' found in database`,
);
} else {
throw new HttpException(
{
statusCode:
e.status ?? (e.code || HttpStatus.INTERNAL_SERVER_ERROR),
error: e.response?.error || "Internal server error",
message: `API call for createUser was unsuccessful: ${e.message}`,
},
e.status ?? HttpStatus.INTERNAL_SERVER_ERROR,
{
cause: e,
},
);
}
}
}
}
6 changes: 6 additions & 0 deletions apps/server/src/user/model/User.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { SubscriptionStatus } from "@prisma/client";

export type createUserParams = {
email: string;
status?: SubscriptionStatus;
};
52 changes: 52 additions & 0 deletions apps/server/test/MarbleFiLsdServerTestingApp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { NestFastifyApplication } from "@nestjs/platform-fastify";
import { Test, TestingModule } from "@nestjs/testing";
import {
Chain as LightMyRequestChain,
InjectOptions,
Response as LightMyRequestResponse,
} from "light-my-request";

import { MarbleFiLsdServerApp } from "../src/MarbleFiLsdServerApp";
import { BaseModule } from "../src/modules/BaseModule";

/**
* Testing app used for testing MarbleFi Server App behaviour through integration tests
*/
export class MarbleFiLsdServerTestingApp extends MarbleFiLsdServerApp<NestFastifyApplication> {
async createTestingModule(): Promise<TestingModule> {
return Test.createTestingModule({
imports: [
BaseModule.with({
imports: [this.module],
}),
],
}).compile();
}

override async createNestApp(): Promise<NestFastifyApplication> {
const module = await this.createTestingModule();
return module.createNestApplication<NestFastifyApplication>(
this.fastifyAdapter,
this.nestApplicationOptions,
);
}

async start(): Promise<NestFastifyApplication> {
return this.init();
}

/**
* A wrapper function around native `fastify.inject()` method.
* @returns {void}
*/
inject(): LightMyRequestChain;
inject(opts: InjectOptions | string): Promise<LightMyRequestResponse>;
inject(
opts?: InjectOptions | string,
): LightMyRequestChain | Promise<LightMyRequestResponse> {
if (opts === undefined) {
return this.app!.inject();
}
return this.app!.inject(opts);
}
}
Loading
Loading