make item creation easier

This commit is contained in:
keyan 2023-08-23 19:06:26 -05:00
parent 137e99cf7f
commit a847b16b2c
9 changed files with 310 additions and 185 deletions

View File

@ -5,10 +5,9 @@ import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
import { getMetadata, metadataRuleSets } from 'page-metadata-parser'
import domino from 'domino'
import {
BOOST_MIN, ITEM_SPAM_INTERVAL,
MAX_TITLE_LENGTH, ITEM_FILTER_THRESHOLD,
ITEM_SPAM_INTERVAL, ITEM_FILTER_THRESHOLD,
DONT_LIKE_THIS_COST, COMMENT_DEPTH_LIMIT, COMMENT_TYPE_QUERY,
ANON_COMMENT_FEE, ANON_USER_ID, ANON_POST_FEE, ANON_ITEM_SPAM_INTERVAL
ANON_COMMENT_FEE, ANON_USER_ID, ANON_POST_FEE, ANON_ITEM_SPAM_INTERVAL, POLL_COST
} from '../../lib/constants'
import { msatsToSats, numWithUnits } from '../../lib/format'
import { parse } from 'tldts'
@ -604,57 +603,34 @@ export default {
return await models.item.update({ where: { id: Number(id) }, data })
},
upsertLink: async (parent, args, { me, models }) => {
const { id, ...data } = args
data.url = ensureProtocol(data.url)
data.url = removeTracking(data.url)
await ssValidate(linkSchema, data, models)
upsertLink: async (parent, { id, invoiceHash, invoiceHmac, ...item }, { me, models, lnd }) => {
await ssValidate(linkSchema, item, models)
if (id) {
return await updateItem(parent, { id, data }, { me, models })
return await updateItem(parent, { id, ...item }, { me, models })
} else {
return await createItem(parent, data, { me, models, invoiceHash: args.invoiceHash, invoiceHmac: args.invoiceHmac })
return await createItem(parent, item, { me, models, lnd, invoiceHash, invoiceHmac })
}
},
upsertDiscussion: async (parent, args, { me, models }) => {
const { id, ...data } = args
await ssValidate(discussionSchema, data, models)
upsertDiscussion: async (parent, { id, invoiceHash, invoiceHmac, ...item }, { me, models, lnd }) => {
await ssValidate(discussionSchema, item, models)
if (id) {
return await updateItem(parent, { id, data }, { me, models })
return await updateItem(parent, { id, ...item }, { me, models })
} else {
return await createItem(parent, data, { me, models, invoiceHash: args.invoiceHash, invoiceHmac: args.invoiceHmac })
return await createItem(parent, item, { me, models, lnd, invoiceHash, invoiceHmac })
}
},
upsertBounty: async (parent, args, { me, models }) => {
const { id, ...data } = args
await ssValidate(bountySchema, data, models)
upsertBounty: async (parent, { id, invoiceHash, invoiceHmac, ...item }, { me, models, lnd }) => {
await ssValidate(bountySchema, item, models)
if (id) {
return await updateItem(parent, { id, data }, { me, models })
return await updateItem(parent, { id, ...item }, { me, models })
} else {
return await createItem(parent, data, { me, models })
return await createItem(parent, item, { me, models, lnd, invoiceHash, invoiceHmac })
}
},
upsertPoll: async (parent, { id, ...data }, { me, models }) => {
const { sub, forward, boost, title, text, options, invoiceHash, invoiceHmac } = data
let author = me
let spamInterval = ITEM_SPAM_INTERVAL
const trx = []
if (!me && invoiceHash) {
const invoice = await checkInvoice(models, invoiceHash, invoiceHmac, ANON_POST_FEE)
author = invoice.user
spamInterval = ANON_ITEM_SPAM_INTERVAL
trx.push(models.invoice.delete({ where: { hash: invoiceHash } }))
}
if (!author) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
}
upsertPoll: async (parent, { id, invoiceHash, invoiceHmac, ...item }, { me, models, lnd }) => {
const optionCount = id
? await models.pollOption.count({
where: {
@ -663,93 +639,60 @@ export default {
})
: 0
await ssValidate(pollSchema, data, models, optionCount)
const fwdUsers = await getForwardUsers(models, forward)
await ssValidate(pollSchema, item, models, optionCount)
if (id) {
const old = await models.item.findUnique({ where: { id: Number(id) } })
if (Number(old.userId) !== Number(author.id)) {
throw new GraphQLError('item does not belong to you', { extensions: { code: 'FORBIDDEN' } })
}
const [item] = await serialize(models,
models.$queryRawUnsafe(`${SELECT} FROM update_poll($1, $2::INTEGER, $3, $4, $5::INTEGER, $6, $7::JSON) AS "Item"`,
sub || 'bitcoin', Number(id), title, text, Number(boost || 0), options, JSON.stringify(fwdUsers)))
await createMentions(item, models)
item.comments = []
return item
return await updateItem(parent, { id, ...item }, { me, models })
} else {
const [query] = await serialize(models,
models.$queryRawUnsafe(
`${SELECT} FROM create_poll($1, $2, $3, $4::INTEGER, $5::INTEGER, $6::INTEGER, $7, $8::JSON, '${spamInterval}') AS "Item"`,
sub || 'bitcoin', title, text, 1, Number(boost || 0), Number(author.id), options, JSON.stringify(fwdUsers)), ...trx)
const item = trx.length > 0 ? query[0] : query
await createMentions(item, models)
item.comments = []
return item
item.pollCost = item.pollCost || POLL_COST
return await createItem(parent, item, { me, models, lnd, invoiceHash, invoiceHmac })
}
},
upsertJob: async (parent, { id, ...data }, { me, models }) => {
upsertJob: async (parent, { id, ...item }, { me, models }) => {
if (!me) {
throw new GraphQLError('you must be logged in to create job', { extensions: { code: 'FORBIDDEN' } })
}
const { sub, title, company, location, remote, text, url, maxBid, status, logo } = data
const fullSub = await models.sub.findUnique({ where: { name: sub } })
if (!fullSub) {
throw new GraphQLError('not a valid sub', { extensions: { code: 'BAD_INPUT' } })
item.location = item.location?.toLowerCase() === 'remote' ? undefined : item.location
await ssValidate(jobSchema, item, models)
if (item.logo) {
item.uploadId = item.logo
delete item.logo
}
item.maxBid ??= 0
await ssValidate(jobSchema, data, models)
const loc = location.toLowerCase() === 'remote' ? undefined : location
let item
if (id) {
const old = await models.item.findUnique({ where: { id: Number(id) } })
if (Number(old.userId) !== Number(me?.id)) {
throw new GraphQLError('item does not belong to you', { extensions: { code: 'FORBIDDEN' } })
}
([item] = await serialize(models,
models.$queryRawUnsafe(
`${SELECT} FROM update_job($1::INTEGER, $2, $3, $4, $5::INTEGER, $6, $7, $8, $9::INTEGER, $10::"Status") AS "Item"`,
Number(id), title, url, text, Number(maxBid), company, loc, remote, Number(logo), status)))
return await updateItem(parent, { id, ...item }, { me, models })
} else {
([item] = await serialize(models,
models.$queryRawUnsafe(
`${SELECT} FROM create_job($1, $2, $3, $4::INTEGER, $5::INTEGER, $6, $7, $8, $9::INTEGER) AS "Item"`,
title, url, text, Number(me.id), Number(maxBid), company, loc, remote, Number(logo))))
return await createItem(parent, item, { me, models })
}
await createMentions(item, models)
return item
},
createComment: async (parent, data, { me, models }) => {
await ssValidate(commentSchema, data)
const item = await createItem(parent, data,
{ me, models, invoiceHash: data.invoiceHash, invoiceHmac: data.invoiceHmac })
// fetch user to get up-to-date name
const user = await models.user.findUnique({ where: { id: me?.id || ANON_USER_ID } })
upsertComment: async (parent, { id, invoiceHash, invoiceHmac, ...item }, { me, models, lnd }) => {
await ssValidate(commentSchema, item)
const parents = await models.$queryRawUnsafe(
'SELECT DISTINCT p."userId" FROM "Item" i JOIN "Item" p ON p.path @> i.path WHERE i.id = $1 and p."userId" <> $2',
Number(item.parentId), Number(user.id))
Promise.allSettled(
parents.map(({ userId }) => sendUserNotification(userId, {
title: `@${user.name} replied to you`,
body: data.text,
item,
tag: 'REPLY'
}))
)
if (id) {
return await updateItem(parent, { id, ...item }, { me, models })
} else {
const rItem = await createItem(parent, item, { me, models, lnd, invoiceHash, invoiceHmac })
return item
},
updateComment: async (parent, { id, ...data }, { me, models }) => {
await ssValidate(commentSchema, data)
return await updateItem(parent, { id, data }, { me, models })
const notify = async () => {
const user = await models.user.findUnique({ where: { id: me?.id || ANON_USER_ID } })
const parents = await models.$queryRawUnsafe(
'SELECT DISTINCT p."userId" FROM "Item" i JOIN "Item" p ON p.path @> i.path WHERE i.id = $1 and p."userId" <> $2',
Number(item.parentId), Number(user.id))
Promise.allSettled(
parents.map(({ userId }) => sendUserNotification(userId, {
title: `@${user.name} replied to you`,
body: item.text,
item,
tag: 'REPLY'
}))
)
}
notify().catch(e => console.error(e))
return rItem
}
},
pollVote: async (parent, { id }, { me, models }) => {
if (!me) {
@ -1118,84 +1061,78 @@ export const createMentions = async (item, models) => {
}
}
export const updateItem = async (parent, { id, data: { sub, title, url, text, boost, forward, bounty, parentId } }, { me, models }) => {
export const updateItem = async (parent, { sub: subName, forward, options, ...item }, { me, models }) => {
// update iff this item belongs to me
const old = await models.item.findUnique({ where: { id: Number(id) } })
const old = await models.item.findUnique({ where: { id: Number(item.id) } })
if (Number(old.userId) !== Number(me?.id)) {
throw new GraphQLError('item does not belong to you', { extensions: { code: 'FORBIDDEN' } })
}
// if it's not the FAQ, not their bio, and older than 10 minutes
const user = await models.user.findUnique({ where: { id: me.id } })
if (![349, 76894, 78763, 81862].includes(old.id) && user.bioId !== id && Date.now() > new Date(old.createdAt).getTime() + 10 * 60000) {
if (![349, 76894, 78763, 81862].includes(old.id) && user.bioId !== old.id &&
typeof item.maxBid === 'undefined' && Date.now() > new Date(old.createdAt).getTime() + 10 * 60000) {
throw new GraphQLError('item can no longer be editted', { extensions: { code: 'BAD_INPUT' } })
}
if (boost && boost < BOOST_MIN) {
throw new GraphQLError(`boost must be at least ${BOOST_MIN}`, { extensions: { code: 'BAD_INPUT' } })
}
if (!old.parentId && title.length > MAX_TITLE_LENGTH) {
throw new GraphQLError('title too long', { extensions: { code: 'BAD_INPUT' } })
if (item.text) {
item.text = await proxyImages(item.text)
}
if (item.url && typeof item.maxBid === 'undefined') {
item.url = ensureProtocol(item.url)
item.url = removeTracking(item.url)
item.url = await proxyImages(item.url)
}
item = { subName, userId: me.id, ...item }
const fwdUsers = await getForwardUsers(models, forward)
url = await proxyImages(url)
text = await proxyImages(text)
const [item] = await serialize(models,
models.$queryRawUnsafe(
`${SELECT} FROM update_item($1, $2::INTEGER, $3, $4, $5, $6::INTEGER, $7::INTEGER, $8::JSON) AS "Item"`,
old.parentId ? null : sub || 'bitcoin', Number(id), title, url, text,
Number(boost || 0), bounty ? Number(bounty) : null, JSON.stringify(fwdUsers)))
const [rItem] = await serialize(models,
models.$queryRawUnsafe(`${SELECT} FROM update_item($1::JSONB, $2::JSONB, $3::JSONB) AS "Item"`,
JSON.stringify(item), JSON.stringify(fwdUsers), JSON.stringify(options)))
await createMentions(item, models)
await createMentions(rItem, models)
item.comments = []
return item
}
const createItem = async (parent, { sub, title, url, text, boost, forward, bounty, parentId }, { me, models, invoiceHash, invoiceHmac }) => {
let author = me
const createItem = async (parent, { forward, options, ...item }, { me, models, lnd, invoiceHash, invoiceHmac }) => {
let spamInterval = ITEM_SPAM_INTERVAL
const trx = []
if (!me && invoiceHash) {
const invoice = await checkInvoice(models, invoiceHash, invoiceHmac, parentId ? ANON_COMMENT_FEE : ANON_POST_FEE)
author = invoice.user
// rename to match column name
item.subName = item.sub
delete item.sub
if (me) {
item.userId = Number(me.id)
} else {
if (!invoiceHash) {
throw new GraphQLError('you must be logged in or pay', { extensions: { code: 'FORBIDDEN' } })
}
const invoice = await checkInvoice(models, invoiceHash, invoiceHmac, item.parentId ? ANON_COMMENT_FEE : ANON_POST_FEE)
item.userId = invoice.user.id
spamInterval = ANON_ITEM_SPAM_INTERVAL
trx.push(models.invoice.delete({ where: { hash: invoiceHash } }))
}
if (!author) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
}
if (boost && boost < BOOST_MIN) {
throw new GraphQLError(`boost must be at least ${BOOST_MIN}`, { extensions: { code: 'BAD_INPUT' } })
}
if (!parentId && title.length > MAX_TITLE_LENGTH) {
throw new GraphQLError('title too long', { extensions: { code: 'BAD_INPUT' } })
}
const fwdUsers = await getForwardUsers(models, forward)
url = await proxyImages(url)
text = await proxyImages(text)
if (item.text) {
item.text = await proxyImages(item.text)
}
if (item.url && typeof item.maxBid === 'undefined') {
item.url = ensureProtocol(item.url)
item.url = removeTracking(item.url)
item.url = await proxyImages(item.url)
}
const [query] = await serialize(
const [result] = await serialize(
models,
models.$queryRawUnsafe(
`${SELECT} FROM create_item($1, $2, $3, $4, $5::INTEGER, $6::INTEGER, $7::INTEGER, $8::INTEGER, $9::JSON, '${spamInterval}') AS "Item"`,
parentId ? null : sub || 'bitcoin',
title,
url,
text,
Number(boost || 0),
bounty ? Number(bounty) : null,
Number(parentId),
Number(author.id),
JSON.stringify(fwdUsers)),
models.$queryRawUnsafe(`${SELECT} FROM create_item($1::JSONB, $2::JSONB, $3::JSONB, '${spamInterval}'::INTERVAL) AS "Item"`,
JSON.stringify(item), JSON.stringify(fwdUsers), JSON.stringify(options)),
...trx)
const item = trx.length > 0 ? query[0] : query
item = Array.isArray(result) ? result[0] : result
await createMentions(item, models)

View File

@ -32,8 +32,7 @@ export default gql`
upsertJob(id: ID, sub: String!, title: String!, company: String!, location: String, remote: Boolean,
text: String!, url: String!, maxBid: Int!, status: String, logo: Int): Item!
upsertPoll(id: ID, sub: String, title: String!, text: String, options: [String!]!, boost: Int, forward: [ItemForwardInput], invoiceHash: String, invoiceHmac: String): Item!
createComment(text: String!, parentId: ID!, invoiceHash: String, invoiceHmac: String): Item!
updateComment(id: ID!, text: String!): Item!
upsertComment(id:ID, text: String!, parentId: ID, invoiceHash: String, invoiceHmac: String): Item!
dontLikeThis(id: ID!): Boolean!
act(id: ID!, sats: Int, invoiceHash: String, invoiceHmac: String): ItemActResult!
pollVote(id: ID!): ID!

View File

@ -7,19 +7,19 @@ import Delete from './delete'
import { commentSchema } from '../lib/validate'
export default function CommentEdit ({ comment, editThreshold, onSuccess, onCancel }) {
const [updateComment] = useMutation(
const [upsertComment] = useMutation(
gql`
mutation updateComment($id: ID! $text: String!) {
updateComment(id: $id, text: $text) {
mutation upsertComment($id: ID! $text: String!) {
upsertComment(id: $id, text: $text) {
text
}
}`, {
update (cache, { data: { updateComment } }) {
update (cache, { data: { upsertComment } }) {
cache.modify({
id: `Item:${comment.id}`,
fields: {
text () {
return updateComment.text
return upsertComment.text
}
}
})
@ -35,7 +35,7 @@ export default function CommentEdit ({ comment, editThreshold, onSuccess, onCanc
}}
schema={commentSchema}
onSubmit={async (values, { resetForm }) => {
const { error } = await updateComment({ variables: { ...values, id: comment.id } })
const { error } = await upsertComment({ variables: { ...values, id: comment.id } })
if (error) {
throw new Error({ message: error.toString() })
}

View File

@ -43,24 +43,24 @@ export default function Reply ({ item, onSuccess, replyOpen, children, placehold
setReply(replyOpen || !!window.localStorage.getItem('reply-' + parentId + '-' + 'text'))
}, [])
const [createComment] = useMutation(
const [upsertComment] = useMutation(
gql`
${COMMENTS}
mutation createComment($text: String!, $parentId: ID!, $invoiceHash: String, $invoiceHmac: String) {
createComment(text: $text, parentId: $parentId, invoiceHash: $invoiceHash, invoiceHmac: $invoiceHmac) {
mutation upsertComment($text: String!, $parentId: ID!, $invoiceHash: String, $invoiceHmac: String) {
upsertComment(text: $text, parentId: $parentId, invoiceHash: $invoiceHash, invoiceHmac: $invoiceHmac) {
...CommentFields
comments {
...CommentsRecursive
}
}
}`, {
update (cache, { data: { createComment } }) {
update (cache, { data: { upsertComment } }) {
cache.modify({
id: `Item:${parentId}`,
fields: {
comments (existingCommentRefs = []) {
const newCommentRef = cache.writeFragment({
data: createComment,
data: upsertComment,
fragment: COMMENTS,
fragmentName: 'CommentsRecursive'
})
@ -86,20 +86,20 @@ export default function Reply ({ item, onSuccess, replyOpen, children, placehold
// so that we don't see indicator for our own comments, we record this comments as the latest time
// but we also have record num comments, in case someone else commented when we did
const root = ancestors[0]
commentsViewedAfterComment(root, createComment.createdAt)
commentsViewedAfterComment(root, upsertComment.createdAt)
}
}
)
const submitComment = useCallback(
async (_, values, parentId, resetForm, invoiceHash, invoiceHmac) => {
const { error } = await createComment({ variables: { ...values, parentId, invoiceHash, invoiceHmac } })
const { error } = await upsertComment({ variables: { ...values, parentId, invoiceHash, invoiceHmac } })
if (error) {
throw new Error({ message: error.toString() })
}
resetForm({ text: '' })
setReply(replyOpen || false)
}, [createComment, setReply])
}, [upsertComment, setReply])
const invoiceableCreateComment = useInvoiceable(submitComment)

View File

@ -26,6 +26,7 @@ module.exports = {
ANON_BALANCE_LIMIT_MSATS: 0, // disable
MAX_POLL_NUM_CHOICES: 10,
MIN_POLL_NUM_CHOICES: 2,
POLL_COST: 1,
ITEM_FILTER_THRESHOLD: 1.2,
DONT_LIKE_THIS_COST: 1,
COMMENT_TYPE_QUERY: ['comments', 'freebies', 'outlawed', 'borderland', 'all', 'bookmarks'],

View File

@ -1,4 +1,5 @@
export function ensureProtocol (value) {
if (!value) return value
value = value.trim()
if (!/^([a-z0-9]+:\/\/|mailto:)/.test(value)) {
value = 'http://' + value
@ -11,6 +12,7 @@ export function isExternal (url) {
}
export function removeTracking (value) {
if (!value) return value
const exprs = [
// twitter URLs
/^(?<url>https?:\/\/twitter\.com\/(?:#!\/)?(?<user>\w+)\/status(?:es)?\/(?<id>\d+))/

View File

@ -63,10 +63,7 @@ export function advPostSchemaMembers (client) {
boost: intValidator
.min(BOOST_MIN, `must be blank or at least ${BOOST_MIN}`).test({
name: 'boost',
test: async boost => {
if (!boost || boost % BOOST_MIN === 0) return true
return false
},
test: async boost => !boost || boost % BOOST_MIN === 0,
message: `must be divisble be ${BOOST_MIN}`
}),
// XXX this lets you forward to youself (it's financially equivalent but it should be disallowed)

View File

@ -0,0 +1,189 @@
-- AlterTable
ALTER TABLE "Invoice" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;
-- AlterTable
ALTER TABLE "ItemAct" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;
-- AlterTable
ALTER TABLE "Mention" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;
-- AlterTable
ALTER TABLE "Withdrawl" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;
-- AlterTable
ALTER TABLE "accounts" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;
-- AlterTable
ALTER TABLE "sessions" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;
-- AlterTable
ALTER TABLE "verification_requests" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;
-- remove boost denormalization
DROP TRIGGER IF EXISTS boost_after_act ON "ItemAct";
DROP FUNCTION IF EXISTS boost_after_act();
-- remove functions that are hereto unused
DROP FUNCTION IF EXISTS create_bio(title text, text text, user_id integer);
DROP FUNCTION IF EXISTS create_item(sub text, title text, url text, text text, boost integer, bounty integer, parent_id integer, user_id integer, forward json, spam_within interval);
DROP FUNCTION IF EXISTS create_poll(sub text, title text, text text, poll_cost integer, boost integer, user_id integer, options text[], forward json, spam_within interval);
DROP FUNCTION IF EXISTS create_job(title TEXT, url TEXT, text TEXT, user_id INTEGER, job_bid INTEGER, job_company TEXT, job_location TEXT, job_remote BOOLEAN, job_upload_id INTEGER);
DROP FUNCTION IF EXISTS update_item(
sub TEXT, item_id INTEGER, item_title TEXT, item_url TEXT, item_text TEXT, boost INTEGER,
item_bounty INTEGER, forward JSON);
DROP FUNCTION IF EXISTS update_job(item_id INTEGER,
item_title TEXT, item_url TEXT, item_text TEXT, job_bid INTEGER, job_company TEXT,
job_location TEXT, job_remote BOOLEAN, job_upload_id INTEGER, job_status "Status");
DROP FUNCTION IF EXISTS update_poll(
sub TEXT, id INTEGER, title TEXT, text TEXT, boost INTEGER,
options TEXT[], forward JSON);
-- remove type because table "ItemForward" has an implicit type already
DROP TYPE IF EXISTS ItemForwardType;
-- only have one function to create items
CREATE OR REPLACE FUNCTION create_item(
jitem JSONB, forward JSONB, poll_options JSONB, spam_within INTERVAL)
RETURNS "Item"
LANGUAGE plpgsql
AS $$
DECLARE
user_msats BIGINT;
cost_msats BIGINT;
freebie BOOLEAN;
item "Item";
med_votes FLOAT;
select_clause TEXT;
BEGIN
PERFORM ASSERT_SERIALIZED();
-- access fields with appropriate types
item := jsonb_populate_record(NULL::"Item", jitem);
SELECT msats INTO user_msats FROM users WHERE id = item."userId";
IF item."maxBid" IS NOT NULL THEN
cost_msats := 1000000;
ELSE
cost_msats := 1000 * POWER(10, item_spam(item."parentId", item."userId", spam_within));
END IF;
-- it's only a freebie if it's a 1 sat cost, they have < 1 sat, and boost = 0
freebie := (cost_msats <= 1000) AND (user_msats < 1000) AND (item.boost = 0);
IF NOT freebie AND cost_msats > user_msats THEN
RAISE EXCEPTION 'SN_INSUFFICIENT_FUNDS';
END IF;
-- get this user's median item score
SELECT COALESCE(
percentile_cont(0.5) WITHIN GROUP(
ORDER BY "weightedVotes" - "weightedDownVotes"), 0)
INTO med_votes FROM "Item" WHERE "userId" = item."userId";
-- if their median votes are positive, start at 0
-- if the median votes are negative, start their post with that many down votes
-- basically: if their median post is bad, presume this post is too
-- addendum: if they're an anon poster, always start at 0
IF med_votes >= 0 OR item."userId" = 27 THEN
med_votes := 0;
ELSE
med_votes := ABS(med_votes);
END IF;
-- there's no great way to set default column values when using json_populate_record
-- so we need to only select fields with non-null values that way when func input
-- does not include a value, the default value is used instead of null
SELECT string_agg('"' || key || '"', ',') INTO select_clause
FROM jsonb_object_keys(jsonb_strip_nulls(jitem)) k(key);
-- insert the item
EXECUTE format($fmt$
INSERT INTO "Item" (%s, "weightedDownVotes")
SELECT %1$s, %L
FROM jsonb_populate_record(NULL::"Item", %L) RETURNING *
$fmt$, select_clause, med_votes, jitem) INTO item;
INSERT INTO "ItemForward" ("itemId", "userId", "pct")
SELECT item.id, "userId", "pct" FROM jsonb_populate_recordset(NULL::"ItemForward", forward);
INSERT INTO "PollOption" ("itemId", "option")
SELECT item.id, "option" FROM jsonb_array_elements_text(poll_options) o("option");
IF NOT freebie THEN
UPDATE users SET msats = msats - cost_msats WHERE id = item."userId";
INSERT INTO "ItemAct" (msats, "itemId", "userId", act)
VALUES (cost_msats, item.id, item."userId", 'FEE');
END IF;
-- if this item has boost
IF item.boost > 0 THEN
PERFORM item_act(item.id, item."userId", 'BOOST', item.boost);
END IF;
-- if this is a job
IF item."maxBid" IS NOT NULL THEN
PERFORM run_auction(item.id);
END IF;
-- if this is a bio
IF item.bio THEN
UPDATE users SET "bioId" = item.id WHERE id = item."userId";
END IF;
RETURN item;
END;
$$;
-- only have one function to update items
CREATE OR REPLACE FUNCTION update_item(
jitem JSONB, forward JSONB, poll_options JSONB)
RETURNS "Item"
LANGUAGE plpgsql
AS $$
DECLARE
user_msats INTEGER;
item "Item";
select_clause TEXT;
BEGIN
PERFORM ASSERT_SERIALIZED();
item := jsonb_populate_record(NULL::"Item", jitem);
IF item.boost > 0 THEN
UPDATE "Item" SET boost = boost + item.boost WHERE id = item.id;
PERFORM item_act(item.id, item."userId", 'BOOST', item.boost);
END IF;
IF item.status IS NOT NULL THEN
UPDATE "Item" SET "statusUpdatedAt" = now_utc()
WHERE id = item.id AND status <> item.status;
END IF;
SELECT string_agg('"' || key || '"', ',') INTO select_clause
FROM jsonb_object_keys(jsonb_strip_nulls(jitem)) k(key)
WHERE key <> 'boost';
EXECUTE format($fmt$
UPDATE "Item" SET (%s) = (
SELECT %1$s
FROM jsonb_populate_record(NULL::"Item", %L)
) WHERE id = %L RETURNING *
$fmt$, select_clause, jitem, item.id) INTO item;
-- Delete all old forward entries, to recreate in next command
DELETE FROM "ItemForward" WHERE "itemId" = item.id;
INSERT INTO "ItemForward" ("itemId", "userId", "pct")
SELECT item.id, "userId", "pct" FROM jsonb_populate_recordset(NULL::"ItemForward", forward);
INSERT INTO "PollOption" ("itemId", "option")
SELECT item.id, "option" FROM jsonb_array_elements_text(poll_options) o("option");
-- if this is a job
IF item."maxBid" IS NOT NULL THEN
PERFORM run_auction(item.id);
END IF;
RETURN item;
END;
$$;

View File

@ -376,7 +376,7 @@ model ReferralAct {
model ItemAct {
id Int @id(map: "Vote_pkey") @default(autoincrement())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
msats BigInt
act ItemActType
itemId Int
@ -400,7 +400,7 @@ model ItemAct {
model Mention {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
itemId Int
userId Int
item Item @relation(fields: [itemId], references: [id], onDelete: Cascade)
@ -415,7 +415,7 @@ model Mention {
model Invoice {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
userId Int
hash String @unique(map: "Invoice.hash_unique")
bolt11 String
@ -434,7 +434,7 @@ model Invoice {
model Withdrawl {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
userId Int
hash String
bolt11 String
@ -452,7 +452,7 @@ model Withdrawl {
model Account {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
userId Int @map("user_id")
type String @map("provider_type")
provider String @map("provider_id")
@ -480,7 +480,7 @@ model Session {
id Int @id @default(autoincrement())
sessionToken String @unique(map: "sessions.session_token_unique") @map("session_token")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
userId Int @map("user_id")
expires DateTime
@ -492,7 +492,7 @@ model Session {
model VerificationToken {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
identifier String
token String @unique(map: "verification_requests.token_unique")
expires DateTime