query is working
This commit is contained in:
parent
5ad70efbd7
commit
96a18e6c9d
@ -1,6 +1,8 @@
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const prisma = global.prisma || new PrismaClient()
|
||||
const prisma = global.prisma || new PrismaClient({
|
||||
log: ['query', 'warn', 'error']
|
||||
})
|
||||
|
||||
if (process.env.NODE_ENV === 'development') global.prisma = prisma
|
||||
|
||||
|
16
api/resolvers/cursor.js
Normal file
16
api/resolvers/cursor.js
Normal file
@ -0,0 +1,16 @@
|
||||
export const LIMIT = 21
|
||||
|
||||
export 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
|
||||
}
|
||||
}
|
||||
|
||||
export function nextCursorEncoded (cursor) {
|
||||
cursor.offset += LIMIT
|
||||
return Buffer.from(JSON.stringify(cursor)).toString('base64')
|
||||
}
|
@ -3,5 +3,6 @@ import message from './message'
|
||||
import item from './item'
|
||||
import wallet from './wallet'
|
||||
import lnurl from './lnurl'
|
||||
import notifications from './notifications'
|
||||
|
||||
export default [user, item, message, wallet, lnurl]
|
||||
export default [user, item, message, wallet, lnurl, notifications]
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { UserInputError, AuthenticationError } from 'apollo-server-micro'
|
||||
import { ensureProtocol } from '../../lib/url'
|
||||
import serialize from './serial'
|
||||
|
||||
const LIMIT = 21
|
||||
import { decodeCursor, LIMIT, nextCursorEncoded } from './cursor'
|
||||
|
||||
async function comments (models, id) {
|
||||
const flat = await models.$queryRaw(`
|
||||
@ -20,21 +19,6 @@ async function comments (models, id) {
|
||||
return nestComments(flat, id)[0]
|
||||
}
|
||||
|
||||
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')
|
||||
}
|
||||
|
||||
export default {
|
||||
Query: {
|
||||
moreItems: async (parent, { sort, cursor, userId }, { me, models }) => {
|
||||
@ -88,6 +72,7 @@ export default {
|
||||
OFFSET $3
|
||||
LIMIT ${LIMIT}`, Number(userId), decodedCursor.time, decodedCursor.offset)
|
||||
} else {
|
||||
// notifications ... god such spagetti
|
||||
if (!me) {
|
||||
throw new AuthenticationError('you must be logged in')
|
||||
}
|
||||
@ -105,18 +90,6 @@ export default {
|
||||
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
|
||||
ORDER BY "Item".created_at DESC`, me.id)
|
||||
},
|
||||
item: async (parent, { id }, { models }) => {
|
||||
const [item] = await models.$queryRaw(`
|
||||
${SELECT}
|
||||
|
88
api/resolvers/notifications.js
Normal file
88
api/resolvers/notifications.js
Normal file
@ -0,0 +1,88 @@
|
||||
import { decodeCursor, LIMIT, nextCursorEncoded } from './cursor'
|
||||
|
||||
export default {
|
||||
Query: {
|
||||
notifications: async (parent, { cursor }, { me, models }) => {
|
||||
const decodedCursor = decodeCursor(cursor)
|
||||
// if (!me) {
|
||||
// throw new AuthenticationError('you must be logged in')
|
||||
// }
|
||||
|
||||
/*
|
||||
So that we can cursor over results, we union notifications together ...
|
||||
this requires we have the same number of columns in all results
|
||||
|
||||
select "Item".id, NULL as earnedSats, "Item".created_at as created_at from
|
||||
"Item" JOIN "Item" p ON "Item"."parentId" = p.id AND p."userId" = 622 AND
|
||||
"Item"."userId" <> 622 UNION ALL select "Item".id, "Vote".sats as earnedSats,
|
||||
"Vote".created_at as created_at FROM "Item" LEFT JOIN "Vote" on
|
||||
"Vote"."itemId" = "Item".id AND "Vote"."userId" <> 622 AND "Vote".boost = false
|
||||
WHERE "Item"."userId" = 622 ORDER BY created_at DESC;
|
||||
|
||||
Because we want to "collapse" time adjacent votes in the result
|
||||
|
||||
select vote.id, sum(vote."earnedSats") as "earnedSats", max(vote.voted_at)
|
||||
as "createdAt" from (select "Item".*, "Vote".sats as "earnedSats",
|
||||
"Vote".created_at as voted_at, ROW_NUMBER() OVER(ORDER BY "Vote".created_at) -
|
||||
ROW_NUMBER() OVER(PARTITION BY "Item".id ORDER BY "Vote".created_at) as island
|
||||
FROM "Item" LEFT JOIN "Vote" on "Vote"."itemId" = "Item".id AND
|
||||
"Vote"."userId" <> 622 AND "Vote".boost = false WHERE "Item"."userId" = 622)
|
||||
as vote group by vote.id, vote.island order by max(vote.voted_at) desc;
|
||||
|
||||
We can also "collapse" votes occuring within 1 hour intervals of each other
|
||||
(I haven't yet combined with the above collapsing method .. but might be
|
||||
overkill)
|
||||
|
||||
select "Item".id, sum("Vote".sats) as earnedSats, max("Vote".created_at)
|
||||
as created_at, ROW_NUMBER() OVER(ORDER BY max("Vote".created_at)) - ROW_NUMBER()
|
||||
OVER(PARTITION BY "Item".id ORDER BY max("Vote".created_at)) as island FROM
|
||||
"Item" LEFT JOIN "Vote" on "Vote"."itemId" = "Item".id AND "Vote"."userId" <> 622
|
||||
AND "Vote".boost = false WHERE "Item"."userId" = 622 group by "Item".id,
|
||||
date_trunc('hour', "Vote".created_at) order by created_at desc;
|
||||
*/
|
||||
|
||||
let notifications = await models.$queryRaw(`
|
||||
SELECT ${ITEM_FIELDS}, "Item".created_at as sort_time, NULL as "earnedSats"
|
||||
From "Item"
|
||||
JOIN "Item" p ON "Item"."parentId" = p.id AND p."userId" = $1
|
||||
AND "Item"."userId" <> $1 AND "Item".created_at <= $2
|
||||
UNION ALL
|
||||
(SELECT ${ITEM_SUBQUERY_FIELDS}, max(subquery.voted_at) as sort_time, sum(subquery.sats) as "earnedSats"
|
||||
FROM
|
||||
(SELECT ${ITEM_FIELDS}, "Vote".created_at as voted_at, "Vote".sats,
|
||||
ROW_NUMBER() OVER(ORDER BY "Vote".created_at) -
|
||||
ROW_NUMBER() OVER(PARTITION BY "Item".id ORDER BY "Vote".created_at) as island
|
||||
FROM "Item"
|
||||
LEFT JOIN "Vote" on "Vote"."itemId" = "Item".id
|
||||
AND "Vote"."userId" <> $1
|
||||
AND "Item".created_at <= $2
|
||||
AND "Vote".boost = false
|
||||
WHERE "Item"."userId" = $1) subquery
|
||||
GROUP BY ${ITEM_SUBQUERY_FIELDS}, subquery.island ORDER BY max(subquery.voted_at) desc)
|
||||
ORDER BY sort_time DESC
|
||||
OFFSET $3
|
||||
LIMIT ${LIMIT}`, me ? me.id : 622, decodedCursor.time, decodedCursor.offset)
|
||||
|
||||
notifications = notifications.map(n => {
|
||||
n.item = { ...n }
|
||||
return n
|
||||
})
|
||||
return {
|
||||
cursor: notifications.length === LIMIT ? nextCursorEncoded(decodedCursor) : null,
|
||||
notifications
|
||||
}
|
||||
}
|
||||
},
|
||||
Notification: {
|
||||
__resolveType: async (notification, args, { models }) =>
|
||||
notification.earnedSats ? 'Votification' : 'Reply'
|
||||
}
|
||||
}
|
||||
|
||||
const ITEM_SUBQUERY_FIELDS =
|
||||
`subquery.id, subquery."createdAt", subquery."updatedAt", subquery.title, subquery.text,
|
||||
subquery.url, subquery."userId", subquery."parentId", subquery.path`
|
||||
|
||||
const ITEM_FIELDS =
|
||||
`"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"`
|
@ -5,6 +5,7 @@ import message from './message'
|
||||
import item from './item'
|
||||
import wallet from './wallet'
|
||||
import lnurl from './lnurl'
|
||||
import notifications from './notifications'
|
||||
|
||||
const link = gql`
|
||||
type Query {
|
||||
@ -20,4 +21,4 @@ const link = gql`
|
||||
}
|
||||
`
|
||||
|
||||
export default [link, user, item, message, wallet, lnurl]
|
||||
export default [link, user, item, message, wallet, lnurl, notifications]
|
||||
|
@ -4,7 +4,6 @@ export default gql`
|
||||
extend type Query {
|
||||
moreItems(sort: String!, cursor: String, userId: ID): Items
|
||||
moreFlatComments(cursor: String, userId: ID): Comments
|
||||
notifications: [Item!]!
|
||||
item(id: ID!): Item
|
||||
userComments(userId: ID!): [Item!]
|
||||
}
|
||||
|
23
api/typeDefs/notifications.js
Normal file
23
api/typeDefs/notifications.js
Normal file
@ -0,0 +1,23 @@
|
||||
import { gql } from 'apollo-server-micro'
|
||||
|
||||
export default gql`
|
||||
extend type Query {
|
||||
notifications(cursor: String): Notifications
|
||||
}
|
||||
|
||||
type Votification {
|
||||
earnedSats: Int!
|
||||
item: Item!
|
||||
}
|
||||
|
||||
type Reply {
|
||||
item: Item!
|
||||
}
|
||||
|
||||
union Notification = Reply | Votification
|
||||
|
||||
type Notifications {
|
||||
cursor: String
|
||||
notifications: [Notification!]!
|
||||
}
|
||||
`
|
68
components/notifications.js
Normal file
68
components/notifications.js
Normal file
@ -0,0 +1,68 @@
|
||||
import { useQuery } from '@apollo/client'
|
||||
import Button from 'react-bootstrap/Button'
|
||||
import { useState } from 'react'
|
||||
import Comment, { CommentSkeleton } from './comment'
|
||||
import { NOTIFICATIONS } from '../fragments/notifications'
|
||||
|
||||
export default function CommentsFlat ({ variables, ...props }) {
|
||||
const { loading, error, data, fetchMore } = useQuery(NOTIFICATIONS)
|
||||
if (error) return <div>Failed to load!</div>
|
||||
if (loading) {
|
||||
return <CommentsFlatSkeleton />
|
||||
}
|
||||
|
||||
const { notifications: { notifications, cursor } } = data
|
||||
return (
|
||||
<>
|
||||
{notifications.map(item => (
|
||||
<Comment key={item.id} item={item} {...props} />
|
||||
))}
|
||||
<MoreFooter cursor={cursor} fetchMore={fetchMore} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function CommentsFlatSkeleton () {
|
||||
const comments = new Array(21).fill(null)
|
||||
|
||||
return (
|
||||
<div>{comments.map((_, i) => (
|
||||
<CommentSkeleton key={i} skeletonChildren={0} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function MoreFooter ({ cursor, fetchMore }) {
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
if (loading) {
|
||||
return <div><CommentsFlatSkeleton /></div>
|
||||
}
|
||||
|
||||
let Footer
|
||||
if (cursor) {
|
||||
Footer = () => (
|
||||
<Button
|
||||
variant='primary'
|
||||
size='md'
|
||||
onClick={async () => {
|
||||
setLoading(true)
|
||||
await fetchMore({
|
||||
variables: {
|
||||
cursor
|
||||
}
|
||||
})
|
||||
setLoading(false)
|
||||
}}
|
||||
>more
|
||||
</Button>
|
||||
)
|
||||
} else {
|
||||
Footer = () => (
|
||||
<div className='text-muted' style={{ fontFamily: 'lightning', fontSize: '2rem', opacity: '0.6' }}>GENISIS</div>
|
||||
)
|
||||
}
|
||||
|
||||
return <div className='d-flex justify-content-center mt-4 mb-2'><Footer /></div>
|
||||
}
|
@ -7,7 +7,7 @@ export default function Seo ({ item, user }) {
|
||||
const pathNoQuery = router.asPath.split('?')[0]
|
||||
const defaultTitle = pathNoQuery.slice(1)
|
||||
let fullTitle = `${defaultTitle && `${defaultTitle} \\ `}stacker news`
|
||||
let desc = 'Bitcoin news powered by the Lightning Network.'
|
||||
let desc = "It's like Hacker News but we pay you Bitcoin."
|
||||
if (item) {
|
||||
if (item.title) {
|
||||
fullTitle = `${item.title} \\ stacker news`
|
||||
|
27
fragments/notifications.js
Normal file
27
fragments/notifications.js
Normal file
@ -0,0 +1,27 @@
|
||||
import { gql } from 'apollo-server-micro'
|
||||
import { ITEM_FIELDS } from './items'
|
||||
|
||||
export const NOTIFICATIONS = gql`
|
||||
${ITEM_FIELDS}
|
||||
|
||||
query Notifications($cursor: String) {
|
||||
notifications(cursor: $cursor) {
|
||||
cursor
|
||||
notifications {
|
||||
__typename
|
||||
... on Votification {
|
||||
earnedSats
|
||||
item {
|
||||
...ItemFields
|
||||
text
|
||||
}
|
||||
}
|
||||
... on Reply {
|
||||
item {
|
||||
...ItemFields
|
||||
text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} `
|
Loading…
x
Reference in New Issue
Block a user