Restructure codebase to use src directory

This commit is contained in:
Aaron William Po
2023-04-11 23:32:06 -04:00
parent 90f2cc2c0c
commit 08422fe24e
141 changed files with 6 additions and 4 deletions

View File

@@ -0,0 +1,33 @@
import DBClient from '@/prisma/DBClient';
import { z } from 'zod';
import BeerCommentValidationSchema from './schema/CreateBeerCommentValidationSchema';
const CreateNewBeerCommentServiceSchema = BeerCommentValidationSchema.extend({
userId: z.string().uuid(),
beerPostId: z.string().uuid(),
});
const createNewBeerComment = async ({
content,
rating,
beerPostId,
userId,
}: z.infer<typeof CreateNewBeerCommentServiceSchema>) => {
return DBClient.instance.beerComment.create({
data: {
content,
rating,
beerPost: { connect: { id: beerPostId } },
postedBy: { connect: { id: userId } },
},
select: {
id: true,
content: true,
rating: true,
postedBy: { select: { id: true, username: true } },
createdAt: true,
},
});
};
export default createNewBeerComment;

View File

@@ -0,0 +1,28 @@
import DBClient from '@/prisma/DBClient';
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
import { z } from 'zod';
import BeerCommentQueryResult from './schema/BeerCommentQueryResult';
const getAllBeerComments = async (
{ id }: Pick<z.infer<typeof beerPostQueryResult>, 'id'>,
{ pageSize, pageNum = 0 }: { pageSize: number; pageNum?: number },
) => {
const skip = (pageNum - 1) * pageSize;
const beerComments: z.infer<typeof BeerCommentQueryResult>[] =
await DBClient.instance.beerComment.findMany({
skip,
take: pageSize,
where: { beerPostId: id },
select: {
id: true,
content: true,
rating: true,
createdAt: true,
postedBy: { select: { id: true, username: true, createdAt: true } },
},
orderBy: { createdAt: 'desc' },
});
return beerComments;
};
export default getAllBeerComments;

View File

@@ -0,0 +1,11 @@
import DBClient from '@/prisma/DBClient';
const getBeerCommentCount = async (beerPostId: string) => {
const count = await DBClient.instance.beerComment.count({
where: { beerPostId },
});
return count;
};
export default getBeerCommentCount;

View File

@@ -0,0 +1,14 @@
import { z } from 'zod';
const BeerCommentQueryResult = z.object({
id: z.string().uuid(),
content: z.string().min(1).max(500),
rating: z.number().int().min(1).max(5),
createdAt: z.coerce.date(),
postedBy: z.object({
id: z.string().uuid(),
username: z.string().min(1).max(50),
}),
});
export default BeerCommentQueryResult;

View File

@@ -0,0 +1,15 @@
import { z } from 'zod';
const BeerCommentValidationSchema = z.object({
content: z
.string()
.min(1, { message: 'Comment must not be empty.' })
.max(300, { message: 'Comment must be less than 300 characters.' }),
rating: z
.number()
.int()
.min(1, { message: 'Rating must be greater than 1.' })
.max(5, { message: 'Rating must be less than 5.' }),
});
export default BeerCommentValidationSchema;

View File

@@ -0,0 +1,46 @@
import DBClient from '@/prisma/DBClient';
import { z } from 'zod';
import beerPostQueryResult from './schema/BeerPostQueryResult';
import CreateBeerPostValidationSchema from './schema/CreateBeerPostValidationSchema';
const CreateBeerPostWithUserSchema = CreateBeerPostValidationSchema.extend({
userId: z.string().uuid(),
});
const createNewBeerPost = async ({
name,
description,
abv,
ibu,
typeId,
breweryId,
userId,
}: z.infer<typeof CreateBeerPostWithUserSchema>) => {
const newBeerPost: z.infer<typeof beerPostQueryResult> =
await DBClient.instance.beerPost.create({
data: {
name,
description,
abv,
ibu,
type: { connect: { id: typeId } },
postedBy: { connect: { id: userId } },
brewery: { connect: { id: breweryId } },
},
select: {
id: true,
name: true,
description: true,
abv: true,
ibu: true,
createdAt: true,
beerImages: { select: { id: true, path: true, caption: true, alt: true } },
brewery: { select: { id: true, name: true } },
type: { select: { id: true, name: true } },
postedBy: { select: { id: true, username: true } },
},
});
return newBeerPost;
};
export default createNewBeerPost;

View File

@@ -0,0 +1,14 @@
import DBClient from '@/prisma/DBClient';
import { z } from 'zod';
import EditBeerPostValidationSchema from './schema/EditBeerPostValidationSchema';
const schema = EditBeerPostValidationSchema.omit({ id: true });
export default async function editBeerPostById(id: string, data: z.infer<typeof schema>) {
const beerPost = await DBClient.instance.beerPost.update({
where: { id },
data,
});
return beerPost;
}

