import bcrypt from 'bcrypt'; import type Log from 'bunyan'; import type { PrismaClient, Want, User, Listing, Offer, Prisma } from '@prisma/client'; import * as Bluebird from 'bluebird'; import _ from 'lodash'; import { Status } from './offers'; import { wantDetails } from './utils'; export interface UserCreateParams { email: string, password: string, } export default function createPg(log: Log, prisma: PrismaClient) { async function userFindId(id: UUID) { return prisma.user.findUnique({ where: { id }}); } async function userFindEmail(email: Email) { return prisma.user.findUnique({ where: { email }}); } async function userWants(user: User) { const wants = await prisma.want.findMany({ where: { userId: user.id }, include: { _count: { select: { offers: true }} }, }) return _.sortBy(wants, w => `${w.data.artists_sort} - ${w.data.title}`); } async function userOffers(user: User) { return prisma.offer.findMany({ where: { userId: user.id, status: { not: Status.Declined } }, include: { want: true, }, orderBy: [ { updatedAt: 'asc' }, { total: 'asc' }, { listingId: 'asc' }, ], }); } async function offerFindId(id: UUID) { return prisma.offer.findFirstOrThrow({ where: { id }, include: { want: true, } }); } async function userOfferFind(user: User, id: UUID) { return prisma.offer.findFirstOrThrow({ where: { userId: user.id, id, }, include: { want: true, } }); } async function userCreate(params: UserCreateParams) { const hashRounds = 10; const password = await bcrypt.hash(params.password, hashRounds); const data = { email: params.email, password, }; return prisma.user.create({ data, }); } // userUpdate({ email: 'jen@vdk.com', password: 'jen' }); // userUpdate({ email: 'james@mcd', password: 'james' }); async function userUpdate(params: UserCreateParams) { const hashRounds = 10; const password = await bcrypt.hash(params.password, hashRounds); const data = { email: params.email, password, }; return prisma.user.update({ where: { email: params.email }, data, }); } async function wantFindId(id: UUID) { return prisma.want.findFirstOrThrow({ where: { id }, }); } async function wantList(user: User) { return prisma.want.findMany({ where: { userId: user.id }, }); } async function wantsToSync() { return prisma.want.findMany({ orderBy: { updatedAt: 'asc' }, where: { updatedAt: { lte: new Date(Date.now() - 1000 * 60 * 15) } } }); } async function wantRm(id: UUID) { await prisma.want.delete({ where: { id }, }); } async function wantCreate(discogsRelease, user) { const data: { userId: UUID, release: DiscogsListing, data: any } = { data: discogsRelease, release: discogsRelease.id, userId: user.id, }; return prisma.want.create({ data, }); } async function wantSynced(want: Want) { return prisma.want.update({ where: { id: want.id }, data: { updatedAt: new Date() }, }); } async function listingRmUnlisted(want: Want, currentListingIds: DiscogsListing[]) { return prisma.$transaction(async (prisma) => { log.debug({ currentListingIds }); const protectedOfferStatuses = [Status.Confirmed, Status.Complete]; // delete all the offers where we don't want to keep them // for historical reasons const { count: offersRemoved } = await prisma.offer.deleteMany({ where: { wantId: want.id, listingId: { notIn: currentListingIds, }, status: { notIn: protectedOfferStatuses, }, } }); if (offersRemoved > 0) log.info({ offersRemoved, ...wantDetails(want) }, 'pending offers removed'); // now we have to filter listingIds with confirmed offers // and get their listingIds to filter out const confirmedListingIds = (await prisma.offer.findMany({ where: { wantId: want.id, status: { in: protectedOfferStatuses, }, }, select: { listingId: true, }, })).map(r => r.listingId); const removalIds = currentListingIds.filter(id => confirmedListingIds.includes(id)); if (removalIds.length) log.info({ removalIds }); // finally we delete listings that aren't related // to a historical offer const { count } = await prisma.listing.deleteMany({ where: { id: { in: removalIds }, }, }); if (count) log.info({ count }, 'second stage open offers removed'); return count; }); } async function listingUpsert(listing, want) { const values = { id: listing.id, wantId: want.id, seller: listing.seller.id, data: listing, }; return prisma.listing.upsert({ where: { id: listing.id }, update: values, create: values, }); } async function listingFind(id: DiscogsListing) { return prisma.listing.findUnique({ where: { id, }, include: { want: { include: { user: true }, } } }); } async function listingByIds(ids: DiscogsListing[]) { return prisma.listing.findMany({ where: { id: { in: ids }, }, include: { want: { include: { user: true }, } } }); } async function interestingListings() { // this basically just needs to return the listingIds to generate offers // but for ease of abstraction we get the listings themselves // grouped by seller const candidates = await prisma.$queryRaw` WITH groups AS ( select seller, count(distinct("wantId")), array_agg(id) as listings from listings where seller not in (3709203) group by seller order by count desc ) select * from groups where count >= 5; ` as BundleCandidate[]; return Promise.all(candidates.map(async c => prisma.listing.findMany({ where: { id: { in: c.listings } }, include: { want: { include: { user: true }, } }, distinct: ['wantId'], }))); } async function offersUpdate(offers) { return await prisma.$transaction( offers.map(offer => prisma.offer.upsert({ where: { listingId: offer.listingId, }, create: offer, update: _.omit(offer, ['status', 'id']), })) ); } async function offerStatusSet(ids: UUID[], status: Status) { return prisma.offer.updateMany({ where: { id: { in: ids } }, data: { status }, }); } // effectively all offers async function offersAdmin() { return prisma.offer.findMany({ include: { listing: true, user: true, want: true, }, }); } return { userFindId, userFindEmail, userCreate, userWants, userOffers, userOfferFind, wantCreate, wantList, wantsToSync, wantSynced, wantRm, listingUpsert, listingFind, listingRmUnlisted, listingByIds, interestingListings, offersUpdate, offerStatusSet, offersAdmin, offerFindId, }; } export type Pg = ReturnType;