Skip to content

Commit

Permalink
consistency between deployment begin and end projects
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasmachadorj committed Jul 14, 2024
1 parent 616b6ac commit 52f0952
Show file tree
Hide file tree
Showing 20 changed files with 552 additions and 422 deletions.
Original file line number Diff line number Diff line change
@@ -1,18 +1,4 @@
import { PrismaClient } from "@prisma/client";
import { User } from "@dddforum/shared/src/api/users";
import { Post } from "@dddforum/shared/src/api/posts";
import { CreateUserCommand } from "../../modules/users/usersCommand";

export interface UsersPersistence {
save(user: CreateUserCommand): Promise<User & { password: string }>;
findUserByEmail(email: string): Promise<User | null>;
findUserByUsername(username: string): Promise<User | null>;
}

export interface PostsPersistence {
findPosts(sort: string): Promise<Post[]>;
}

export interface Database {
getConnection(): PrismaClient
connect(): Promise<void>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,13 @@
"description": "The backend for dddforum",
"main": "index.js",
"scripts": {
"clean": "rimraf ./dist",
"build": "npm run clean && tsc -b tsconfig.build.json && tsc-alias -p tsconfig.build.json -r ../../tsAliasReplacer.js",
"build": "tsc -b tsconfig.json && npm run generate",
"generate": "ts-node prepareEnv.ts prisma generate --schema=./src/shared/database/prisma/schema.prisma",
"migrate": "ts-node prepareEnv.ts prisma migrate dev --schema=./src/shared/database/prisma/schema.prisma",
"db:seed": "ts-node prepareEnv.ts prisma db seed --schema=./src/shared/database/prisma/schema.prisma",
"db:reset": "ts-node prepareEnv.ts prisma migrate reset --preview-feature --schema src/shared/database/prisma/schema.prisma && npm run migrate && npm run generate",
"start:dev": "ts-node -r tsconfig-paths/register prepareDevCli.ts .env.development && dotenv -e .env.development -- nodemon",
"start:dev:no-watch": "npm run generate && npm run migrate && ts-node prepareEnv.ts ts-node src/index.ts",
"start:ci": "node dist/index.js",
"migrate:deploy": "npx prisma migrate deploy --schema src/shared/database/prisma/schema.prisma",
"start:dev": "npm run migrate && npm run generate && npx nodemon",
"start:dev:no-watch": "npm run migrate && npm run generate && ts-node prepareEnv.ts ts-node src/index.ts",
"lint": "eslint . --ext .ts --fix",
"test": "jest",
"test:dev": "jest --watchAll",
Expand All @@ -21,14 +19,16 @@
"test:infra": "jest -c jest.config.infra.ts",
"test:infra:dev": "jest -c jest.config.infra.ts --watch",
"test:unit": "jest -c jest.config.unit.ts",
"test:unit:dev": "jest -c jest.config.unit.ts --watchAll",
"test:staging": "npm run generate && npm run migrate && npm run test:e2e"
"test:unit:dev": "jest -c jest.config.unit.ts --watchAll"
},
"husky": {
"hooks": {
"pre-commit": "npm run test && npm run prettier-format && npm run lint"
}
},
"prisma": {
"seed": "ts-node ./src/shared/database/prisma/seed.ts"
},
"keywords": [],
"author": "",
"license": "ISC",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { User } from "@dddforum/shared/src/api/users";
import { ValidatedUser } from "@dddforum/shared/src/api/users";
import { Spy } from "../../../shared/testDoubles/spy";
import { UsersRepository } from "../ports/usersRepository";
import { CreateUserCommand } from "../usersCommand";
import { User } from "@prisma/client";

export class InMemoryUserRepositorySpy
extends Spy<UsersRepository>
Expand All @@ -14,11 +15,12 @@ export class InMemoryUserRepositorySpy
this.users = [];
}

