stacker.news/api/resolvers/item.js

261 lines
7.7 KiB
JavaScript
Raw Normal View History

2021-04-12 18:05:09 +00:00
import { UserInputError, AuthenticationError } from 'apollo-server-micro'
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 17:47:49 +00:00
const LIMIT = 5
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}
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})
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) {
return { offset: 0, time: new Date() }
} 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-22 17:47:49 +00:00
moreItems: async (parent, { sort, cursor, userId }, { models }) => {
const decodedCursor = decodeCursor(cursor)
const items = userId
? await models.$queryRaw(`
${SELECT}
FROM "Item"
WHERE "userId" = $1 AND "parentId" IS NULL AND created_at <= $2
ORDER BY created_at
OFFSET $3
LIMIT ${LIMIT}`, Number(userId), decodedCursor.time, decodedCursor.offset)
: sort === 'hot'
? await models.$queryRaw(`
${SELECT}
FROM "Item"
${timedLeftJoinSats(1)}
WHERE "parentId" IS NULL AND created_at <= $1
${timedOrderBySats(1)}
OFFSET $2
LIMIT ${LIMIT}`, decodedCursor.time, decodedCursor.offset)
: await models.$queryRaw(`
${SELECT}
FROM "Item"
WHERE "parentId" IS NULL AND created_at <= $1
ORDER BY created_at DESC
OFFSET $2
LIMIT ${LIMIT}`, decodedCursor.time, decodedCursor.offset)
return {
cursor: items.length === LIMIT ? nextCursorEncoded(decodedCursor) : null,
items
}
2021-04-24 21:05:07 +00:00
},
2021-04-14 23:56:29 +00:00
item: async (parent, { id }, { models }) => {
const [item] = await models.$queryRaw(`
2021-04-22 22:14:32 +00:00
${SELECT}
2021-04-14 23:56:29 +00:00
FROM "Item"
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"
WHERE "userId" = $1 AND "parentId" IS NOT NULL
ORDER BY created_at DESC`, Number(userId))
2021-04-22 22:14:32 +00:00
},
2021-04-15 19:41:02 +00:00
root: async (parent, { id }, { models }) => {
2021-04-22 22:14:32 +00:00
return (await models.$queryRaw(`
${SELECT}
2021-04-15 19:41:02 +00:00
FROM "Item"
2021-04-22 22:14:32 +00:00
WHERE id = (
SELECT ltree2text(subltree(path, 0, 1))::integer
FROM "Item"
WHERE id = $1)`, Number(id)))[0]
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) {
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) {
throw new UserInputError('link must have url', { argumentName: 'url' })
2021-04-12 18:05:09 +00:00
}
console.log(ensureProtocol(url))
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) {
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) {
throw new UserInputError('comment must have text', { argumentName: 'text' })
2021-04-14 00:57:32 +00:00
}
if (!parentId) {
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 })
},
vote: async (parent, { id, sats = 1 }, { me, models }) => {
// need to make sure we are logged in
if (!me) {
throw new AuthenticationError('you must be logged in')
}
2021-04-27 21:30:58 +00:00
if (sats <= 0) {
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)})`)
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
},
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-05-11 15:52:50 +00:00
return sats || 0
},
meSats: async (item, args, { me, models }) => {
if (!me) return 0
2021-06-11 23:12:39 +00:00
const meFull = await models.user.findUnique({ where: { name: me.name } })
const { sum: { sats } } = await models.vote.aggregate({
sum: {
sats: true
},
where: {
itemId: item.id,
2021-06-11 23:12:39 +00:00
userId: meFull.id
}
})
2021-05-11 15:52:50 +00:00
return sats || 0
}
}
}
const createItem = async (parent, { title, url, text, parentId }, { me, models }) => {
if (!me) {
throw new AuthenticationError('you must be logged in')
}
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
}
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
}
return [result, added]
2021-04-12 18:05:09 +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-06-22 17:47:49 +00:00
function timedLeftJoinSats (num) {
return `LEFT JOIN (SELECT i.id, SUM("Vote".sats) as sats
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 =
`LEFT JOIN (SELECT i.id, SUM("Vote".sats) as sats
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) {
return `ORDER BY (x.sats-1)/POWER(EXTRACT(EPOCH FROM ($${num} - "Item".created_at))/3600+2, 1.5) DESC NULLS LAST`
}
2021-04-27 21:30:58 +00:00
const ORDER_BY_SATS =
'ORDER BY (x.sats-1)/POWER(EXTRACT(EPOCH FROM ((NOW() AT TIME ZONE \'UTC\') - "Item".created_at))/3600+2, 1.5) DESC NULLS LAST'