Skip to content
Open
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ NUXT_PUBLIC_POSTHOG_DISABLED=true
# Maps and Auth
GOOGLE_CLIENT_ID=GOOGLE_CLIENT_ID
GOOGLE_CLIENT_SECRET=GOOGLE_CLIENT_SECRET
GOOGLE_MAPS_API_KEY=GOOGLE_MAPS_API_KEY
NUXT_GOOGLE_MAPS_API_KEY=GOOGLE_MAPS_API_KEY
GOOGLE_MAPS_API_KEY_BACKEND=GOOGLE_MAPS_API_KEY_BACKEND
FIREBASE_SALT_SEPARATOR=c2U=
FIREBASE_SIGNER_KEY=c29tZS12ZXJ5LXNlY3JldC1zdHJpbmc=
Expand Down
1 change: 0 additions & 1 deletion components/inputs/VenueInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ function getVenue(address: any) {

return $client.profiles.findVenueOrCreate.mutate({
placeId: address.place_id,
googleMapsPlace: address,
})
}

Expand Down
50 changes: 4 additions & 46 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -201,10 +201,6 @@ model Profile {

calendars Calendar[] @relation("calendar_owner")

ConversationAsA Conversation[] @relation("AProfile")
ConversationAsB Conversation[] @relation("BProfile")
messages Message[]

@@index([photo])
@@index([userId])
@@index([cityId])
Expand Down Expand Up @@ -295,8 +291,9 @@ model Event {
guests Guest[] @relation("event_guests")
ticketPurchases TicketPurchase[] @relation("event_purchases")

createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

calendarEvents CalendarEvent[] @relation("calendar_event_to_event")
}

Expand Down Expand Up @@ -603,7 +600,7 @@ model Calendar {
name String @default("")

profileId String
profile Profile @relation("calendar_owner", fields: [profileId], references: [id])
profile Profile @relation("calendar_owner", fields: [profileId], references: [id], onDelete: Cascade)

state String @default("pending") // pending, processing, processed, failed
lastSyncedAt DateTime?
Expand Down Expand Up @@ -652,42 +649,3 @@ model CalendarEvent {
@@index([calendarId, startDate])
@@index([facebookId])
}

model Conversation {
id String @id @default(cuid())
pairKey String @unique // `${min(aId,bId)}-${max(aId,bId)}`
aId String
bId String
aLastSeenAt DateTime?
bLastSeenAt DateTime?
lastMessageId String? @unique
lastMessage Message? @relation("LastMessage", fields: [lastMessageId], references: [id])

messages Message[] @relation("ConversationMessages")

a Profile @relation("AProfile", fields: [aId], references: [id])
b Profile @relation("BProfile", fields: [bId], references: [id])

createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

@@unique([aId, bId])
@@index([aId, updatedAt])
@@index([bId, updatedAt])
}

model Message {
id String @id @default(cuid())
conversationId String
senderId String
content String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
lastMessageFor Conversation? @relation("LastMessage")

conversation Conversation @relation("ConversationMessages", fields: [conversationId], references: [id], onDelete: Cascade)
sender Profile @relation(fields: [senderId], references: [id])

@@index([conversationId, createdAt])
@@index([senderId, createdAt])
}
4 changes: 2 additions & 2 deletions server/trpc/routers/cities.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { publicProcedure, router } from '~/server/trpc/init'
import { prisma } from '~/server/prisma'
import { z } from 'zod'
import { addCity } from '~/server/utils/city'
import { findOrCreateCity } from '~/server/utils/city'

export const citiesRouter = router({
popular: publicProcedure.query(async ({ ctx }) => {
Expand Down Expand Up @@ -106,6 +106,6 @@ export const citiesRouter = router({
.mutation(async ({ ctx, input }) => {
const { city } = input

return await addCity(city)
return await findOrCreateCity(city.id)
}),
})
105 changes: 94 additions & 11 deletions server/trpc/routers/profiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@ import { publicProcedure, router } from '~/server/trpc/init'
import { prisma } from '~/server/prisma'
import { getServerSession } from '#auth'
import { privacySettingsSchema } from '~/schemas/profile'
import {
getCityIdFromGooglePlace,
getPlaceDetails,
} from '~/server/utils/google_maps'
import { TRPCError } from '@trpc/server'
import { findOrCreateCity } from '~/server/utils/city'
import { getSlug } from '~/utils/slug'
import { AddressType } from '@googlemaps/google-maps-services-js'
import { toCalendar } from '@internationalized/date'

const profileUpdateSchema = z.object({
bio: z.string().optional(),
Expand Down Expand Up @@ -304,21 +313,95 @@ export const profilesRouter = router({

return { profiles, totalCount, hasMore, nextPage }
}),

findVenueOrCreate: publicProcedure
.input(z.object({ placeId: z.string(), googleMapsPlace: z.any() }))
.input(z.object({ placeId: z.string() }))
.mutation(async ({ input }) => {
const { placeId, googleMapsPlace } = input
const { placeId } = input
const place = await getPlaceDetails(placeId)
if (!place || !place.name) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Invalid Google Place ID or place has no name.',
})
}
const placeTypes = place.types || []
if (
placeTypes.includes(AddressType.locality) ||
placeTypes.includes(AddressType.administrative_area_level_1) ||
placeTypes.includes(AddressType.country)
) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Please select a specific venue, not a city or region.',
})
}
return await prisma.$transaction(async (tx) => {
const existingVenue = await tx.profile.findFirst({
where: { placeId },
})
if (existingVenue) {
return existingVenue
}
Comment on lines +341 to +345
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Schema guardrails: ensure uniqueness.

Double‑check unique indexes for Profile.username and Profile.placeId exist; otherwise the retry logic can’t rely on P2002.


🏁 Script executed:

#!/bin/bash
fd -e prisma | xargs rg -nP -C2 'model\s+Profile\b[\s\S]*?\busername\b[^\n]*@unique'
fd -e prisma | xargs rg -nP -C2 'model\s+Profile\b[\s\S]*?\bplaceId\b[^\n]*@unique'

Length of output: 0


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "PWD: $(pwd)"
echo "Top-level files:"
ls -la | sed -n '1,200p'

echo
echo "Looking for Prisma schema files (schema.prisma and any *.prisma) and model definitions..."
rg -nP --hidden --no-ignore-vcs -S --glob '!**/node_modules/**' "datasource\s+db" -g '*.prisma' || true
rg --files -g '*.prisma' || true

echo
echo "Search for 'model Profile' (case-sensitive and insensitive):"
rg -nP -S "model\s+Profile\b" || true
rg -nP -S "(?i)model\s+profile\b" || true

echo
echo "Search inside .prisma files for field-level uniques and @@unique involving username/placeId:"
rg -nP -S "username\b.*@unique" -g '*.prisma' || true
rg -nP -S "placeId\b.*@unique" -g '*.prisma' || true
rg -nP -S "@@unique\([^\)]*(username|placeId)" -g '*.prisma' || true

echo
echo "Fallback: search the entire repo for 'username' and 'placeId' occurrences (to locate alternate schemas or naming):"
rg -nP -S "username\b" || true
rg -nP -S "placeId\b" || true

Length of output: 26869


Schema guardrails: add a unique constraint for Profile.placeId or stop relying on P2002

prisma/schema.prisma — Profile.username is declared as String @unique (line ~127); Profile.placeId is String? with no @unique (line ~139). If your retry logic depends on Prisma P2002, add a unique constraint on placeId (field @unique or @@unique([placeId])) and run a migration, or change the create/retry flow to handle concurrent duplicates.

🤖 Prompt for AI Agents
In server/trpc/routers/profiles.ts around lines 341 to 345, the code queries by
placeId then returns existingVenue but the review warns that you rely on Prisma
P2002 for duplicate protection while Profile.placeId is not unique in the
schema; either add a unique constraint to Profile.placeId in
prisma/schema.prisma (add `@unique` on the field or an `@@unique([placeId])`)
and run a migration, or modify this create/retry flow to handle concurrent
creates safely (e.g., perform an atomic upsert, wrap creation in a transaction
with a select-for-update pattern, or catch duplicate create errors without
assuming P2002); choose one approach and implement it consistently across the
codebase.


const existingVenue = await prisma.profile.findFirst({
where: {
placeId,
},
})
// Get the city's unique Place ID from the venue's data.
const cityPlaceId = await getCityIdFromGooglePlace(place)

if (existingVenue) {
return existingVenue
}
if (!cityPlaceId) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Could not determine a city for the selected venue.',
})
}

let city
try {
city = await findOrCreateCity(cityPlaceId)
} catch (error) {
console.error('Error in findOrCreateCity', error)
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: `Failed to create city: ${error}`,
})
}

throw new Error('Venue not found')
// Generate a unique username for the venue to avoid conflicts.
const baseSlug = getSlug(place.name!)
let username = baseSlug
const existingProfile = await tx.profile.findFirst({
where: { username },
})
if (existingProfile) {
username = getSlug(`${place.name!}-${city.name}`)
const anotherExisting = await tx.profile.findFirst({
where: { username },
})
if (anotherExisting) {
throw new Error(
`Could not generate a unique username for this venue.`
)
}
}
try {
const newVenue = await tx.profile.create({
data: {
name: place.name!,
username,
type: 'Venue',
placeId,
cityId: city.id,
formattedAddress: place.formatted_address,
website: place.website || null,
phone: place.international_phone_number || null,
lat: place.geometry?.location?.lat || null,
lng: place.geometry?.location?.lng || null,
mapUrl: place.url || null,
},
})
return newVenue
} catch (error) {
throw error
}
})
}),
})
77 changes: 44 additions & 33 deletions server/utils/city.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { getCountryCode } from '~/utils/country'
import { getSlug } from '~/utils/slug'
import { prisma } from '~/server/prisma'
import fetch from 'node-fetch'