View File

@@ -0,0 +1,32 @@
import DBClient from '@/prisma/DBClient';
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
import { z } from 'zod';
const prisma = DBClient.instance;
const getAllBeerPosts = async (pageNum: number, pageSize: number) => {
const skip = (pageNum - 1) * pageSize;
const beerPosts: z.infer<typeof beerPostQueryResult>[] = await prisma.beerPost.findMany(
{
select: {
id: true,
name: true,
ibu: true,
abv: true,
description: true,
createdAt: true,
type: { select: { name: true, id: true } },
brewery: { select: { name: true, id: true } },
postedBy: { select: { id: true, username: true } },
beerImages: { select: { path: true, caption: true, id: true, alt: true } },
},
take: pageSize,
skip,
},
);
return beerPosts;
};
export default getAllBeerPosts;

View File

@@ -0,0 +1,28 @@
import DBClient from '@/prisma/DBClient';
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
import { z } from 'zod';
const prisma = DBClient.instance;
const getBeerPostById = async (id: string) => {
const beerPost: z.infer<typeof beerPostQueryResult> | null =
await prisma.beerPost.findFirst({
select: {
id: true,
name: true,
ibu: true,
abv: true,
createdAt: true,
description: true,
postedBy: { select: { username: true, id: true } },
brewery: { select: { name: true, id: true } },
type: { select: { name: true, id: true } },
beerImages: { select: { alt: true, path: true, caption: true, id: true } },
},
where: { id },
});
return beerPost;
};
export default getBeerPostById;

View File

@@ -0,0 +1,22 @@
import DBClient from '@/prisma/DBClient';
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
import { z } from 'zod';
const getBeerRecommendations = async (
beerPost: Pick<z.infer<typeof beerPostQueryResult>, 'type' | 'brewery' | 'id'>,
) => {
const beerRecommendations = await DBClient.instance.beerPost.findMany({
where: {
OR: [{ typeId: beerPost.type.id }, { breweryId: beerPost.brewery.id }],
NOT: { id: beerPost.id },
},
include: {
beerImages: { select: { id: true, path: true, caption: true, alt: true } },
brewery: { select: { id: true, name: true } },
},
});
return beerRecommendations;
};
export default getBeerRecommendations;

View File

@@ -0,0 +1,18 @@
import { z } from 'zod';
const beerPostQueryResult = z.object({
id: z.string(),
name: z.string(),
brewery: z.object({ id: z.string(), name: z.string() }),
description: z.string(),
beerImages: z.array(
z.object({ path: z.string(), caption: z.string(), id: z.string(), alt: z.string() }),
),
ibu: z.number(),
abv: z.number(),
type: z.object({ id: z.string(), name: z.string() }),
postedBy: z.object({ id: z.string(), username: z.string() }),
createdAt: z.coerce.date(),
});
export default beerPostQueryResult;

View File

@@ -0,0 +1,15 @@
import { BeerPost } from '@prisma/client';
type BeerRecommendationQueryResult = BeerPost & {
brewery: {
id: string;
name: string;
};
beerImages: {
id: string;
alt: string;
url: string;
}[];
};
export default BeerRecommendationQueryResult;

View File

@@ -0,0 +1,43 @@
import { z } from 'zod';
const CreateBeerPostValidationSchema = z.object({
name: z
.string({
required_error: 'Beer name is required.',
invalid_type_error: 'Beer name must be a string.',
})
.min(1, { message: 'Beer name is required.' })
.max(100, { message: 'Beer name is too long.' }),
description: z
.string()
.min(1, { message: 'Description is required.' })
.max(500, { message: 'Description is too long.' }),
abv: z
.number({
required_error: 'ABV is required.',
invalid_type_error: 'ABV must be a number.',
})
.min(0.1, { message: 'ABV must be greater than 0.1.' })
.max(50, { message: 'ABV must be less than 50.' }),
ibu: z
.number({
required_error: 'IBU is required.',
invalid_type_error: 'IBU must be a number.',
})
.min(2, { message: 'IBU must be greater than 2.' })
.max(100, { message: 'IBU must be less than 100.' }),
typeId: z
.string({
required_error: 'Type id is required.',
invalid_type_error: 'Type id must be a string.',
})
.uuid({ message: 'Invalid type id.' }),
breweryId: z
.string({
required_error: 'Brewery id is required.',
invalid_type_error: 'Brewery id must be a string.',
})
.uuid({ message: 'Invalid brewery id.' }),
});
export default CreateBeerPostValidationSchema;

View File

