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-04-12 18:05:09 +00:00
2021-06-22 18:14:08 +00:00
const LIMIT = 21
2021-06-22 17:47:49 +00:00
2021-04-29 15:56:28 +00:00
async function comments ( models , id ) {
const flat = await models . $queryRaw ( `
WITH RECURSIVE base AS (
$ { SELECT } , ARRAY [ row _number ( ) OVER ( $ { ORDER _BY _SATS } , "Item" . path ) ] AS sort _path
FROM "Item"
$ { LEFT _JOIN _SATS }
2021-05-11 20:29:44 +00:00
WHERE "parentId" = $1
2021-04-29 15:56:28 +00:00
UNION ALL
$ { SELECT } , p . sort _path || row _number ( ) OVER ( $ { ORDER _BY _SATS } , "Item" . path )
FROM base p
JOIN "Item" ON ltree2text ( subpath ( "Item" . "path" , 0 , - 1 ) ) = p . "path"
$ { LEFT _JOIN _SATS } )
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 ]
}
2021-06-22 17:47:49 +00:00
function decodeCursor ( cursor ) {
if ( ! cursor ) {
2021-07-09 19:16:26 +00:00
return { offset : 0 , time : new Date ( ) }
2021-06-22 17:47:49 +00:00
} else {
const res = JSON . parse ( Buffer . from ( cursor , 'base64' ) )
res . time = new Date ( res . time )
return res
}
}
function nextCursorEncoded ( cursor ) {
cursor . offset += LIMIT
return Buffer . from ( JSON . stringify ( cursor ) ) . toString ( 'base64' )
}
2021-04-12 18:05:09 +00:00
export default {
Query : {
2021-06-24 23:56:01 +00:00
moreItems : async ( parent , { sort , cursor , userId } , { me , models } ) => {
2021-06-22 17:47:49 +00:00
const decodedCursor = decodeCursor ( cursor )
2021-06-24 23:56:01 +00:00
let items
switch ( sort ) {
case 'user' :
items = await models . $queryRaw ( `
$ { SELECT }
FROM "Item"
WHERE "userId" = $1 AND "parentId" IS NULL AND created _at <= $2
ORDER BY created _at DESC
OFFSET $3
LIMIT $ { LIMIT } ` , Number(userId), decodedCursor.time, decodedCursor.offset)
break
case 'hot' :
items = await models . $queryRaw ( `
2021-06-22 17:47:49 +00:00
$ { SELECT }
FROM "Item"
$ { timedLeftJoinSats ( 1 ) }
WHERE "parentId" IS NULL AND created _at <= $1
$ { timedOrderBySats ( 1 ) }
OFFSET $2
LIMIT $ { LIMIT } ` , decodedCursor.time, decodedCursor.offset)
2021-06-24 23:56:01 +00:00
break
default :
items = await models . $queryRaw ( `
2021-06-22 17:47:49 +00:00
$ { SELECT }
FROM "Item"
WHERE "parentId" IS NULL AND created _at <= $1
ORDER BY created _at DESC
OFFSET $2
LIMIT $ { LIMIT } ` , decodedCursor.time, decodedCursor.offset)
2021-06-24 23:56:01 +00:00
break
}
2021-06-22 17:47:49 +00:00
return {
cursor : items . length === LIMIT ? nextCursorEncoded ( decodedCursor ) : null ,
items
}
2021-04-24 21:05:07 +00:00
} ,
2021-06-24 23:56:01 +00:00
moreFlatComments : async ( parent , { cursor , userId } , { me , models } ) => {
const decodedCursor = decodeCursor ( cursor )
let comments
if ( userId ) {
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 } ` , Number(userId), decodedCursor.time, decodedCursor.offset)
} else {
if ( ! me ) {
throw new AuthenticationError ( 'you must be logged in' )
}
comments = await models . $queryRaw ( `
$ { SELECT }
From "Item"
JOIN "Item" p ON "Item" . "parentId" = p . id AND p . "userId" = $1
AND "Item" . "userId" < > $1 AND "Item" . created _at <= $2
ORDER BY "Item" . created _at DESC
OFFSET $3
2021-06-27 03:09:39 +00:00
LIMIT $ { LIMIT } ` , me.id, decodedCursor.time, decodedCursor.offset)
2021-06-24 23:56:01 +00:00
}
return {
cursor : comments . length === LIMIT ? nextCursorEncoded ( decodedCursor ) : null ,
comments
}
} ,
notifications : async ( parent , args , { me , models } ) => {
if ( ! me ) {
throw new AuthenticationError ( 'you must be logged in' )
}
return await models . $queryRaw ( `
$ { SELECT }
From "Item"
JOIN "Item" p ON "Item" . "parentId" = p . id AND p . "userId" = $1
AND "Item" . "userId" < > $1
2021-06-27 03:09:39 +00:00
ORDER BY "Item" . created _at DESC ` , me.id)
2021-06-24 23:56:01 +00:00
} ,
2021-04-14 23:56:29 +00:00
item : async ( parent , { id } , { models } ) => {
2021-05-11 20:29:44 +00:00
const [ item ] = await models . $queryRaw ( `
2021-04-22 22:14:32 +00:00
$ { SELECT }
2021-04-14 23:56:29 +00:00
FROM "Item"
2021-05-11 20:29:44 +00:00
WHERE id = $1 ` , Number(id))
2021-04-29 15:56:28 +00:00
item . comments = comments ( models , id )
return item
2021-04-14 23:56:29 +00:00
} ,
2021-04-22 22:14:32 +00:00
userComments : async ( parent , { userId } , { models } ) => {
return await models . $queryRaw ( `
$ { SELECT }
FROM "Item"
2021-05-11 20:29:44 +00:00
WHERE "userId" = $1 AND "parentId" IS NOT NULL
ORDER BY created _at DESC ` , Number(userId))
2021-04-12 18:05:09 +00:00
}
} ,
Mutation : {
2021-04-14 00:57:32 +00:00
createLink : async ( parent , { title , url } , { me , models } ) => {
if ( ! title ) {
2021-05-11 20:29:44 +00:00
throw new UserInputError ( 'link must have title' , { argumentName : 'title' } )
2021-04-12 18:05:09 +00:00
}
2021-04-14 00:57:32 +00:00
if ( ! url ) {
2021-05-11 20:29:44 +00:00
throw new UserInputError ( 'link must have url' , { argumentName : 'url' } )
2021-04-12 18:05:09 +00:00
}
2021-05-20 19:11:58 +00:00
return await createItem ( parent , { title , url : ensureProtocol ( url ) } , { me , models } )
2021-04-14 00:57:32 +00:00
} ,
createDiscussion : async ( parent , { title , text } , { me , models } ) => {
if ( ! title ) {
2021-05-11 20:29:44 +00:00
throw new UserInputError ( 'link must have title' , { argumentName : 'title' } )
2021-04-12 18:05:09 +00:00
}
2021-04-14 00:57:32 +00:00
return await createItem ( parent , { title , text } , { me , models } )
} ,
createComment : async ( parent , { text , parentId } , { me , models } ) => {
if ( ! text ) {
2021-05-11 20:29:44 +00:00
throw new UserInputError ( 'comment must have text' , { argumentName : 'text' } )
2021-04-14 00:57:32 +00:00
}
if ( ! parentId ) {
2021-05-11 20:29:44 +00:00
throw new UserInputError ( 'comment must have parent' , { argumentName : 'text' } )
2021-04-12 18:05:09 +00:00
}
2021-04-14 00:57:32 +00:00
return await createItem ( parent , { text , parentId } , { me , models } )
2021-04-26 21:55:15 +00:00
} ,
vote : async ( parent , { id , sats = 1 } , { me , models } ) => {
// 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
}
2021-05-20 01:09:32 +00:00
await serialize ( models , models . $queryRaw ` SELECT vote( ${ Number ( id ) } , ${ me . name } , ${ Number ( sats ) } ) ` )
2021-04-26 21:55:15 +00:00
return sats
2021-04-12 18:05:09 +00:00
}
} ,
Item : {
user : async ( item , args , { models } ) =>
await models . user . findUnique ( { where : { id : item . userId } } ) ,
2021-04-14 23:56:29 +00:00
ncomments : async ( item , args , { models } ) => {
const [ { count } ] = await models . $queryRaw `
SELECT count ( * )
FROM "Item"
2021-04-15 19:41:02 +00:00
WHERE path < @ text2ltree ( $ { item . path } ) AND id != $ { item . id } `
2021-05-11 15:52:50 +00:00
return count || 0
2021-04-14 23:56:29 +00:00
} ,
2021-04-26 21:55:15 +00:00
sats : async ( item , args , { models } ) => {
const { sum : { sats } } = await models . vote . aggregate ( {
sum : {
sats : true
} ,
where : {
2021-04-27 21:30:58 +00:00
itemId : item . id ,
boost : false
}
} )
2021-05-11 15:52:50 +00:00
return sats || 0
2021-04-27 21:30:58 +00:00
} ,
boost : async ( item , args , { models } ) => {
const { sum : { sats } } = await models . vote . aggregate ( {
sum : {
sats : true
} ,
where : {
itemId : item . id ,
boost : true
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
} ,
meSats : async ( item , args , { me , models } ) => {
if ( ! me ) return 0
const { sum : { sats } } = await models . vote . aggregate ( {
sum : {
sats : true
} ,
where : {
itemId : item . id ,
2021-06-27 03:09:39 +00:00
userId : me . id
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
} ,
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-05-11 20:29:44 +00:00
const createItem = async ( parent , { title , url , text , 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
}
2021-05-20 01:09:32 +00:00
const [ item ] = await serialize ( models , models . $queryRaw (
` ${ SELECT } FROM create_item( $ 1, $ 2, $ 3, $ 4, $ 5) AS "Item" ` ,
title , url , text , Number ( parentId ) , me . name ) )
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
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,
"Item" . text , "Item" . url , "Item" . "userId" , "Item" . "parentId" , ltree2text ( "Item" . "path" ) AS "path" `
2021-07-09 19:12:35 +00:00
const LEFT _JOIN _SATS _SELECT = 'SELECT i.id, SUM(CASE WHEN "Vote".boost THEN 0 ELSE "Vote".sats END) as sats, SUM(CASE WHEN "Vote".boost THEN "Vote".sats ELSE 0 END) as boost'
2021-06-22 17:47:49 +00:00
function timedLeftJoinSats ( num ) {
2021-07-09 19:12:35 +00:00
return ` LEFT JOIN ( ${ LEFT _JOIN _SATS _SELECT }
2021-06-22 17:47:49 +00:00
FROM "Item" i
JOIN "Vote" ON i . id = "Vote" . "itemId" AND "Vote" . created _at <= $$ { num }
GROUP BY i . id ) x ON "Item" . id = x . id `
}
2021-04-27 21:30:58 +00:00
const LEFT _JOIN _SATS =
2021-07-09 19:12:35 +00:00
` LEFT JOIN ( ${ LEFT _JOIN _SATS _SELECT }
2021-04-27 21:30:58 +00:00
FROM "Item" i
JOIN "Vote" ON i . id = "Vote" . "itemId"
GROUP BY i . id ) x ON "Item" . id = x . id `
2021-06-22 17:47:49 +00:00
function timedOrderBySats ( num ) {
2021-07-09 19:12:35 +00:00
return ` ORDER BY ((x.sats-1)/POWER(EXTRACT(EPOCH FROM ( $ ${ num } - "Item".created_at))/3600+2, 1.5) +
2021-07-10 13:03:37 +00:00
( x . boost ) / POWER ( EXTRACT ( EPOCH FROM ( $$ { num } - "Item" . created _at ) ) / 3600 + 2 , 5 ) ) DESC NULLS LAST `
2021-06-22 17:47:49 +00:00
}
2021-04-27 21:30:58 +00:00
const ORDER _BY _SATS =
2021-07-09 19:12:35 +00:00
` ORDER BY ((x.sats-1)/POWER(EXTRACT(EPOCH FROM ((NOW() AT TIME ZONE 'UTC') - "Item".created_at))/3600+2, 1.5) +
2021-07-10 13:03:37 +00:00
( x . boost ) / POWER ( EXTRACT ( EPOCH FROM ( ( NOW ( ) AT TIME ZONE 'UTC' ) - "Item" . created _at ) ) / 3600 + 2 , 5 ) ) DESC NULLS LAST `