2021-04-12 18:05:09 +00:00
import { UserInputError , AuthenticationError } from 'apollo-server-micro'
2021-05-20 19:11:58 +00:00
import { ensureProtocol } from '../../lib/url'
2021-05-20 01:09:32 +00:00
import serialize from './serial'
2021-09-06 22:36:08 +00:00
import { decodeCursor , LIMIT , nextCursorEncoded } from '../../lib/cursor'
2021-08-22 15:25:17 +00:00
import { getMetadata , metadataRuleSets } from 'page-metadata-parser'
import domino from 'domino'
2022-03-09 19:44:50 +00:00
import { BOOST _MIN } from '../../lib/constants'
2021-06-22 17:47:49 +00:00
2021-12-21 21:29:42 +00:00
async function comments ( models , id , sort ) {
let orderBy
switch ( sort ) {
case 'top' :
2022-05-17 19:54:12 +00:00
orderBy = 'ORDER BY "Item"."weightedVotes" DESC, "Item".id DESC'
2021-12-21 21:29:42 +00:00
break
case 'recent' :
2022-05-17 19:54:12 +00:00
orderBy = 'ORDER BY "Item".created_at DESC, "Item".id DESC'
2021-12-21 21:29:42 +00:00
break
default :
2022-04-02 22:39:15 +00:00
orderBy = COMMENTS _ORDER _BY _SATS
2021-12-21 21:29:42 +00:00
break
}
2021-04-29 15:56:28 +00:00
const flat = await models . $queryRaw ( `
WITH RECURSIVE base AS (
2021-12-21 21:29:42 +00:00
$ { SELECT } , ARRAY [ row _number ( ) OVER ( $ { orderBy } , "Item" . path ) ] AS sort _path
2021-04-29 15:56:28 +00:00
FROM "Item"
2021-05-11 20:29:44 +00:00
WHERE "parentId" = $1
2021-04-29 15:56:28 +00:00
UNION ALL
2021-12-21 21:29:42 +00:00
$ { SELECT } , p . sort _path || row _number ( ) OVER ( $ { orderBy } , "Item" . path )
2021-04-29 15:56:28 +00:00
FROM base p
2022-05-17 19:54:12 +00:00
JOIN "Item" ON "Item" . "parentId" = p . id )
2021-05-11 20:29:44 +00:00
SELECT * FROM base ORDER BY sort _path ` , Number(id))
2021-04-29 15:56:28 +00:00
return nestComments ( flat , id ) [ 0 ]
}
2022-04-03 17:25:39 +00:00
const COMMENTS _ORDER _BY _SATS =
2022-07-01 18:38:47 +00:00
'ORDER BY POWER("Item"."weightedVotes", 1.2)/POWER(EXTRACT(EPOCH FROM ((NOW() AT TIME ZONE \'UTC\') - "Item".created_at))/3600+2, 1.3) DESC NULLS LAST, "Item".id DESC'
2022-04-03 17:25:39 +00:00
2021-09-23 17:42:00 +00:00
export async function getItem ( parent , { id } , { models } ) {
const [ item ] = await models . $queryRaw ( `
$ { SELECT }
FROM "Item"
WHERE id = $1 ` , Number(id))
return item
}
2021-12-16 23:05:31 +00:00
function topClause ( within ) {
2022-04-02 22:39:15 +00:00
let interval = ' AND "Item".created_at >= $1 - INTERVAL '
2021-12-16 23:05:31 +00:00
switch ( within ) {
case 'day' :
interval += "'1 day'"
break
case 'week' :
interval += "'7 days'"
break
case 'month' :
interval += "'1 month'"
break
case 'year' :
interval += "'1 year'"
break
default :
interval = ''
break
}
return interval
}
2021-04-12 18:05:09 +00:00
export default {
Query : {
2022-02-17 17:23:43 +00:00
items : async ( parent , { sub , sort , cursor , name , within } , { me , models } ) => {
2021-06-22 17:47:49 +00:00
const decodedCursor = decodeCursor ( cursor )
2022-02-17 17:23:43 +00:00
let items ; let user ; let pins ; let subFull
const subClause = ( num ) => {
2022-02-28 20:09:21 +00:00
return sub ? ` AND "subName" = $ ${ num } ` : ` AND ("subName" IS NULL OR "subName" = $ ${ num } ) `
2022-02-26 16:41:30 +00:00
}
const activeOrMine = ( ) => {
2022-06-02 23:25:21 +00:00
return me ? ` AND (status <> 'STOPPED' OR "userId" = ${ me . id } ) ` : ' AND status <> \'STOPPED\' '
2022-02-17 17:23:43 +00:00
}
2021-10-26 20:49:37 +00:00
2021-06-24 23:56:01 +00:00
switch ( sort ) {
case 'user' :
2021-10-26 20:49:37 +00:00
if ( ! name ) {
throw new UserInputError ( 'must supply name' , { argumentName : 'name' } )
}
user = await models . user . findUnique ( { where : { name } } )
if ( ! user ) {
throw new UserInputError ( 'no user has that name' , { argumentName : 'name' } )
}
2021-06-24 23:56:01 +00:00
items = await models . $queryRaw ( `
$ { SELECT }
FROM "Item"
WHERE "userId" = $1 AND "parentId" IS NULL AND created _at <= $2
2022-01-07 16:32:31 +00:00
AND "pinId" IS NULL
2022-02-26 16:41:30 +00:00
$ { activeOrMine ( ) }
2021-06-24 23:56:01 +00:00
ORDER BY created _at DESC
OFFSET $3
2021-10-26 20:49:37 +00:00
LIMIT $ { LIMIT } ` , user.id, decodedCursor.time, decodedCursor.offset)
2021-06-24 23:56:01 +00:00
break
2022-02-17 17:23:43 +00:00
case 'recent' :
2021-06-24 23:56:01 +00:00
items = await models . $queryRaw ( `
2021-06-22 17:47:49 +00:00
$ { SELECT }
FROM "Item"
2022-04-25 17:03:21 +00:00
WHERE "parentId" IS NULL AND created _at <= $1
2022-02-17 17:23:43 +00:00
$ { subClause ( 3 ) }
2022-02-26 16:41:30 +00:00
$ { activeOrMine ( ) }
2021-06-22 17:47:49 +00:00
ORDER BY created _at DESC
OFFSET $2
2022-02-17 17:23:43 +00:00
LIMIT $ { LIMIT } ` , decodedCursor.time, decodedCursor.offset, sub || 'NULL')
break
case 'top' :
items = await models . $queryRaw ( `
$ { SELECT }
FROM "Item"
2022-04-02 22:39:15 +00:00
WHERE "parentId" IS NULL AND "Item" . created _at <= $1
2022-02-17 17:23:43 +00:00
AND "pinId" IS NULL
$ { topClause ( within ) }
2022-04-02 22:39:15 +00:00
$ { TOP _ORDER _BY _SATS }
2022-02-17 17:23:43 +00:00
OFFSET $2
2021-06-22 17:47:49 +00:00
LIMIT $ { LIMIT } ` , decodedCursor.time, decodedCursor.offset)
2021-06-24 23:56:01 +00:00
break
2022-02-17 17:23:43 +00:00
default :
// sub so we know the default ranking
if ( sub ) {
subFull = await models . sub . findUnique ( { where : { name : sub } } )
}
switch ( subFull ? . rankingType ) {
case 'AUCTION' :
// it might be sufficient to sort by the floor(maxBid / 1000) desc, created_at desc
// we pull from their wallet
// TODO: need to filter out by payment status
items = await models . $queryRaw ( `
$ { SELECT }
FROM "Item"
WHERE "parentId" IS NULL AND created _at <= $1
AND "pinId" IS NULL
$ { subClause ( 3 ) }
2022-06-02 23:25:21 +00:00
AND status < > 'STOPPED'
ORDER BY ( CASE WHEN status = 'ACTIVE' THEN "maxBid" ELSE 0 END ) DESC , created _at ASC
2022-02-17 17:23:43 +00:00
OFFSET $2
LIMIT $ { LIMIT } ` , decodedCursor.time, decodedCursor.offset, sub)
break
default :
// HACK we can speed hack the first hot page, by limiting our query to only
// the most recently created items so that the tables doesn't have to
// fully be computed
// if the offset is 0, we limit our search to posts from the last week
// if there are 21 items, return them ... if not do the unrestricted query
// instead of doing this we should materialize a view ... but this is easier for now
if ( decodedCursor . offset === 0 ) {
items = await models . $queryRaw ( `
$ { SELECT }
FROM "Item"
2022-04-02 22:39:15 +00:00
WHERE "parentId" IS NULL AND "Item" . created _at <= $1 AND "Item" . created _at > $3
2022-02-17 17:23:43 +00:00
AND "pinId" IS NULL
2022-02-28 20:09:21 +00:00
$ { subClause ( 4 ) }
2022-04-02 22:39:15 +00:00
$ { newTimedOrderByWeightedSats ( 1 ) }
2022-02-17 17:23:43 +00:00
OFFSET $2
2022-04-02 22:39:15 +00:00
LIMIT $ { LIMIT } ` , decodedCursor.time, decodedCursor.offset, new Date(new Date().setDate(new Date().getDate() - 5)), sub || 'NULL')
2022-02-17 17:23:43 +00:00
}
if ( decodedCursor . offset !== 0 || items ? . length < LIMIT ) {
items = await models . $queryRaw ( `
$ { SELECT }
FROM "Item"
2022-04-02 22:39:15 +00:00
WHERE "parentId" IS NULL AND "Item" . created _at <= $1
2022-02-17 17:23:43 +00:00
AND "pinId" IS NULL
2022-02-28 20:09:21 +00:00
$ { subClause ( 3 ) }
2022-04-02 22:39:15 +00:00
$ { newTimedOrderByWeightedSats ( 1 ) }
2022-02-17 17:23:43 +00:00
OFFSET $2
2022-02-28 20:09:21 +00:00
LIMIT $ { LIMIT } ` , decodedCursor.time, decodedCursor.offset, sub || 'NULL')
2022-02-17 17:23:43 +00:00
}
if ( decodedCursor . offset === 0 ) {
// get pins for the page and return those separately
pins = await models . $queryRaw ( ` SELECT rank_filter.*
FROM (
$ { SELECT } ,
rank ( ) OVER (
PARTITION BY "pinId"
ORDER BY created _at DESC
)
FROM "Item"
WHERE "pinId" IS NOT NULL
) rank _filter WHERE RANK = 1 ` )
}
break
}
break
2021-06-24 23:56:01 +00:00
}
2021-06-22 17:47:49 +00:00
return {
cursor : items . length === LIMIT ? nextCursorEncoded ( decodedCursor ) : null ,
2022-01-07 16:32:31 +00:00
items ,
pins
2021-06-22 17:47:49 +00:00
}
2021-04-24 21:05:07 +00:00
} ,
2022-01-25 19:34:51 +00:00
allItems : async ( parent , { cursor } , { models } ) => {
const decodedCursor = decodeCursor ( cursor )
const items = await models . $queryRaw ( `
$ { SELECT }
FROM "Item"
ORDER BY created _at DESC
OFFSET $1
LIMIT $ { LIMIT } ` , decodedCursor.offset)
return {
cursor : items . length === LIMIT ? nextCursorEncoded ( decodedCursor ) : null ,
items
}
} ,
2021-12-16 23:05:31 +00:00
moreFlatComments : async ( parent , { cursor , name , sort , within } , { me , models } ) => {
2021-06-24 23:56:01 +00:00
const decodedCursor = decodeCursor ( cursor )
2021-08-18 22:20:33 +00:00
2021-12-16 23:05:31 +00:00
let comments , user
switch ( sort ) {
case 'user' :
if ( ! name ) {
throw new UserInputError ( 'must supply name' , { argumentName : 'name' } )
}
2021-10-26 20:49:37 +00:00
2021-12-16 23:05:31 +00:00
user = await models . user . findUnique ( { where : { name } } )
if ( ! user ) {
throw new UserInputError ( 'no user has that name' , { argumentName : 'name' } )
}
2021-08-18 22:20:33 +00:00
2021-12-16 23:05:31 +00:00
comments = await models . $queryRaw ( `
$ { SELECT }
FROM "Item"
WHERE "userId" = $1 AND "parentId" IS NOT NULL
AND created _at <= $2
ORDER BY created _at DESC
OFFSET $3
LIMIT $ { LIMIT } ` , user.id, decodedCursor.time, decodedCursor.offset)
break
case 'top' :
comments = await models . $queryRaw ( `
$ { SELECT }
FROM "Item"
WHERE "parentId" IS NOT NULL
2022-04-02 22:39:15 +00:00
AND "Item" . created _at <= $1
2021-12-16 23:05:31 +00:00
$ { topClause ( within ) }
2022-04-02 22:39:15 +00:00
$ { TOP _ORDER _BY _SATS }
2021-12-16 23:05:31 +00:00
OFFSET $2
LIMIT $ { LIMIT } ` , decodedCursor.time, decodedCursor.offset)
break
default :
throw new UserInputError ( 'invalid sort type' , { argumentName : 'sort' } )
}
2021-08-18 22:20:33 +00:00
2021-06-24 23:56:01 +00:00
return {
cursor : comments . length === LIMIT ? nextCursorEncoded ( decodedCursor ) : null ,
comments
}
} ,
2021-09-23 17:42:00 +00:00
item : getItem ,
2021-08-22 15:25:17 +00:00
pageTitle : async ( parent , { url } , { models } ) => {
try {
const response = await fetch ( ensureProtocol ( url ) , { redirect : 'follow' } )
const html = await response . text ( )
const doc = domino . createWindow ( html ) . document
const metadata = getMetadata ( doc , url , { title : metadataRuleSets . title } )
return metadata ? . title
} catch ( e ) {
return null
}
2021-10-28 20:49:51 +00:00
} ,
dupes : async ( parent , { url } , { models } ) => {
const urlObj = new URL ( ensureProtocol ( url ) )
2021-12-20 22:26:22 +00:00
let uri = urlObj . hostname + urlObj . pathname
uri = uri . endsWith ( '/' ) ? uri . slice ( 0 , - 1 ) : uri
let similar = ` (http(s)?://)? ${ uri } /? `
2022-03-10 21:44:46 +00:00
const whitelist = [ 'news.ycombinator.com/item' , 'bitcointalk.org/index.php' ]
const youtube = [ 'www.youtube.com' , 'youtu.be' ]
2021-12-20 22:26:22 +00:00
if ( whitelist . includes ( uri ) ) {
similar += ` \\ ${ urlObj . search } `
2022-03-10 21:44:46 +00:00
} else if ( youtube . includes ( urlObj . hostname ) ) {
// extract id and create both links
const matches = url . match ( /(https?:\/\/)?((www\.)?(youtube(-nocookie)?|youtube.googleapis)\.com.*(v\/|v=|vi=|vi\/|e\/|embed\/|user\/.*\/u\/\d+\/)|youtu\.be\/)(?<id>[_0-9a-z-]+)/i )
similar = ` (http(s)?://)?(www.youtube.com/watch \\ ?v= ${ matches ? . groups ? . id } |youtu.be/ ${ matches ? . groups ? . id } ) `
2021-12-20 22:26:22 +00:00
} else {
2022-05-18 18:21:24 +00:00
similar += '((\\?|#)%)?'
2021-12-20 22:26:22 +00:00
}
2021-10-28 20:49:51 +00:00
return await models . $queryRaw ( `
$ { SELECT }
FROM "Item"
2021-12-20 22:26:22 +00:00
WHERE url SIMILAR TO $1
2021-10-28 20:49:51 +00:00
ORDER BY created _at DESC
2021-12-20 22:26:22 +00:00
LIMIT 3 ` , similar)
2021-12-21 21:29:42 +00:00
} ,
comments : async ( parent , { id , sort } , { models } ) => {
return comments ( models , id , sort )
2022-01-26 15:35:14 +00:00
} ,
2022-02-26 16:41:30 +00:00
search : async ( parent , { q : query , sub , cursor } , { me , models , search } ) => {
2022-01-26 15:35:14 +00:00
const decodedCursor = decodeCursor ( cursor )
2022-01-28 18:37:23 +00:00
let sitems
2022-01-26 15:35:14 +00:00
2022-01-28 18:37:23 +00:00
try {
sitems = await search . search ( {
index : 'item' ,
size : LIMIT ,
from : decodedCursor . offset ,
body : {
query : {
bool : {
2022-02-17 17:23:43 +00:00
must : [
sub
2022-02-26 16:41:30 +00:00
? { match : { 'sub.name' : sub } }
2022-02-17 17:23:43 +00:00
: { bool : { must _not : { exists : { field : 'sub.name' } } } } ,
2022-02-26 16:41:30 +00:00
me
? {
bool : {
should : [
{ match : { status : 'ACTIVE' } } ,
{ match : { userId : me . id } }
]
}
}
: { match : { status : 'ACTIVE' } } ,
2022-02-17 17:23:43 +00:00
{
bool : {
should : [
{
2022-02-01 22:27:10 +00:00
// all terms are matched in fields
2022-02-17 17:23:43 +00:00
multi _match : {
query ,
type : 'most_fields' ,
fields : [ 'title^20' , 'text' ] ,
minimum _should _match : '100%' ,
boost : 400
}
} ,
{
// all terms are matched in fields
multi _match : {
query ,
type : 'most_fields' ,
fields : [ 'title^20' , 'text' ] ,
fuzziness : 'AUTO' ,
prefix _length : 3 ,
minimum _should _match : '100%' ,
boost : 20
}
} ,
{
// only some terms must match
multi _match : {
query ,
type : 'most_fields' ,
fields : [ 'title^20' , 'text' ] ,
fuzziness : 'AUTO' ,
prefix _length : 3 ,
minimum _should _match : '60%'
}
2022-02-01 22:27:10 +00:00
}
2022-02-17 17:23:43 +00:00
// TODO: add wildcard matches for
// user.name and url
]
}
2022-01-28 18:37:23 +00:00
}
2022-02-17 17:23:43 +00:00
] ,
2022-01-28 18:37:23 +00:00
filter : {
range : {
createdAt : {
lte : decodedCursor . time
}
2022-01-26 15:35:14 +00:00
}
}
}
2022-02-03 19:41:09 +00:00
} ,
highlight : {
fields : {
2022-02-03 22:01:42 +00:00
title : { number _of _fragments : 0 , pre _tags : [ ':high[' ] , post _tags : [ ']' ] } ,
text : { number _of _fragments : 0 , pre _tags : [ ':high[' ] , post _tags : [ ']' ] }
2022-02-03 19:41:09 +00:00
}
2022-01-26 15:35:14 +00:00
}
}
2022-01-28 18:37:23 +00:00
} )
} catch ( e ) {
console . log ( e )
return {
cursor : null ,
items : [ ]
2022-01-26 15:35:14 +00:00
}
2022-01-28 18:37:23 +00:00
}
2022-01-26 15:35:14 +00:00
2022-02-03 19:41:09 +00:00
// return highlights
const items = sitems . body . hits . hits . map ( e => {
const item = e . _source
2022-02-03 22:01:42 +00:00
item . searchTitle = ( e . highlight . title && e . highlight . title [ 0 ] ) || item . title
item . searchText = ( e . highlight . text && e . highlight . text [ 0 ] ) || item . text
2022-02-03 19:41:09 +00:00
return item
} )
2022-01-26 15:35:14 +00:00
return {
cursor : items . length === LIMIT ? nextCursorEncoded ( decodedCursor ) : null ,
items
}
2022-02-17 17:23:43 +00:00
} ,
auctionPosition : async ( parent , { id , sub , bid } , { models } ) => {
2022-03-03 18:56:02 +00:00
// count items that have a bid gte to the current bid or
2022-02-17 17:23:43 +00:00
// gte current bid and older
const where = {
where : {
subName : sub ,
2022-02-26 16:41:30 +00:00
status : 'ACTIVE' ,
2022-03-03 18:56:02 +00:00
maxBid : {
gte : bid
}
2022-02-17 17:23:43 +00:00
}
}
if ( id ) {
where . where . id = { not : Number ( id ) }
}
return await models . item . count ( where ) + 1
2021-04-12 18:05:09 +00:00
}
} ,
Mutation : {
2022-04-18 22:10:26 +00:00
upsertLink : async ( parent , args , { me , models } ) => {
const { id , ... data } = args
data . url = ensureProtocol ( data . url )
2021-08-11 20:13:10 +00:00
2022-04-18 22:10:26 +00:00
if ( id ) {
const { forward , boost , ... remaining } = data
return await updateItem ( parent , { id , data : remaining } , { me , models } )
} else {
return await createItem ( parent , data , { me , models } )
2021-08-11 20:13:10 +00:00
}
} ,
2022-04-18 22:10:26 +00:00
upsertDiscussion : async ( parent , args , { me , models } ) => {
const { id , ... data } = args
2021-08-11 20:13:10 +00:00
2022-04-18 22:10:26 +00:00
if ( id ) {
const { forward , boost , ... remaining } = data
return await updateItem ( parent , { id , data : remaining } , { me , models } )
} else {
return await createItem ( parent , data , { me , models } )
2021-08-11 20:13:10 +00:00
}
} ,
2022-03-07 21:50:13 +00:00
upsertJob : async ( parent , { id , sub , title , company , location , remote , text , url , maxBid , status } , { me , models } ) => {
2022-02-17 17:23:43 +00:00
if ( ! me ) {
throw new AuthenticationError ( 'you must be logged in to create job' )
}
const fullSub = await models . sub . findUnique ( { where : { name : sub } } )
if ( ! fullSub ) {
throw new UserInputError ( 'not a valid sub' , { argumentName : 'sub' } )
}
if ( fullSub . baseCost > maxBid ) {
throw new UserInputError ( ` bid must be at least ${ fullSub . baseCost } ` , { argumentName : 'maxBid' } )
2022-03-03 18:56:02 +00:00
}
2022-03-07 21:50:13 +00:00
if ( ! location && ! remote ) {
throw new UserInputError ( 'must specify location or remote' , { argumentName : 'location' } )
}
2022-02-26 16:41:30 +00:00
const checkSats = async ( ) => {
// check if the user has the funds to run for the first minute
2022-03-07 21:50:13 +00:00
const minuteMsats = maxBid * 1000
2022-02-28 20:09:21 +00:00
const user = await models . user . findUnique ( { where : { id : me . id } } )
2022-02-26 16:41:30 +00:00
if ( user . msats < minuteMsats ) {
throw new UserInputError ( 'insufficient funds' )
}
}
2022-02-17 17:23:43 +00:00
const data = {
title ,
2022-03-07 21:50:13 +00:00
company ,
location : location . toLowerCase ( ) === 'remote' ? undefined : location ,
remote ,
2022-02-17 17:23:43 +00:00
text ,
url ,
maxBid ,
subName : sub ,
userId : me . id
}
if ( id ) {
2022-02-26 16:41:30 +00:00
if ( status ) {
data . status = status
// if the job is changing to active, we need to check they have funds
if ( status === 'ACTIVE' ) {
await checkSats ( )
}
}
2022-04-18 22:10:26 +00:00
const old = await models . item . findUnique ( { where : { id : Number ( id ) } } )
if ( Number ( old . userId ) !== Number ( me ? . id ) ) {
throw new AuthenticationError ( 'item does not belong to you' )
}
2022-02-17 17:23:43 +00:00
return await models . item . update ( {
where : { id : Number ( id ) } ,
data
} )
}
2022-02-26 16:41:30 +00:00
// before creating job, check the sats
await checkSats ( )
2022-02-17 17:23:43 +00:00
return await models . item . create ( {
data
} )
} ,
2021-04-14 00:57:32 +00:00
createComment : async ( parent , { text , parentId } , { me , models } ) => {
return await createItem ( parent , { text , parentId } , { me , models } )
2021-04-26 21:55:15 +00:00
} ,
2021-08-10 22:59:06 +00:00
updateComment : async ( parent , { id , text } , { me , models } ) => {
2021-08-18 22:20:33 +00:00
return await updateItem ( parent , { id , data : { text } } , { me , models } )
2021-08-10 22:59:06 +00:00
} ,
2022-01-20 23:04:12 +00:00
act : async ( parent , { id , sats } , { me , models } ) => {
2021-04-26 21:55:15 +00:00
// need to make sure we are logged in
if ( ! me ) {
2021-05-11 20:29:44 +00:00
throw new AuthenticationError ( 'you must be logged in' )
2021-04-26 21:55:15 +00:00
}
2021-04-27 21:30:58 +00:00
if ( sats <= 0 ) {
2021-05-11 20:29:44 +00:00
throw new UserInputError ( 'sats must be positive' , { argumentName : 'sats' } )
2021-04-27 21:30:58 +00:00
}
2022-01-20 23:04:12 +00:00
// disallow self tips
const [ item ] = await models . $queryRaw ( `
$ { SELECT }
FROM "Item"
WHERE id = $1 AND "userId" = $2 ` , Number(id), me.id)
if ( item ) {
throw new UserInputError ( 'cannot tip your self' )
2021-09-10 21:13:52 +00:00
}
2022-01-20 23:04:12 +00:00
const [ { item _act : vote } ] = await serialize ( models , models . $queryRaw ` SELECT item_act( ${ Number ( id ) } , ${ me . id } , 'TIP', ${ Number ( sats ) } ) ` )
2021-09-10 21:13:52 +00:00
return {
2022-01-20 23:04:12 +00:00
vote ,
sats
2021-09-10 21:13:52 +00:00
}
2021-04-12 18:05:09 +00:00
}
} ,
Item : {
2022-02-17 17:23:43 +00:00
sub : async ( item , args , { models } ) => {
if ( ! item . subName ) {
return null
}
return await models . sub . findUnique ( { where : { name : item . subName } } )
} ,
2022-01-07 16:32:31 +00:00
position : async ( item , args , { models } ) => {
if ( ! item . pinId ) {
return null
}
const pin = await models . pin . findUnique ( { where : { id : item . pinId } } )
if ( ! pin ) {
return null
}
return pin . position
} ,
2022-01-13 19:05:43 +00:00
prior : async ( item , args , { models } ) => {
if ( ! item . pinId ) {
return null
}
const prior = await models . item . findFirst ( {
where : {
pinId : item . pinId ,
createdAt : {
lt : item . createdAt
}
} ,
orderBy : {
createdAt : 'desc'
}
} )
if ( ! prior ) {
return null
}
return prior . id
} ,
2021-04-12 18:05:09 +00:00
user : async ( item , args , { models } ) =>
await models . user . findUnique ( { where : { id : item . userId } } ) ,
2022-04-19 18:32:39 +00:00
fwdUser : async ( item , args , { models } ) => {
if ( ! item . fwdUserId ) {
return null
}
return await models . user . findUnique ( { where : { id : item . fwdUserId } } )
} ,
2021-04-14 23:56:29 +00:00
ncomments : async ( item , args , { models } ) => {
const [ { count } ] = await models . $queryRaw `
SELECT count ( * )
FROM "Item"
2022-01-27 19:18:48 +00:00
WHERE path < @ text2ltree ( $ { item . path } ) AND id != $ { Number ( item . id ) } `
2021-05-11 15:52:50 +00:00
return count || 0
2021-04-14 23:56:29 +00:00
} ,
2022-03-04 18:05:16 +00:00
comments : async ( item , args , { models } ) => {
2022-03-31 18:05:11 +00:00
if ( item . comments ) {
return item . comments
}
2022-03-04 18:05:16 +00:00
return comments ( models , item . id , 'hot' )
} ,
2021-04-26 21:55:15 +00:00
sats : async ( item , args , { models } ) => {
2021-09-08 21:51:23 +00:00
const { sum : { sats } } = await models . itemAct . aggregate ( {
2021-04-26 21:55:15 +00:00
sum : {
sats : true
} ,
where : {
2022-01-27 19:18:48 +00:00
itemId : Number ( item . id ) ,
2022-01-20 23:04:12 +00:00
userId : {
2022-01-27 19:18:48 +00:00
not : Number ( item . userId )
2022-01-20 23:04:12 +00:00
} ,
act : {
not : 'BOOST'
}
2021-04-26 21:55:15 +00:00
}
} )
2021-05-11 15:52:50 +00:00
return sats || 0
2021-04-26 21:55:15 +00:00
} ,
2022-01-20 23:04:12 +00:00
upvotes : async ( item , args , { models } ) => {
2021-09-10 21:13:52 +00:00
const { sum : { sats } } = await models . itemAct . aggregate ( {
sum : {
sats : true
} ,
where : {
2022-01-27 19:18:48 +00:00
itemId : Number ( item . id ) ,
2022-01-20 23:04:12 +00:00
userId : {
2022-01-27 19:18:48 +00:00
not : Number ( item . userId )
2022-01-20 23:04:12 +00:00
} ,
act : 'VOTE'
2021-09-10 21:13:52 +00:00
}
} )
return sats || 0
} ,
2022-01-20 23:04:12 +00:00
boost : async ( item , args , { models } ) => {
2021-09-08 21:51:23 +00:00
const { sum : { sats } } = await models . itemAct . aggregate ( {
2021-04-26 21:55:15 +00:00
sum : {
sats : true
} ,
where : {
2022-01-27 19:18:48 +00:00
itemId : Number ( item . id ) ,
2022-01-20 23:04:12 +00:00
act : 'BOOST'
2021-04-26 21:55:15 +00:00
}
} )
2021-05-11 15:52:50 +00:00
return sats || 0
2021-07-08 00:15:27 +00:00
} ,
2021-12-05 17:37:55 +00:00
meSats : async ( item , args , { me , models } ) => {
2021-09-10 21:13:52 +00:00
if ( ! me ) return 0
const { sum : { sats } } = await models . itemAct . aggregate ( {
sum : {
sats : true
} ,
where : {
2022-01-27 19:18:48 +00:00
itemId : Number ( item . id ) ,
2021-09-10 21:13:52 +00:00
userId : me . id ,
2021-12-05 17:37:55 +00:00
OR : [
{
act : 'TIP'
} ,
{
act : 'VOTE'
}
]
2021-09-10 21:13:52 +00:00
}
} )
return sats || 0
} ,
2022-04-15 16:51:49 +00:00
meComments : async ( item , args , { me , models } ) => {
if ( ! me ) return 0
2022-04-17 13:13:52 +00:00
return await models . item . count ( { where : { userId : me . id , parentId : item . id } } )
2022-04-15 16:51:49 +00:00
} ,
2021-12-05 17:37:55 +00:00
mine : async ( item , args , { me , models } ) => {
return me ? . id === item . userId
} ,
2021-07-08 00:15:27 +00:00
root : async ( item , args , { models } ) => {
if ( ! item . parentId ) {
return null
}
return ( await models . $queryRaw ( `
$ { SELECT }
FROM "Item"
WHERE id = (
SELECT ltree2text ( subltree ( path , 0 , 1 ) ) : : integer
FROM "Item"
WHERE id = $1 ) ` , Number(item.id)))[0]
} ,
parent : async ( item , args , { models } ) => {
if ( ! item . parentId ) {
return null
}
return await models . item . findUnique ( { where : { id : item . parentId } } )
2021-04-26 21:55:15 +00:00
}
}
}
2021-08-18 22:20:33 +00:00
const namePattern = /\B@[\w_]+/gi
2021-09-23 17:42:00 +00:00
export const createMentions = async ( item , models ) => {
2021-08-18 22:20:33 +00:00
// if we miss a mention, in the rare circumstance there's some kind of
// failure, it's not a big deal so we don't do it transactionally
// ideally, we probably would
if ( ! item . text ) {
return
}
try {
2021-08-19 19:53:11 +00:00
const mentions = item . text . match ( namePattern ) ? . map ( m => m . slice ( 1 ) )
if ( mentions ? . length > 0 ) {
2021-08-18 22:20:33 +00:00
const users = await models . user . findMany ( {
where : {
name : { in : mentions }
}
} )
users . forEach ( async user => {
const data = {
itemId : item . id ,
userId : user . id
}
await models . mention . upsert ( {
where : {
itemId _userId : data
} ,
update : data ,
create : data
} )
} )
}
} catch ( e ) {
console . log ( 'mention failure' , e )
}
}
const updateItem = async ( parent , { id , data } , { me , models } ) => {
2022-04-18 22:10:26 +00:00
// update iff this item belongs to me
const old = await models . item . findUnique ( { where : { id : Number ( id ) } } )
if ( Number ( old . userId ) !== Number ( me ? . id ) ) {
throw new AuthenticationError ( 'item does not belong to you' )
}
// if it's not the FAQ and older than 10 minutes
if ( old . id !== 349 && Date . now ( ) > new Date ( old . createdAt ) . getTime ( ) + 10 * 60000 ) {
throw new UserInputError ( 'item can no longer be editted' )
}
2021-08-18 22:20:33 +00:00
const item = await models . item . update ( {
where : { id : Number ( id ) } ,
data
} )
await createMentions ( item , models )
return item
}
2022-04-18 22:10:26 +00:00
const createItem = async ( parent , { title , url , text , boost , forward , parentId } , { me , models } ) => {
2021-04-26 21:55:15 +00:00
if ( ! me ) {
2021-05-11 20:29:44 +00:00
throw new AuthenticationError ( 'you must be logged in' )
2021-04-26 21:55:15 +00:00
}
2022-03-09 19:44:50 +00:00
if ( boost && boost < BOOST _MIN ) {
throw new UserInputError ( ` boost must be at least ${ BOOST _MIN } ` , { argumentName : 'boost' } )
2021-09-11 21:52:19 +00:00
}
2022-04-19 18:32:39 +00:00
let fwdUser
if ( forward ) {
fwdUser = await models . user . findUnique ( { where : { name : forward } } )
if ( ! fwdUser ) {
throw new UserInputError ( 'forward user does not exist' , { argumentName : 'forward' } )
}
}
2021-09-11 21:52:19 +00:00
const [ item ] = await serialize ( models ,
2021-09-14 17:55:59 +00:00
models . $queryRaw ( ` ${ SELECT } FROM create_item( $ 1, $ 2, $ 3, $ 4, $ 5, $ 6) AS "Item" ` ,
title , url , text , Number ( boost || 0 ) , Number ( parentId ) , Number ( me . id ) ) )
2021-08-18 22:20:33 +00:00
await createMentions ( item , models )
2022-04-19 18:32:39 +00:00
if ( fwdUser ) {
await models . item . update ( {
where : { id : item . id } ,
data : {
fwdUserId : fwdUser . id
}
} )
}
2021-05-20 01:09:32 +00:00
item . comments = [ ]
return item
2021-04-26 21:55:15 +00:00
}
function nestComments ( flat , parentId ) {
const result = [ ]
let added = 0
for ( let i = 0 ; i < flat . length ; ) {
if ( ! flat [ i ] . comments ) flat [ i ] . comments = [ ]
if ( Number ( flat [ i ] . parentId ) === Number ( parentId ) ) {
result . push ( flat [ i ] )
added ++
i ++
} else if ( result . length > 0 ) {
const item = result [ result . length - 1 ]
const [ nested , newAdded ] = nestComments ( flat . slice ( i ) , item . id )
if ( newAdded === 0 ) {
break
}
item . comments . push ( ... nested )
i += newAdded
added += newAdded
} else {
break
}
2021-04-12 18:05:09 +00:00
}
2021-04-26 21:55:15 +00:00
return [ result , added ]
2021-04-12 18:05:09 +00:00
}
2021-04-26 21:55:15 +00:00
// we have to do our own query because ltree is unsupported
2021-09-23 17:42:00 +00:00
export const SELECT =
2021-04-27 21:30:58 +00:00
` SELECT "Item".id, "Item".created_at as "createdAt", "Item".updated_at as "updatedAt", "Item".title,
2022-04-19 18:32:39 +00:00
"Item" . text , "Item" . url , "Item" . "userId" , "Item" . "fwdUserId" , "Item" . "parentId" , "Item" . "pinId" , "Item" . "maxBid" ,
2022-03-07 21:50:13 +00:00
"Item" . company , "Item" . location , "Item" . remote ,
2022-02-26 16:41:30 +00:00
"Item" . "subName" , "Item" . status , ltree2text ( "Item" . "path" ) AS "path" `
2021-04-27 21:30:58 +00:00
2022-04-02 22:39:15 +00:00
function newTimedOrderByWeightedSats ( num ) {
return `
2022-07-01 18:38:47 +00:00
ORDER BY ( POWER ( "Item" . "weightedVotes" , 1.2 ) / POWER ( EXTRACT ( EPOCH FROM ( $$ { num } - "Item" . created _at ) ) / 3600 + 2 , 1.3 ) +
2022-05-17 19:54:12 +00:00
GREATEST ( "Item" . boost - 1000 + 5 , 0 ) / POWER ( EXTRACT ( EPOCH FROM ( $$ { num } - "Item" . created _at ) ) / 3600 + 2 , 4 ) ) DESC NULLS LAST , "Item" . id DESC `
2022-01-17 22:38:40 +00:00
}
2022-05-17 19:54:12 +00:00
const TOP _ORDER _BY _SATS = 'ORDER BY "Item"."weightedVotes" DESC NULLS LAST, "Item".id DESC'