@@ -0,0 +1,9 @@
import { z } from 'zod';
import CreateBeerPostValidationSchema from './CreateBeerPostValidationSchema';
const EditBeerPostValidationSchema = CreateBeerPostValidationSchema.omit({
breweryId: true,
typeId: true,
}).extend({ id: z.string().uuid() });
export default EditBeerPostValidationSchema;

View File

@@ -0,0 +1,20 @@
import DBClient from '@/prisma/DBClient';
import { z } from 'zod';
import GetUserSchema from '../User/schema/GetUserSchema';
const createBeerPostLike = async ({
id,
user,
}: {
id: string;
user: z.infer<typeof GetUserSchema>;
}) => {
return DBClient.instance.beerPostLike.create({
data: {
beerPost: { connect: { id } },
likedBy: { connect: { id: user.id } },
},
});
};
export default createBeerPostLike;

View File

@@ -0,0 +1,6 @@
import DBClient from '@/prisma/DBClient';
const findBeerPostLikeById = async (beerPostId: string, likedById: string) =>
DBClient.instance.beerPostLike.findFirst({ where: { beerPostId, likedById } });
export default findBeerPostLikeById;

View File

@@ -0,0 +1,11 @@
import DBClient from '@/prisma/DBClient';
const getBeerPostLikeCount = async (beerPostId: string) => {
const count = await DBClient.instance.beerPostLike.count({
where: { beerPostId },
});
return count;
};
export default getBeerPostLikeCount;

View File

@@ -0,0 +1,11 @@
import DBClient from '@/prisma/DBClient';
const removeBeerPostLikeById = async (id: string) => {
return DBClient.instance.beerPostLike.delete({
where: {
id,
},
});
};
export default removeBeerPostLikeById;

View File

@@ -0,0 +1,22 @@
import DBClient from '@/prisma/DBClient';
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
import { z } from 'zod';
const prisma = DBClient.instance;
const getAllBreweryPosts = async () => {
const breweryPosts: z.infer<typeof BreweryPostQueryResult>[] =
await prisma.breweryPost.findMany({
select: {
id: true,
location: true,
name: true,
postedBy: { select: { username: true, id: true } },
breweryImages: { select: { path: true, caption: true, id: true, alt: true } },
},
});
return breweryPosts;
};
export default getAllBreweryPosts;

View File

@@ -0,0 +1,23 @@
import DBClient from '@/prisma/DBClient';
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
import { z } from 'zod';
const prisma = DBClient.instance;
const getBreweryPostById = async (id: string) => {
const breweryPost: z.infer<typeof BreweryPostQueryResult> | null =
await prisma.breweryPost.findFirst({
select: {
id: true,
location: true,
name: true,
breweryImages: { select: { path: true, caption: true, id: true, alt: true } },
postedBy: { select: { username: true, id: true } },
},
where: { id },
});
return breweryPost;
};
export default getBreweryPostById;

View File

@@ -0,0 +1,13 @@
import { z } from 'zod';
const BreweryPostQueryResult = z.object({
id: z.string(),
location: z.string(),
name: z.string(),
postedBy: z.object({ id: z.string(), username: z.string() }),
breweryImages: z.array(
z.object({ path: z.string(), caption: z.string(), id: z.string(), alt: z.string() }),
),
});
export default BreweryPostQueryResult;

View File

@@ -0,0 +1,40 @@
import { hashPassword } from '@/config/auth/passwordFns';
import DBClient from '@/prisma/DBClient';
import { z } from 'zod';
import CreateUserValidationSchema from './schema/CreateUserValidationSchema';
import GetUserSchema from './schema/GetUserSchema';
const createNewUser = async ({
email,
password,
firstName,
lastName,
dateOfBirth,
username,
}: z.infer<typeof CreateUserValidationSchema>) => {
const hash = await hashPassword(password);
const user: z.infer<typeof GetUserSchema> = await DBClient.instance.user.create({
data: {
username,
email,
hash,
firstName,
lastName,
dateOfBirth: new Date(dateOfBirth),
},
select: {
id: true,
username: true,
email: true,
firstName: true,
lastName: true,
dateOfBirth: true,
createdAt: true,
isAccountVerified: true,
},
});
return user;
};
export default createNewUser;

View File

@@ -0,0 +1,13 @@
import DBClient from '@/prisma/DBClient';
const findUserByEmail = async (email: string) =>
DBClient.instance.user.findFirst({
where: { email },
select: {
id: true,
username: true,
hash: true,
},
});
export default findUserByEmail;

View File

@@ -0,0 +1,24 @@
import DBClient from '@/prisma/DBClient';
import { z } from 'zod';
import GetUserSchema from './schema/GetUserSchema';
const findUserById = async (id: string) => {
const user: z.infer<typeof GetUserSchema> | null =
await DBClient.instance.user.findUnique({
where: { id },
select: {
id: true,
username: true,
email: true,
firstName: true,
lastName: true,
dateOfBirth: true,
createdAt: true,
isAccountVerified: true,
},
});
return user;
};
export default findUserById;