const addressPart = (result: any, type: string) => {
if (!result || !result.address_components) {
Expand Down Expand Up @@ -45,7 +44,7 @@ const getAddress = (places: any) => {

const getAddressFromPlaceId = async (placeId: string) => {
if (!placeId) {
return {}
return null
}

const apiKey = useRuntimeConfig().public.googleMapsApiKey
Expand All @@ -60,51 +59,63 @@ const getAddressFromPlaceId = async (placeId: string) => {
return getAddress(data.results)
}

export async function addCity(city: any) {
const address = await getAddressFromPlaceId(city.id)

const existingCity = await prisma.city.findUnique({
where: { id: city.id },
export async function findOrCreateCity(placeId: string) {
const existing = await prisma.city.findUnique({
where: { id: placeId },
})
if (existing) return existing

if (!!existingCity) {
return existingCity
}
const address = await getAddressFromPlaceId(placeId)
Comment on lines +62 to +68
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Update call sites to new API (findOrCreateCity).

Ensure no stale addCity(...) usages remain (e.g., server/trpc/routers/cities.ts snippet).


🏁 Script executed:

#!/bin/bash
# Find legacy addCity call sites and confirm new usage
rg -n '\baddCity\s*\(' -g '!**/dist/**' -g '!**/.nuxt/**' || true
rg -n '\bfindOrCreateCity\s*\(' -g '!**/dist/**' -g '!**/.nuxt/**' || true

Length of output: 297


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show ripgrep debug to understand why files were skipped previously
rg --debug 'addCity\s*\(' || true
rg --debug 'findOrCreateCity\s*\(' || true

# Search repository for legacy and new API call sites, don't respect .gitignore to avoid skipping files,
# but explicitly exclude common generated directories to reduce noise.
rg -n -C2 --hidden --no-ignore -S --follow -g '!node_modules/**' -g '!**/dist/**' -g '!**/.nuxt/**' '(\baddCity\s*\(|\bfindOrCreateCity\s*\()' || true

Length of output: 13374


Update call sites to the new findOrCreateCity API — remove legacy addCity usages

  • server/trpc/routers/cities.ts — contains return await addCity(city); replace with findOrCreateCity(...) (or migrate/wrap accordingly).
  • cli/importer/profile.ts & cli/importer/index.ts — export/call a local addCity(...); either migrate these to the new API or rename to avoid confusion.
  • server/utils/city.ts already provides findOrCreateCity and server/trpc/routers/profiles.ts already uses it.
🤖 Prompt for AI Agents
In server/utils/city.ts around lines 90 to 96, the project now exposes
findOrCreateCity and legacy addCity usages must be replaced: update
server/trpc/routers/cities.ts to remove the call to addCity(city) and instead
call await findOrCreateCity(cityPlaceId) (or pass the proper placeId/value that
addCity formerly accepted), refactor cli/importer/profile.ts and
cli/importer/index.ts to either call the new findOrCreateCity API or rename
their local addCity to avoid confusion (export the new function name and update
all imports/usages), and ensure any type/signature differences are handled (map
arguments/returned value to the new API). Make these changes consistently across
the listed files and run tests to validate no remaining references to addCity
remain.


const { locality, region, country } = address
if (!address || !address.locality || !address.country) {
throw new Error("Couldn't determine city from Google data")
}
const { locality, region, country, lat, lng } = address

let slug = getSlug(locality)

let existingLocality = await prisma.city.findFirst({
const existingLocality = await prisma.city.findFirst({
where: { slug },
})

if (!!existingLocality) {
slug = getSlug([region, locality].join('-'))
if (existingLocality) {
slug = getSlug(`${region} ${locality}`)
}

let existingRegion = await prisma.city.findFirst({
const existingRegion = await prisma.city.findFirst({
where: { slug },
})

if (!!existingRegion) {
throw new Error(`city: region-locality slug already exists: ${slug}`)
if (existingRegion) {
throw new Error(
`City slug conflict: A city with slug "${slug}" already exists.`
)
}

const result = {
id: city.id,
const countryCode = await getCountryCode(country)

const createData = {
id: placeId,
name: locality,
region,
slug,
countryCode: await getCountryCode(country),
description: '',
lat: address.lat,
lng: address.lng,
region: region || '',
slug: slug,
countryCode,
lat: address.lat ?? 0,
lng: address.lng ?? 0,
}
const updateData = {
name: locality,
region: region || '',
countryCode,
lat: address.lat ?? 0,
lng: address.lng ?? 0,
}

const newCity = await prisma.city.create({
data: result,
})

return newCity
try {
const city = await prisma.city.upsert({
where: { id: placeId },
create: createData,
update: updateData,
})
return city
} catch (error) {
throw error
}
}
Loading
Loading