Create location table for brewery post locations

This commit is contained in:
Aaron William Po
2023-04-26 08:37:59 -04:00
parent 4aeafc0de8
commit c19cddceb7
17 changed files with 209 additions and 55 deletions

View File

@@ -8,7 +8,7 @@
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
"format": "npx prettier . --write", "format": "npx prettier . --write",
"seed": "npx ts-node ./src/prisma/seed/index.ts" "seed": "npx --max-old-space-size=4096 ts-node ./src/prisma/seed/index.ts"
}, },
"dependencies": { "dependencies": {
"@hapi/iron": "^7.0.1", "@hapi/iron": "^7.0.1",

View File

@@ -44,7 +44,9 @@ const BeerCard: FC<{ post: z.infer<typeof beerPostQueryResult> }> = ({ post }) =
<span className="text-sm lg:text-lg">{post.abv}% ABV</span> <span className="text-sm lg:text-lg">{post.abv}% ABV</span>
<span className="text-sm lg:text-lg">{post.ibu} IBU</span> <span className="text-sm lg:text-lg">{post.ibu} IBU</span>
</div> </div>
<span>liked by {likeCount} users</span> <span>
liked by {likeCount} user{likeCount === 1 ? '' : 's'}
</span>
</div> </div>
<div> <div>
{user && <BeerPostLikeButton beerPostId={post.id} mutateCount={mutate} />} {user && <BeerPostLikeButton beerPostId={post.id} mutateCount={mutate} />}

View File

@@ -32,7 +32,8 @@ const BreweryCard: FC<{ brewery: z.infer<typeof BreweryPostQueryResult> }> = ({
</Link> </Link>
</h2> </h2>
<h3 className="text-xl font-normal lg:text-2xl"> <h3 className="text-xl font-normal lg:text-2xl">
located in {brewery.city}, {brewery.stateOrProvince || brewery.country} located in {brewery.location.city},{' '}
{brewery.location.stateOrProvince || brewery.location.country}
</h3> </h3>
<h4 className="text-lg lg:text-xl"> <h4 className="text-lg lg:text-xl">
est. {brewery.dateEstablished.getFullYear()} est. {brewery.dateEstablished.getFullYear()}

View File

@@ -54,6 +54,8 @@ const useBeerPostComments = ({ id, pageSize }: UseBeerPostCommentsProps) => {
const isAtEnd = !(size < data?.[0].pageCount!); const isAtEnd = !(size < data?.[0].pageCount!);
console.log(comments);
return { return {
comments, comments,
isLoading, isLoading,

View File

@@ -43,8 +43,8 @@ const BreweryInfoHeader: FC<BreweryInfoHeaderProps> = ({ breweryPost }) => {
<h1 className="text-2xl font-bold lg:text-4xl">{breweryPost.name}</h1> <h1 className="text-2xl font-bold lg:text-4xl">{breweryPost.name}</h1>
<h2 className="text-lg font-semibold lg:text-2xl"> <h2 className="text-lg font-semibold lg:text-2xl">
Located in Located in
{` ${breweryPost.city}, ${ {` ${breweryPost.location.city}, ${
breweryPost.stateOrProvince || breweryPost.country breweryPost.location.stateOrProvince || breweryPost.location.country
}`} }`}
</h2> </h2>
</div> </div>
@@ -130,7 +130,7 @@ const BreweryMap: FC<BreweryMapProps> = ({ latitude, longitude }) => {
}; };
const BreweryByIdPage: NextPage<BreweryPageProps> = ({ breweryPost }) => { const BreweryByIdPage: NextPage<BreweryPageProps> = ({ breweryPost }) => {
const [longitude, latitude] = breweryPost.coordinates; const [longitude, latitude] = breweryPost.location.coordinates;
return ( return (
<> <>
<Head> <Head>

View File

@@ -0,0 +1,41 @@
/*
Warnings:
- You are about to drop the column `address` on the `BreweryPost` table. All the data in the column will be lost.
- You are about to drop the column `city` on the `BreweryPost` table. All the data in the column will be lost.
- You are about to drop the column `coordinates` on the `BreweryPost` table. All the data in the column will be lost.
- You are about to drop the column `country` on the `BreweryPost` table. All the data in the column will be lost.
- You are about to drop the column `stateOrProvince` on the `BreweryPost` table. All the data in the column will be lost.
- A unique constraint covering the columns `[locationId]` on the table `BreweryPost` will be added. If there are existing duplicate values, this will fail.
- Added the required column `locationId` to the `BreweryPost` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "BreweryPost" DROP COLUMN "address";
ALTER TABLE "BreweryPost" DROP COLUMN "city";
ALTER TABLE "BreweryPost" DROP COLUMN "coordinates";
ALTER TABLE "BreweryPost" DROP COLUMN "country";
ALTER TABLE "BreweryPost" DROP COLUMN "stateOrProvince";
ALTER TABLE "BreweryPost" ADD COLUMN "locationId" STRING NOT NULL;
-- CreateTable
CREATE TABLE "Location" (
"id" STRING NOT NULL,
"city" STRING NOT NULL,
"stateOrProvince" STRING,
"country" STRING,
"coordinates" FLOAT8[],
"address" STRING NOT NULL,
"postedById" STRING NOT NULL,
CONSTRAINT "Location_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "BreweryPost_locationId_key" ON "BreweryPost"("locationId");
-- AddForeignKey
ALTER TABLE "Location" ADD CONSTRAINT "Location_postedById_fkey" FOREIGN KEY ("postedById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BreweryPost" ADD CONSTRAINT "BreweryPost_locationId_fkey" FOREIGN KEY ("locationId") REFERENCES "Location"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -30,6 +30,7 @@ model User {
BeerImage BeerImage[] BeerImage BeerImage[]
BreweryImage BreweryImage[] BreweryImage BreweryImage[]
BreweryPostLike BreweryPostLike[] BreweryPostLike BreweryPostLike[]
Location Location[]
} }
model BeerPost { model BeerPost {
@@ -93,14 +94,23 @@ model BeerType {
beerPosts BeerPost[] beerPosts BeerPost[]
} }
model BreweryPost { model Location {
id String @id @default(uuid()) id String @id @default(uuid())
name String
city String city String
stateOrProvince String? stateOrProvince String?
country String? country String?
coordinates Float[] coordinates Float[]
address String address String
postedBy User @relation(fields: [postedById], references: [id], onDelete: Cascade)
postedById String
BreweryPost BreweryPost?
}
model BreweryPost {
id String @id @default(uuid())
name String
location Location @relation(fields: [locationId], references: [id])
locationId String @unique
beers BeerPost[] beers BeerPost[]
description String description String
createdAt DateTime @default(now()) @db.Timestamptz(3) createdAt DateTime @default(now()) @db.Timestamptz(3)

View File

@@ -26,17 +26,23 @@ const createNewBeerPosts = async ({
const beerType = beerTypes[Math.floor(Math.random() * beerTypes.length)]; const beerType = beerTypes[Math.floor(Math.random() * beerTypes.length)];
const breweryPost = breweryPosts[Math.floor(Math.random() * breweryPosts.length)]; const breweryPost = breweryPosts[Math.floor(Math.random() * breweryPosts.length)];
const createdAt = faker.date.past(1); const createdAt = faker.date.past(1);
const abv = Math.floor(Math.random() * (12 - 4) + 4);
const ibu = Math.floor(Math.random() * (60 - 10) + 10);
const name = faker.commerce.productName();
const description = faker.lorem.lines(20).replace(/(\r\n|\n|\r)/gm, ' ');
beerPostPromises.push( beerPostPromises.push(
prisma.beerPost.create({ prisma.beerPost.create({
data: { data: {
abv: Math.floor(Math.random() * (12 - 4) + 4), abv,
ibu: Math.floor(Math.random() * (60 - 10) + 10), ibu,
name: faker.commerce.productName(), name,
description: faker.lorem.lines(12).replace(/(\r\n|\n|\r)/gm, ' '), description,
createdAt,
brewery: { connect: { id: breweryPost.id } }, brewery: { connect: { id: breweryPost.id } },
postedBy: { connect: { id: user.id } }, postedBy: { connect: { id: user.id } },
type: { connect: { id: beerType.id } }, type: { connect: { id: beerType.id } },
createdAt,
}, },
}), }),
); );

View File

@@ -1,13 +1,13 @@
// eslint-disable-next-line import/no-extraneous-dependencies // eslint-disable-next-line import/no-extraneous-dependencies
import { faker } from '@faker-js/faker'; import { faker } from '@faker-js/faker';
import { User } from '@prisma/client'; import { Location, User } from '@prisma/client';
import DBClient from '../../DBClient'; import DBClient from '../../DBClient';
import geocode from '../../../config/mapbox/geocoder';
interface CreateNewBreweryPostsArgs { interface CreateNewBreweryPostsArgs {
numberOfPosts: number; numberOfPosts: number;
joinData: { joinData: {
users: User[]; users: User[];
locations: Location[];
}; };
} }
@@ -15,23 +15,17 @@ const createNewBreweryPosts = async ({
numberOfPosts, numberOfPosts,
joinData, joinData,
}: CreateNewBreweryPostsArgs) => { }: CreateNewBreweryPostsArgs) => {
const { users } = joinData; const { users, locations } = joinData;
const prisma = DBClient.instance; const prisma = DBClient.instance;
const breweryPromises = []; const breweryPromises = [];
// eslint-disable-next-line no-plusplus // eslint-disable-next-line no-plusplus
for (let i = 0; i < numberOfPosts; i++) { for (let i = 0; i < numberOfPosts; i++) {
const name = `${faker.commerce.productName()} Brewing Company`; const name = `${faker.commerce.productName()} Brewing Company`;
const location = faker.address.cityName(); const locationIndex = Math.floor(Math.random() * locations.length);
const location = locations[locationIndex];
// eslint-disable-next-line no-await-in-loop locations.splice(locationIndex, 1); // Remove the location from the array
const geodata = await geocode(location); const description = faker.lorem.lines(20).replace(/(\r\n|\n|\r)/gm, ' ');
const city = geodata.text;
const stateOrProvince = geodata.context.find((c) => c.id.startsWith('region'))?.text;
const country = geodata.context.find((c) => c.id.startsWith('country'))?.text;
const coordinates = geodata.center;
const address = geodata.place_name;
const description = faker.lorem.lines(5);
const user = users[Math.floor(Math.random() * users.length)]; const user = users[Math.floor(Math.random() * users.length)];
const createdAt = faker.date.past(1); const createdAt = faker.date.past(1);
const dateEstablished = faker.date.past(40); const dateEstablished = faker.date.past(40);
@@ -40,17 +34,11 @@ const createNewBreweryPosts = async ({
prisma.breweryPost.create({ prisma.breweryPost.create({
data: { data: {
name, name,
city,
stateOrProvince,
country,
coordinates,
address,
description, description,
postedBy: { connect: { id: user.id } },
createdAt, createdAt,
dateEstablished, dateEstablished,
postedBy: { connect: { id: user.id } },
location: { connect: { id: location.id } },
}, },
}), }),
); );

View File

@@ -0,0 +1,63 @@
/* eslint-disable import/no-extraneous-dependencies */
import { faker } from '@faker-js/faker';
import { User, Location } from '@prisma/client';
import { GeocodeFeature } from '@mapbox/mapbox-sdk/services/geocoding';
import DBClient from '../../DBClient';
import geocode from '../../../config/mapbox/geocoder';
interface CreateNewLocationsArgs {
numberOfLocations: number;
joinData: {
users: User[];
};
}
const createNewLocations = async ({
numberOfLocations,
joinData,
}: CreateNewLocationsArgs) => {
const prisma = DBClient.instance;
const locationNames: string[] = [];
// eslint-disable-next-line no-plusplus
for (let i = 0; i < numberOfLocations; i++) {
locationNames.push(faker.address.cityName());
}
const geocodePromises: Promise<GeocodeFeature>[] = [];
locationNames.forEach((locationName) => {
geocodePromises.push(geocode(locationName));
});
const geocodedLocations = await Promise.all(geocodePromises);
const locationPromises: Promise<Location>[] = [];
geocodedLocations.forEach((geodata) => {
const city = geodata.text;
const user = joinData.users[Math.floor(Math.random() * joinData.users.length)];
const stateOrProvince = geodata.context?.find((c) => c.id.startsWith('region'))?.text;
const country = geodata.context?.find((c) => c.id.startsWith('country'))?.text;
const coordinates = geodata.center;
const address = geodata.place_name;
locationPromises.push(
prisma.location.create({
data: {
city,
stateOrProvince,
country,
coordinates,
address,
postedBy: { connect: { id: user.id } },
},
}),
);
});
return Promise.all(locationPromises);
};
export default createNewLocations;

View File

@@ -16,6 +16,9 @@ const createNewUsers = async ({ numberOfUsers }: CreateNewUsersArgs) => {
Array.from({ length: numberOfUsers }, () => argon2.hash(faker.internet.password())), Array.from({ length: numberOfUsers }, () => argon2.hash(faker.internet.password())),
); );
const takenEmails: string[] = [];
const takenUsernames: string[] = [];
// eslint-disable-next-line no-plusplus // eslint-disable-next-line no-plusplus
for (let i = 0; i < numberOfUsers; i++) { for (let i = 0; i < numberOfUsers; i++) {
const randomValue = crypto.randomBytes(10).toString('hex'); const randomValue = crypto.randomBytes(10).toString('hex');
@@ -24,6 +27,18 @@ const createNewUsers = async ({ numberOfUsers }: CreateNewUsersArgs) => {
const username = `${firstName[0]}.${lastName}.${randomValue}`; const username = `${firstName[0]}.${lastName}.${randomValue}`;
const email = faker.internet.email(firstName, randomValue, 'example.com'); const email = faker.internet.email(firstName, randomValue, 'example.com');
const usernameTaken = takenUsernames.includes(username);
const emailTaken = takenEmails.includes(email);
if (usernameTaken || emailTaken) {
i -= 1;
// eslint-disable-next-line no-continue
continue;
}
takenEmails.push(email);
takenUsernames.push(username);
const hash = hashedPasswords[i]; const hash = hashedPasswords[i];
const dateOfBirth = faker.date.birthdate({ mode: 'age', min: 19 }); const dateOfBirth = faker.date.birthdate({ mode: 'age', min: 19 });
const createdAt = faker.date.past(1); const createdAt = faker.date.past(1);

View File

@@ -14,6 +14,7 @@ import createNewBreweryPostComments from './create/createNewBreweryPostComments'
import createNewBreweryPosts from './create/createNewBreweryPosts'; import createNewBreweryPosts from './create/createNewBreweryPosts';
import createNewUsers from './create/createNewUsers'; import createNewUsers from './create/createNewUsers';
import createNewBreweryPostLikes from './create/createNewBreweryPostLikes'; import createNewBreweryPostLikes from './create/createNewBreweryPostLikes';
import createNewLocations from './create/createNewLocations';
(async () => { (async () => {
try { try {
@@ -25,15 +26,25 @@ import createNewBreweryPostLikes from './create/createNewBreweryPostLikes';
logger.info('Database cleared successfully, preparing to seed.'); logger.info('Database cleared successfully, preparing to seed.');
const users = await createNewUsers({ numberOfUsers: 1000 }); const users = await createNewUsers({ numberOfUsers: 1000 });
logger.info('Users created successfully.');
console.log(users);
const locations = await createNewLocations({
numberOfLocations: 1500,
joinData: { users },
});
logger.info('Locations created successfully.');
const [breweryPosts, beerTypes] = await Promise.all([ const [breweryPosts, beerTypes] = await Promise.all([
createNewBreweryPosts({ numberOfPosts: 30, joinData: { users } }), createNewBreweryPosts({ numberOfPosts: 1300, joinData: { users, locations } }),
createNewBeerTypes({ joinData: { users } }), createNewBeerTypes({ joinData: { users } }),
]); ]);
logger.info('Brewery posts and beer types created successfully.');
const beerPosts = await createNewBeerPosts({ const beerPosts = await createNewBeerPosts({
numberOfPosts: 200, numberOfPosts: 200,
joinData: { breweryPosts, beerTypes, users }, joinData: { breweryPosts, beerTypes, users },
}); });
logger.info('Beer posts created successfully.');
const [ const [
beerPostComments, beerPostComments,
breweryPostComments, breweryPostComments,
@@ -67,6 +78,10 @@ import createNewBreweryPostLikes from './create/createNewBreweryPostLikes';
}), }),
]); ]);
logger.info(
'Beer post comments, brewery post comments, beer post likes, beer images, and brewery images created successfully.',
);
const end = performance.now(); const end = performance.now();
const timeElapsed = (end - start) / 1000; const timeElapsed = (end - start) / 1000;

View File

@@ -1,5 +1,6 @@
import DBClient from '@/prisma/DBClient'; import DBClient from '@/prisma/DBClient';
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult'; import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
import { z } from 'zod'; import { z } from 'zod';
const prisma = DBClient.instance; const prisma = DBClient.instance;
@@ -14,11 +15,15 @@ const getAllBreweryPosts = async (pageNum?: number, pageSize?: number) => {
take, take,
select: { select: {
id: true, id: true,
coordinates: true, location: {
address: true, select: {
city: true, city: true,
stateOrProvince: true, address: true,
coordinates: true,
country: true, country: true,
stateOrProvince: true,
},
},
description: true, description: true,
name: true, name: true,
postedBy: { select: { username: true, id: true } }, postedBy: { select: { username: true, id: true } },

View File

@@ -9,11 +9,15 @@ const getBreweryPostById = async (id: string) => {
await prisma.breweryPost.findFirst({ await prisma.breweryPost.findFirst({
select: { select: {
id: true, id: true,
coordinates: true, location: {
address: true, select: {
city: true, city: true,
stateOrProvince: true, address: true,
coordinates: true,
country: true, country: true,
stateOrProvince: true,
},
},
description: true, description: true,
name: true, name: true,
breweryImages: { select: { path: true, caption: true, id: true, alt: true } }, breweryImages: { select: { path: true, caption: true, id: true, alt: true } },

View File

@@ -4,11 +4,13 @@ const BreweryPostQueryResult = z.object({
id: z.string(), id: z.string(),
name: z.string(), name: z.string(),
description: z.string(), description: z.string(),
address: z.string(), location: z.object({
city: z.string(), city: z.string(),
stateOrProvince: z.string().or(z.null()), address: z.string(),
coordinates: z.array(z.number()), coordinates: z.array(z.number()),
country: z.string().or(z.null()), country: z.string().nullable(),
stateOrProvince: z.string().nullable(),
}),
postedBy: z.object({ id: z.string(), username: z.string() }), postedBy: z.object({ id: z.string(), username: z.string() }),
breweryImages: z.array( breweryImages: z.array(
z.object({ path: z.string(), caption: z.string(), id: z.string(), alt: z.string() }), z.object({ path: z.string(), caption: z.string(), id: z.string(), alt: z.string() }),

View File

@@ -1,4 +1,4 @@
import DBClient from '@/prisma/DBClient'; import DBClient from '../../prisma/DBClient';
const findUserByEmail = async (email: string) => const findUserByEmail = async (email: string) =>
DBClient.instance.user.findFirst({ DBClient.instance.user.findFirst({

View File

@@ -1,4 +1,4 @@
import DBClient from '@/prisma/DBClient'; import DBClient from '../../prisma/DBClient';
const findUserByUsername = async (username: string) => const findUserByUsername = async (username: string) =>
DBClient.instance.user.findFirst({ DBClient.instance.user.findFirst({