View File

@@ -0,0 +1,13 @@
import DBClient from '@/prisma/DBClient';
const findUserByUsername = async (username: string) =>
DBClient.instance.user.findFirst({
where: { username },
select: {
id: true,
username: true,
hash: true,
},
});
export default findUserByUsername;

View File

@@ -0,0 +1,53 @@
import sub from 'date-fns/sub';
import { z } from 'zod';
const minimumDateOfBirth = sub(new Date(), { years: 19 });
const CreateUserValidationSchema = z
.object({
email: z.string().email({ message: 'Email must be a valid email address.' }),
// use special characters, numbers, and uppercase letters
password: z
.string()
.min(8, { message: 'Password must be at least 8 characters.' })
.refine((password) => /[A-Z]/.test(password), {
message: 'Password must contain at least one uppercase letter.',
})
.refine((password) => /[0-9]/.test(password), {
message: 'Password must contain at least one number.',
})
.refine((password) => /[^a-zA-Z0-9]/.test(password), {
message: 'Password must contain at least one special character.',
}),
confirmPassword: z.string(),
firstName: z
.string()
.min(1, { message: 'First name must not be empty.' })
.max(20, { message: 'First name must be less than 20 characters.' })
.refine((firstName) => /^[a-zA-Z]+$/.test(firstName), {
message: 'First name must only contain letters.',
}),
lastName: z
.string()
.min(1, { message: 'Last name must not be empty.' })
.max(20, { message: 'Last name must be less than 20 characters.' })
.refine((lastName) => /^[a-zA-Z]+$/.test(lastName), {
message: 'Last name must only contain letters.',
}),
username: z
.string()
.min(1, { message: 'Username must not be empty.' })
.max(20, { message: 'Username must be less than 20 characters.' }),
dateOfBirth: z.string().refine(
(dateOfBirth) => {
const parsedDateOfBirth = new Date(dateOfBirth);
return parsedDateOfBirth <= minimumDateOfBirth;
},
{ message: 'You must be at least 19 years old to register.' },
),
})
.refine((data) => data.password === data.confirmPassword, {
message: 'Passwords do not match.',
path: ['confirmPassword'],
});
export default CreateUserValidationSchema;

View File

@@ -0,0 +1,15 @@
import { z } from 'zod';
const GetUserSchema = z.object({
id: z.string().uuid(),
username: z.string(),
createdAt: z.coerce.date(),
updatedAt: z.coerce.date().optional(),
email: z.string().email(),
firstName: z.string(),
lastName: z.string(),
dateOfBirth: z.coerce.date(),
isAccountVerified: z.boolean(),
});
export default GetUserSchema;

View File

@@ -0,0 +1,18 @@
import { z } from 'zod';
const LoginValidationSchema = z.object({
username: z
.string({
required_error: 'Username is required.',
invalid_type_error: 'Username must be a string.',
})
.min(1, { message: 'Username is required.' }),
password: z
.string({
required_error: 'Password is required.',
invalid_type_error: 'Password must be a string.',
})
.min(1, { message: 'Password is required.' }),
});
export default LoginValidationSchema;

View File

@@ -0,0 +1,26 @@
import { generateConfirmationToken } from '@/config/jwt';
import sendEmail from '@/config/sparkpost/sendEmail';
import Welcome from '@/emails/Welcome';
import { render } from '@react-email/render';
import { z } from 'zod';
import { BASE_URL } from '@/config/env';
import GetUserSchema from './schema/GetUserSchema';
type UserSchema = z.infer<typeof GetUserSchema>;
const sendConfirmationEmail = async ({ id, username, email }: UserSchema) => {
const confirmationToken = generateConfirmationToken({ id, username });
const subject = 'Confirm your email';
const name = username;
const url = `${BASE_URL}/api/users/confirm?token=${confirmationToken}`;
const address = email;
const html = render(Welcome({ name, url, subject })!);
const text = render(Welcome({ name, url, subject })!, { plainText: true });
await sendEmail({ address, subject, text, html });
};
export default sendConfirmationEmail;

View File

@@ -0,0 +1,24 @@
import GetUserSchema from '@/services/User/schema/GetUserSchema';
import DBClient from '@/prisma/DBClient';
import { z } from 'zod';
const updateUserToBeConfirmedById = async (id: string) => {
const user: z.infer<typeof GetUserSchema> = await DBClient.instance.user.update({
where: { id },
data: { isAccountVerified: true, updatedAt: new Date() },
select: {
id: true,
username: true,
email: true,
isAccountVerified: true,
createdAt: true,
firstName: true,
lastName: true,
dateOfBirth: true,
},
});
return user;
};
export default updateUserToBeConfirmedById;