save(user: CreateUserCommand): Promise<User & { password: string }> {
save(user: ValidatedUser): Promise<User> {
this.addCall("save", [user]);
const newUser: User = {
...user.props,
const newUser = {
...user,
id: this.users.length > 0 ? this.users[this.users.length - 1].id + 1 : 1,
password: '',
};
this.users.push(newUser);
return Promise.resolve({ ...newUser, password: "password" });
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { PrismaClient } from "@prisma/client";

import { PrismaClient, User } from "@prisma/client";
import { UsersRepository } from "../ports/usersRepository";
import { User } from "@dddforum/shared/src/api/users";
import { CreateUserCommand } from "../usersCommand";
import { generateRandomPassword } from "../../../shared/utils";
import { ValidatedUser } from "@dddforum/shared/src/api/users";

export class ProductionUserRepository implements UsersRepository {
constructor(private prisma: PrismaClient) {}
Expand All @@ -27,7 +27,7 @@ export class ProductionUserRepository implements UsersRepository {
}
}

async save(userData: CreateUserCommand) {
async save(userData: ValidatedUser) {
const { email, firstName, lastName, username } = userData;
return await this.prisma.$transaction(async () => {
const user = await this.prisma.user.create({
Expand Down Expand Up @@ -74,7 +74,7 @@ export class ProductionUserRepository implements UsersRepository {

async update(
id: number,
props: Partial<CreateUserCommand>,
props: Partial<ValidatedUser>,
): Promise<User | null> {
const prismaUser = await this.prisma.user.update({
where: { id },
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@

import { PrismaClient } from "@prisma/client";
import { ProductionUserRepository } from "../adapters/productionUserRepository";
import { UserBuilder } from '@dddforum/shared/tests/support/builders/users'
import { UsersRepository } from "./usersRepository";
import { InMemoryUserRepositorySpy } from "../adapters/inMemoryUserRepositorySpy";

describe("userRepo", () => {
let userRepos: UsersRepository[] = [
new ProductionUserRepository(new PrismaClient()),
new InMemoryUserRepositorySpy()
];

it("can save and retrieve users by email", () => {
let createUserInput = new UserBuilder()
.makeValidatedUserBuilder()
.withAllRandomDetails()
.build()

userRepos.forEach(async (userRepo) => {
let savedUserResult = await userRepo.save({
...createUserInput,
password: '',
});
let fetchedUserResult = await userRepo.findUserByEmail(
createUserInput.email,
);

expect(savedUserResult).toBeDefined();
expect(fetchedUserResult).toBeDefined();
expect(savedUserResult.email).toEqual(fetchedUserResult?.email);
});
});

it("can find a user by username", () => {
let createUserInput = new UserBuilder()
.makeValidatedUserBuilder()
.withAllRandomDetails()
.build();

userRepos.forEach(async (userRepo) => {
let savedUserResult = await userRepo.save({
...createUserInput,
password: "",
});
let fetchedUserResult = await userRepo.findUserByUsername(
createUserInput.username,
);

expect(savedUserResult).toBeDefined();
expect(fetchedUserResult).toBeDefined();
expect(savedUserResult.username).toEqual(fetchedUserResult?.username);
});
});
});
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { User } from "@dddforum/shared/src/api/users";
import { CreateUserCommand } from "../usersCommand";

import { ValidatedUser } from "@dddforum/shared/src/api/users";
import { User } from "@prisma/client";

export interface UsersRepository {
findUserByEmail(email: string): Promise<User | null>;
// @note The ideal return type here is a domain object, not a DTO. For
// demonstration purposes, we've kept it intentionally simple to focus on testing.
// @see Pattern-First for domain objects
save(user: CreateUserCommand): Promise<User & { password: string }>;
save(user: ValidatedUser): Promise<User>;
findById(id: number): Promise<User | null>;
delete(email: string): Promise<void>;
findUserByUsername(username: string): Promise<User | null>;
update(id: number, props: Partial<CreateUserCommand>): Promise<User | null>;
update(id: number, props: Partial<ValidatedUser>): Promise<User | null>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@ import {
UserNotFoundException,
UsernameAlreadyTakenException,
} from "./usersExceptions";
import { User } from "@dddforum/shared/src/api/users";
import { ValidatedUser } from "@dddforum/shared/src/api/users";
import { TransactionalEmailAPI } from "../notifications/ports/transactionalEmailAPI";
import { UsersRepository } from "./ports/usersRepository";
import { TextUtil } from "@dddforum/shared/src/utils/textUtil";


export class UsersService {
constructor(
private repository: UsersRepository,
private emailAPI: TransactionalEmailAPI,
) {}

async createUser(userData: CreateUserCommand): Promise<User> {
async createUser(userData: CreateUserCommand) {
const existingUserByEmail = await this.repository.findUserByEmail(
userData.email,
);
Expand All @@ -28,25 +30,31 @@ export class UsersService {
if (existingUserByUsername) {
throw new UsernameAlreadyTakenException(userData.username);
}
const { password, ...user } = await this.repository.save(userData);

const validatedUser: ValidatedUser = {
...userData.props,
password: TextUtil.createRandomText(10)
}

const prismaUser = await this.repository.save(validatedUser);

await this.emailAPI.sendMail({
to: user.email,
to: validatedUser.email,
subject: "Your login details to DDDForum",
text: `Welcome to DDDForum. You can login with the following details </br>
email: ${user.email}
password: ${password}`,
email: ${validatedUser.email}
password: ${validatedUser.password}`,
});

return user;
return prismaUser;
}

async getUserByEmail(email: string) {
const user = await this.repository.findUserByEmail(email);
if (!user) {
const prismaUser = await this.repository.findUserByEmail(email);
if (!prismaUser) {
throw new UserNotFoundException(email);
}
return user;
return prismaUser;
}

async deleteUser(email: string) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { PrismaClient } from "@prisma/client";

export interface Database {
getConnection(): PrismaClient
connect(): Promise<void>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
-- CreateTable
CREATE TABLE "User" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"email" TEXT NOT NULL,
"firstName" TEXT NOT NULL,
"lastName" TEXT NOT NULL,
"username" TEXT NOT NULL,
"password" TEXT NOT NULL
);

-- CreateTable
CREATE TABLE "Member" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"userId" INTEGER NOT NULL,
CONSTRAINT "Member_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);

-- CreateTable
CREATE TABLE "Post" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"memberId" INTEGER NOT NULL,
"postType" TEXT NOT NULL,
"title" TEXT NOT NULL,
"content" TEXT NOT NULL,
"dateCreated" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Post_memberId_fkey" FOREIGN KEY ("memberId") REFERENCES "Member" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);

-- CreateTable
CREATE TABLE "Comment" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"postId" INTEGER NOT NULL,
"text" TEXT NOT NULL,
"memberId" INTEGER NOT NULL,
"parentCommentId" INTEGER,
CONSTRAINT "Comment_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "Comment_memberId_fkey" FOREIGN KEY ("memberId") REFERENCES "Member" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "Comment_parentCommentId_fkey" FOREIGN KEY ("parentCommentId") REFERENCES "Comment" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);

-- CreateTable
CREATE TABLE "Vote" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"postId" INTEGER NOT NULL,
"memberId" INTEGER NOT NULL,
"voteType" TEXT NOT NULL,
CONSTRAINT "Vote_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "Vote_memberId_fkey" FOREIGN KEY ("memberId") REFERENCES "Member" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);

-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");

-- CreateIndex
CREATE UNIQUE INDEX "Member_userId_key" ON "Member"("userId");
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"
provider = "sqlite"
Loading

0 comments on commit 52f0952

Please sign in to comment.