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-09-21 19:57:36 +00:00
import {
BOOST _MIN , ITEM _SPAM _INTERVAL , MAX _POLL _NUM _CHOICES ,
MAX _TITLE _LENGTH , ITEM _FILTER _THRESHOLD , DONT _LIKE _THIS _COST
} from '../../lib/constants'
2022-11-15 20:51:55 +00:00
import { msatsToSats } from '../../lib/format'
2021-06-22 17:47:49 +00:00
2022-09-21 19:57:36 +00:00
async function comments ( me , models , id , sort ) {
2021-12-21 21:29:42 +00:00
let orderBy
switch ( sort ) {
case 'top' :
2022-09-21 19:57:36 +00:00
orderBy = ` ORDER BY ${ await orderByNumerator ( me , models ) } 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-10-31 17:56:48 +00:00
orderBy = ` ORDER BY ${ await orderByNumerator ( me , models ) } /POWER(GREATEST(3, EXTRACT(EPOCH FROM (now_utc() - "Item".created_at))/3600), 1.3) DESC NULLS LAST, "Item".id DESC `
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
2022-09-21 19:57:36 +00:00
$ { await filterClause ( me , models ) }
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-09-21 19:57:36 +00:00
JOIN "Item" ON "Item" . "parentId" = p . id
WHERE true
$ { await filterClause ( me , models ) } )
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-09-21 19:57:36 +00:00
export async function getItem ( parent , { id } , { me , models } ) {
2021-09-23 17:42:00 +00:00
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 ) {
2022-10-25 21:35:32 +00:00
case 'forever' :
interval = ''
2021-12-16 23:05:31 +00:00
break
case 'week' :
interval += "'7 days'"
break
case 'month' :
interval += "'1 month'"
break
case 'year' :
interval += "'1 year'"
break
default :
2022-10-25 21:35:32 +00:00
interval += "'1 day'"
2021-12-16 23:05:31 +00:00
break
}
return interval
}
2022-10-25 21:35:32 +00:00
async function topOrderClause ( sort , me , models ) {
switch ( sort ) {
case 'comments' :
return 'ORDER BY ncomments DESC'
case 'sats' :
2022-11-15 20:51:55 +00:00
return 'ORDER BY msats DESC'
2022-10-25 21:35:32 +00:00
default :
return await topOrderByWeightedSats ( me , models )
}
}
2022-09-21 19:57:36 +00:00
export async function orderByNumerator ( me , models ) {
if ( me ) {
const user = await models . user . findUnique ( { where : { id : me . id } } )
if ( user . wildWestMode ) {
return 'GREATEST("Item"."weightedVotes", POWER("Item"."weightedVotes", 1.2))'
}
}
return ` (CASE WHEN "Item"."weightedVotes" > "Item"."weightedDownVotes"
THEN 1
ELSE - 1 END
* GREATEST ( ABS ( "Item" . "weightedVotes" - "Item" . "weightedDownVotes" ) , POWER ( ABS ( "Item" . "weightedVotes" - "Item" . "weightedDownVotes" ) , 1.2 ) ) ) `
}
export async function filterClause ( me , models ) {
2022-09-27 21:19:15 +00:00
// by default don't include freebies unless they have upvotes
let clause = ' AND (NOT "Item".freebie OR "Item"."weightedVotes" - "Item"."weightedDownVotes" > 0'
2022-09-21 19:57:36 +00:00
if ( me ) {
const user = await models . user . findUnique ( { where : { id : me . id } } )
2022-09-27 21:19:15 +00:00
// wild west mode has everything
2022-09-21 19:57:36 +00:00
if ( user . wildWestMode ) {
return ''
}
2022-09-27 21:19:15 +00:00
// greeter mode includes freebies if feebies haven't been flagged
if ( user . greeterMode ) {
clause = 'AND (NOT "Item".freebie OR ("Item"."weightedVotes" - "Item"."weightedDownVotes" >= 0 AND "Item".freebie)'
}
// always include if it's mine
clause += ` OR "Item"."userId" = ${ me . id } ) `
} else {
// close default freebie clause
clause += ')'
2022-09-21 19:57:36 +00:00
}
// if the item is above the threshold or is mine
2022-09-27 21:19:15 +00:00
clause += ` AND ("Item"."weightedVotes" - "Item"."weightedDownVotes" > - ${ ITEM _FILTER _THRESHOLD } `
2022-09-21 19:57:36 +00:00
if ( me ) {
clause += ` OR "Item"."userId" = ${ me . id } `
}
clause += ')'
return clause
}
2022-12-01 22:22:13 +00:00
function recentClause ( type ) {
switch ( type ) {
case 'links' :
return ' AND url IS NOT NULL'
case 'discussions' :
return ' AND url IS NULL AND bio = false AND "pollCost" IS NULL'
case 'polls' :
return ' AND "pollCost" IS NOT NULL'
case 'bios' :
return ' AND bio = true'
default :
return ''
}
}
2021-04-12 18:05:09 +00:00
export default {
Query : {
2022-08-10 15:06:31 +00:00
itemRepetition : async ( parent , { parentId } , { me , models } ) => {
if ( ! me ) return 0
// how many of the parents starting at parentId belong to me
2022-09-01 21:06:11 +00:00
const [ { item _spam : count } ] = await models . $queryRaw ( ` SELECT item_spam( $ 1, $ 2, ' ${ ITEM _SPAM _INTERVAL } ') ` ,
Number ( parentId ) , Number ( me . id ) )
2022-08-10 15:06:31 +00:00
return count
} ,
2022-10-25 21:35:32 +00:00
topItems : async ( parent , { cursor , sort , when } , { me , models } ) => {
const decodedCursor = decodeCursor ( cursor )
const items = await models . $queryRaw ( `
$ { SELECT }
FROM "Item"
WHERE "parentId" IS NULL AND "Item" . created _at <= $1
AND "pinId" IS NULL
$ { topClause ( when ) }
$ { await filterClause ( me , models ) }
$ { await topOrderClause ( sort , me , models ) }
OFFSET $2
LIMIT $ { LIMIT } ` , decodedCursor.time, decodedCursor.offset)
return {
cursor : items . length === LIMIT ? nextCursorEncoded ( decodedCursor ) : null ,
items
}
} ,
topComments : async ( parent , { cursor , sort , when } , { me , models } ) => {
const decodedCursor = decodeCursor ( cursor )
const comments = await models . $queryRaw ( `
$ { SELECT }
FROM "Item"
WHERE "parentId" IS NOT NULL
AND "Item" . created _at <= $1
$ { topClause ( when ) }
$ { await filterClause ( me , models ) }
$ { await topOrderClause ( sort , me , models ) }
OFFSET $2
LIMIT $ { LIMIT } ` , decodedCursor.time, decodedCursor.offset)
return {
cursor : comments . length === LIMIT ? nextCursorEncoded ( decodedCursor ) : null ,
comments
}
} ,
2022-12-01 22:22:13 +00:00
items : async ( parent , { sub , sort , type , 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 ( ) }
2022-09-21 19:57:36 +00:00
$ { await filterClause ( me , models ) }
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 ( ) }
2022-09-21 19:57:36 +00:00
$ { await filterClause ( me , models ) }
2022-12-01 22:22:13 +00:00
$ { recentClause ( type ) }
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-09-21 19:57:36 +00:00
$ { await filterClause ( me , models ) }
$ { await topOrderByWeightedSats ( me , models ) }
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' :
items = await models . $queryRaw ( `
2022-07-21 22:55:05 +00:00
SELECT *
FROM (
( $ { SELECT }
FROM "Item"
WHERE "parentId" IS NULL AND created _at <= $1
AND "pinId" IS NULL
$ { subClause ( 3 ) }
2022-09-29 20:42:33 +00:00
AND status = 'ACTIVE' AND "maxBid" > 0
2022-07-21 22:55:05 +00:00
ORDER BY "maxBid" DESC , created _at ASC )
UNION ALL
( $ { SELECT }
FROM "Item"
WHERE "parentId" IS NULL AND created _at <= $1
AND "pinId" IS NULL
$ { subClause ( 3 ) }
2022-09-29 20:42:33 +00:00
AND ( ( status = 'ACTIVE' AND "maxBid" = 0 ) OR status = 'NOSATS' )
2022-07-21 22:55:05 +00:00
ORDER BY created _at DESC )
) a
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-09-27 21:19:15 +00:00
AND "pinId" IS NULL AND NOT bio
2022-02-28 20:09:21 +00:00
$ { subClause ( 4 ) }
2022-09-21 19:57:36 +00:00
$ { await filterClause ( me , models ) }
$ { await newTimedOrderByWeightedSats ( me , models , 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-09-27 21:19:15 +00:00
AND "pinId" IS NULL AND NOT bio
2022-02-28 20:09:21 +00:00
$ { subClause ( 3 ) }
2022-09-21 19:57:36 +00:00
$ { await filterClause ( me , models ) }
$ { await newTimedOrderByWeightedSats ( me , models , 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-09-21 19:57:36 +00:00
allItems : async ( parent , { cursor } , { me , models } ) => {
2022-01-25 19:34:51 +00:00
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
}
} ,
2022-09-22 18:44:50 +00:00
outlawedItems : async ( parent , { cursor } , { me , models } ) => {
const decodedCursor = decodeCursor ( cursor )
const notMine = ( ) => {
return me ? ` AND "userId" <> ${ me . id } ` : ''
}
const items = await models . $queryRaw ( `
$ { SELECT }
FROM "Item"
WHERE "Item" . "weightedVotes" - "Item" . "weightedDownVotes" <= - $ { ITEM _FILTER _THRESHOLD }
$ { notMine ( ) }
ORDER BY created _at DESC
OFFSET $1
LIMIT $ { LIMIT } ` , decodedCursor.offset)
return {
cursor : items . length === LIMIT ? nextCursorEncoded ( decodedCursor ) : null ,
items
}
} ,
2022-09-22 20:42:04 +00:00
borderlandItems : async ( parent , { cursor } , { me , models } ) => {
const decodedCursor = decodeCursor ( cursor )
const notMine = ( ) => {
return me ? ` AND "userId" <> ${ me . id } ` : ''
}
const items = await models . $queryRaw ( `
$ { SELECT }
FROM "Item"
WHERE "Item" . "weightedVotes" - "Item" . "weightedDownVotes" < 0
AND "Item" . "weightedVotes" - "Item" . "weightedDownVotes" > - $ { ITEM _FILTER _THRESHOLD }
$ { notMine ( ) }
ORDER BY created _at DESC
OFFSET $1
LIMIT $ { LIMIT } ` , decodedCursor.offset)
return {
cursor : items . length === LIMIT ? nextCursorEncoded ( decodedCursor ) : null ,
items
}
} ,
2022-09-27 21:19:15 +00:00
freebieItems : async ( parent , { cursor } , { me , models } ) => {
const decodedCursor = decodeCursor ( cursor )
const items = await models . $queryRaw ( `
$ { SELECT }
FROM "Item"
WHERE "Item" . freebie
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 ) {
2022-08-18 22:05:58 +00:00
case 'recent' :
comments = await models . $queryRaw ( `
$ { SELECT }
FROM "Item"
WHERE "parentId" IS NOT NULL AND created _at <= $1
2022-09-21 19:57:36 +00:00
$ { await filterClause ( me , models ) }
2022-08-18 22:05:58 +00:00
ORDER BY created _at DESC
OFFSET $2
LIMIT $ { LIMIT } ` , decodedCursor.time, decodedCursor.offset)
break
2021-12-16 23:05:31 +00:00
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
2022-09-21 19:57:36 +00:00
$ { await filterClause ( me , models ) }
2021-12-16 23:05:31 +00:00
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-09-21 19:57:36 +00:00
$ { await filterClause ( me , models ) }
$ { await topOrderByWeightedSats ( me , models ) }
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
} ,
2022-09-21 19:57:36 +00:00
comments : async ( parent , { id , sort } , { me , models } ) => {
return comments ( me , models , id , sort )
2022-01-26 15:35:14 +00:00
} ,
2022-09-29 20:42:33 +00:00
auctionPosition : async ( parent , { id , sub , bid } , { models , me } ) => {
2022-10-05 18:55:30 +00:00
const createdAt = id ? ( await getItem ( parent , { id } , { models , me } ) ) . createdAt : new Date ( )
let where
2022-09-29 20:42:33 +00:00
if ( bid > 0 ) {
2022-10-05 18:55:30 +00:00
// if there's a bid
// it's ACTIVE and has a larger bid than ours, or has an equal bid and is older
// count items: (bid > ours.bid OR (bid = ours.bid AND create_at < ours.created_at)) AND status = 'ACTIVE'
where = {
status : 'ACTIVE' ,
OR : [
{ maxBid : { gt : bid } } ,
{ maxBid : bid , createdAt : { lt : createdAt } }
]
}
2022-09-29 20:42:33 +00:00
} else {
2022-10-05 18:55:30 +00:00
// else
// it's an active with a bid gt ours, or its newer than ours and not STOPPED
// count items: ((bid > ours.bid AND status = 'ACTIVE') OR (created_at > ours.created_at AND status <> 'STOPPED'))
where = {
OR : [
{ maxBid : { gt : 0 } , status : 'ACTIVE' } ,
{ createdAt : { gt : createdAt } , status : { not : 'STOPPED' } }
]
}
2022-09-29 20:42:33 +00:00
}
2022-10-05 18:55:30 +00:00
where . subName = sub
2022-02-17 17:23:43 +00:00
if ( id ) {
2022-10-05 18:55:30 +00:00
where . id = { not : Number ( id ) }
2022-02-17 17:23:43 +00:00
}
2022-10-05 18:55:30 +00:00
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 ) {
2022-08-18 18:15:24 +00:00
return await updateItem ( parent , { id , data } , { me , models } )
2022-04-18 22:10:26 +00:00
} 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 ) {
2022-08-18 18:15:24 +00:00
return await updateItem ( parent , { id , data } , { me , models } )
2022-04-18 22:10:26 +00:00
} else {
return await createItem ( parent , data , { me , models } )
2021-08-11 20:13:10 +00:00
}
} ,
2022-07-30 13:25:46 +00:00
upsertPoll : async ( parent , { id , forward , boost , title , text , options } , { me , models } ) => {
if ( ! me ) {
throw new AuthenticationError ( 'you must be logged in' )
}
if ( boost && boost < BOOST _MIN ) {
throw new UserInputError ( ` boost must be at least ${ BOOST _MIN } ` , { argumentName : 'boost' } )
}
2022-08-18 18:15:24 +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' } )
}
}
2022-07-30 13:25:46 +00:00
2022-08-18 18:15:24 +00:00
if ( id ) {
const optionCount = await models . pollOption . count ( {
where : {
itemId : Number ( id )
}
2022-07-30 13:25:46 +00:00
} )
2022-08-18 18:15:24 +00:00
if ( options . length + optionCount > MAX _POLL _NUM _CHOICES ) {
throw new UserInputError ( ` total choices must be < ${ MAX _POLL _NUM _CHOICES } ` , { argumentName : 'options' } )
}
const [ item ] = await serialize ( models ,
2022-09-27 21:19:15 +00:00
models . $queryRaw ( ` ${ SELECT } FROM update_poll( $ 1, $ 2, $ 3, $ 4, $ 5, $ 6) AS "Item" ` ,
Number ( id ) , title , text , Number ( boost || 0 ) , options , Number ( fwdUser ? . id ) ) )
2022-08-18 18:15:24 +00:00
2022-07-30 13:25:46 +00:00
return item
} else {
2022-08-18 18:15:24 +00:00
if ( options . length < 2 || options . length > MAX _POLL _NUM _CHOICES ) {
throw new UserInputError ( ` choices must be >2 and < ${ MAX _POLL _NUM _CHOICES } ` , { argumentName : 'options' } )
2022-07-30 13:25:46 +00:00
}
const [ item ] = await serialize ( models ,
2022-09-27 21:19:15 +00:00
models . $queryRaw ( ` ${ SELECT } FROM create_poll( $ 1, $ 2, $ 3, $ 4, $ 5, $ 6, $ 7, ' ${ ITEM _SPAM _INTERVAL } ') AS "Item" ` ,
title , text , 1 , Number ( boost || 0 ) , Number ( me . id ) , options , Number ( fwdUser ? . id ) ) )
2022-08-18 18:15:24 +00:00
await createMentions ( item , models )
2022-07-30 13:25:46 +00:00
item . comments = [ ]
return item
}
} ,
2022-07-21 22:55:05 +00:00
upsertJob : async ( parent , {
id , sub , title , company , location , remote ,
text , url , maxBid , status , logo
} , { 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' } )
}
2022-09-29 20:42:33 +00:00
if ( maxBid < 0 ) {
throw new UserInputError ( 'bid must be at least 0' , { 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-09-29 20:42:33 +00:00
location = location . toLowerCase ( ) === 'remote' ? undefined : location
2022-02-17 17:23:43 +00:00
2022-09-29 20:42:33 +00:00
let item
2022-02-17 17:23:43 +00:00
if ( id ) {
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-09-29 20:42:33 +00:00
( [ item ] = await serialize ( models ,
models . $queryRaw (
` ${ SELECT } FROM update_job( $ 1, $ 2, $ 3, $ 4, $ 5, $ 6, $ 7, $ 8, $ 9, $ 10) AS "Item" ` ,
Number ( id ) , title , url , text , Number ( maxBid ) , company , location , remote , Number ( logo ) , status ) ) )
} else {
( [ item ] = await serialize ( models ,
models . $queryRaw (
` ${ SELECT } FROM create_job( $ 1, $ 2, $ 3, $ 4, $ 5, $ 6, $ 7, $ 8, $ 9) AS "Item" ` ,
title , url , text , Number ( me . id ) , Number ( maxBid ) , company , location , remote , Number ( logo ) ) ) )
2022-02-17 17:23:43 +00:00
}
2022-09-29 20:42:33 +00:00
await createMentions ( item , models )
return item
2022-02-17 17:23:43 +00:00
} ,
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-07-30 13:25:46 +00:00
pollVote : async ( parent , { id } , { me , models } ) => {
if ( ! me ) {
throw new AuthenticationError ( 'you must be logged in' )
}
await serialize ( models ,
models . $queryRaw ( ` ${ SELECT } FROM poll_vote( $ 1, $ 2) AS "Item" ` ,
Number ( id ) , Number ( me . id ) ) )
return id
} ,
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
}
2022-09-21 19:57:36 +00:00
} ,
dontLikeThis : async ( parent , { id } , { me , models } ) => {
// need to make sure we are logged in
if ( ! me ) {
throw new AuthenticationError ( 'you must be logged in' )
}
// disallow self down votes
const [ item ] = await models . $queryRaw ( `
$ { SELECT }
FROM "Item"
WHERE id = $1 AND "userId" = $2 ` , Number(id), me.id)
if ( item ) {
throw new UserInputError ( 'cannot downvote your self' )
}
await serialize ( models , models . $queryRaw ` SELECT item_act( ${ Number ( id ) } , ${ me . id } , 'DONT_LIKE_THIS', ${ DONT _LIKE _THIS _COST } ) ` )
return true
2021-04-12 18:05:09 +00:00
}
} ,
Item : {
2022-11-15 20:51:55 +00:00
sats : async ( item , args , { models } ) => {
return msatsToSats ( item . msats )
} ,
commentSats : async ( item , args , { models } ) => {
return msatsToSats ( item . commentMsats )
} ,
2022-09-29 20:42:33 +00:00
isJob : async ( item , args , { models } ) => {
return item . subName === 'jobs'
} ,
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
} ,
2022-07-30 13:25:46 +00:00
poll : async ( item , args , { models , me } ) => {
if ( ! item . pollCost ) {
return null
}
const options = await models . $queryRaw `
SELECT "PollOption" . id , option , count ( "PollVote" . "userId" ) as count ,
coalesce ( bool _or ( "PollVote" . "userId" = $ { me ? . id } ) , 'f' ) as "meVoted"
FROM "PollOption"
LEFT JOIN "PollVote" on "PollVote" . "pollOptionId" = "PollOption" . id
WHERE "PollOption" . "itemId" = $ { item . id }
GROUP BY "PollOption" . id
ORDER BY "PollOption" . id ASC
`
const poll = { }
poll . options = options
poll . meVoted = options . some ( o => o . meVoted )
poll . count = options . reduce ( ( t , o ) => t + o . count , 0 )
return poll
} ,
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 } } )
} ,
2022-09-21 19:57:36 +00:00
comments : async ( item , args , { me , models } ) => {
2022-03-31 18:05:11 +00:00
if ( item . comments ) {
return item . comments
}
2022-09-21 19:57:36 +00:00
return comments ( me , models , item . id , 'hot' )
2022-03-04 18:05:16 +00:00
} ,
2022-01-20 23:04:12 +00:00
upvotes : async ( item , args , { models } ) => {
2022-11-23 18:12:09 +00:00
const [ { count } ] = await models . $queryRaw ( `
SELECT COUNT ( DISTINCT "userId" ) as count
FROM "ItemAct"
WHERE act = 'TIP' AND "itemId" = $1 ` , Number(item.id))
2021-09-10 21:13:52 +00:00
2022-11-15 20:51:55 +00:00
return count
2021-09-10 21:13:52 +00:00
} ,
2022-01-20 23:04:12 +00:00
boost : async ( item , args , { models } ) => {
2022-11-15 20:51:55 +00:00
const { sum : { msats } } = await models . itemAct . aggregate ( {
2021-04-26 21:55:15 +00:00
sum : {
2022-11-15 20:51:55 +00:00
msats : true
2021-04-26 21:55:15 +00:00
} ,
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
}
} )
2022-11-15 20:51:55 +00:00
return ( msats && msatsToSats ( msats ) ) || 0
2021-07-08 00:15:27 +00:00
} ,
2022-10-28 15:58:31 +00:00
wvotes : async ( item ) => {
return item . weightedVotes - item . weightedDownVotes
} ,
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
2022-11-15 20:51:55 +00:00
const { sum : { msats } } = await models . itemAct . aggregate ( {
2021-09-10 21:13:52 +00:00
sum : {
2022-11-15 20:51:55 +00:00
msats : true
2021-09-10 21:13:52 +00:00
} ,
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'
} ,
{
2022-11-23 18:12:09 +00:00
act : 'FEE'
2021-12-05 17:37:55 +00:00
}
]
2021-09-10 21:13:52 +00:00
}
} )
2022-11-15 20:51:55 +00:00
return ( msats && msatsToSats ( msats ) ) || 0
2021-09-10 21:13:52 +00:00
} ,
2022-09-21 19:57:36 +00:00
meDontLike : async ( item , args , { me , models } ) => {
if ( ! me ) return false
const dontLike = await models . itemAct . findFirst ( {
where : {
itemId : Number ( item . id ) ,
userId : me . id ,
act : 'DONT_LIKE_THIS'
}
} )
return ! ! dontLike
} ,
2022-09-22 18:44:50 +00:00
outlawed : async ( item , args , { me , models } ) => {
if ( me && Number ( item . userId ) === Number ( me . id ) ) {
return false
}
return item . weightedVotes - item . weightedDownVotes <= - ITEM _FILTER _THRESHOLD
} ,
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 )
}
}
2022-08-18 18:15:24 +00:00
export const updateItem = async ( parent , { id , data : { title , url , text , boost , forward , parentId } } , { 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' )
}
2022-08-18 18:15:24 +00:00
// if it's not the FAQ, not their bio, and older than 10 minutes
const user = await models . user . findUnique ( { where : { id : me . id } } )
2022-10-23 15:43:39 +00:00
if ( ! [ 349 , 76894 , 78763 , 81862 ] . includes ( old . id ) && user . bioId !== id && Date . now ( ) > new Date ( old . createdAt ) . getTime ( ) + 10 * 60000 ) {
2022-04-18 22:10:26 +00:00
throw new UserInputError ( 'item can no longer be editted' )
}
2022-08-18 18:15:24 +00:00
if ( boost && boost < BOOST _MIN ) {
throw new UserInputError ( ` boost must be at least ${ BOOST _MIN } ` , { argumentName : 'boost' } )
2022-08-10 15:06:31 +00:00
}
2022-08-27 02:57:41 +00:00
if ( ! old . parentId && title . length > MAX _TITLE _LENGTH ) {
2022-08-26 23:31:51 +00:00
throw new UserInputError ( 'title too long' )
}
2022-08-18 18:15:24 +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' } )
}
}
const [ item ] = await serialize ( models ,
models . $queryRaw (
2022-09-27 21:19:15 +00:00
` ${ SELECT } FROM update_item( $ 1, $ 2, $ 3, $ 4, $ 5, $ 6) AS "Item" ` ,
Number ( id ) , title , url , text , Number ( boost || 0 ) , Number ( fwdUser ? . id ) ) )
2021-08-18 22:20:33 +00:00
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-08-27 01:27:57 +00:00
if ( ! parentId && title . length > MAX _TITLE _LENGTH ) {
2022-08-26 23:31:51 +00:00
throw new UserInputError ( 'title too long' )
}
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 ,
2022-08-10 15:06:31 +00:00
models . $queryRaw (
2022-09-27 21:19:15 +00:00
` ${ SELECT } FROM create_item( $ 1, $ 2, $ 3, $ 4, $ 5, $ 6, $ 7, ' ${ ITEM _SPAM _INTERVAL } ') AS "Item" ` ,
2022-08-10 15:06:31 +00:00
title , url , text , Number ( boost || 0 ) , Number ( parentId ) , Number ( me . id ) ,
2022-09-27 21:19:15 +00:00
Number ( fwdUser ? . id ) ) )
2021-08-18 22:20:33 +00:00
await createMentions ( item , models )
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-09-27 21:19:15 +00:00
"Item" . "subName" , "Item" . status , "Item" . "uploadId" , "Item" . "pollCost" ,
2022-11-15 20:51:55 +00:00
"Item" . msats , "Item" . ncomments , "Item" . "commentMsats" , "Item" . "lastCommentAt" , "Item" . "weightedVotes" ,
2022-09-27 21:19:15 +00:00
"Item" . "weightedDownVotes" , "Item" . freebie , ltree2text ( "Item" . "path" ) AS "path" `
2021-04-27 21:30:58 +00:00
2022-09-21 19:57:36 +00:00
async function newTimedOrderByWeightedSats ( me , models , num ) {
2022-04-02 22:39:15 +00:00
return `
2022-10-31 17:56:48 +00:00
ORDER BY ( $ { await orderByNumerator ( me , models ) } / POWER ( GREATEST ( 3 , EXTRACT ( EPOCH FROM ( $$ { num } - "Item" . created _at ) ) / 3600 ) , 1.3 ) +
2022-07-24 00:00:57 +00:00
( "Item" . boost / $ { BOOST _MIN } : : float ) / POWER ( EXTRACT ( EPOCH FROM ( $$ { num } - "Item" . created _at ) ) / 3600 + 2 , 2.6 ) ) DESC NULLS LAST , "Item" . id DESC `
2022-01-17 22:38:40 +00:00
}
2022-09-21 19:57:36 +00:00
async function topOrderByWeightedSats ( me , models ) {
return ` ORDER BY ${ await orderByNumerator ( me , models ) } DESC NULLS LAST, "Item".id DESC `
}