-
-
Notifications
You must be signed in to change notification settings - Fork 114
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Replace toasts - be more optimistic - prepaid and postpaid #1184
Changes from 42 commits
4d052dd
8b71200
4d959c6
d9bf839
eca90ac
a7391a1
23b6103
e3aa20a
a118dd6
c15658f
5b40288
73eec20
bfbcc74
a47c291
b78e8d8
e11561a
eeee7e8
4a4daeb
ac9bcd6
5775245
6740e6a
f747f7d
29c41d7
7fc3d01
4295ac1
f1a3ce7
4afa840
e64a5b3
dc75a27
02df708
3e30af6
572387f
600f697
76ac89f
d90c119
fb43c48
afc4864
6741fab
711ed36
0949b08
dcd59fd
1b6b02a
669ad67
5aadd98
bbdbb62
aff8f5b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
import { GraphQLError } from 'graphql' | ||
import { ensureProtocol, removeTracking, stripTrailingSlash } from '@/lib/url' | ||
import serialize from './serial' | ||
import serialize, { InsufficientFundsError } from './serial' | ||
import { decodeCursor, LIMIT, nextCursorEncoded } from '@/lib/cursor' | ||
import { getMetadata, metadataRuleSets } from 'page-metadata-parser' | ||
import { ruleSet as publicationDateRuleSet } from '@/lib/timedate-scraper' | ||
|
@@ -9,7 +9,7 @@ import { | |
ITEM_SPAM_INTERVAL, ITEM_FILTER_THRESHOLD, | ||
COMMENT_DEPTH_LIMIT, COMMENT_TYPE_QUERY, | ||
ANON_USER_ID, ANON_ITEM_SPAM_INTERVAL, POLL_COST, | ||
ITEM_ALLOW_EDITS, GLOBAL_SEED, ANON_FEE_MULTIPLIER, NOFOLLOW_LIMIT, UNKNOWN_LINK_REL | ||
ITEM_ALLOW_EDITS, GLOBAL_SEED, ANON_FEE_MULTIPLIER, NOFOLLOW_LIMIT, UNKNOWN_LINK_REL, ANON_INV_PENDING_LIMIT, ANON_BALANCE_LIMIT_MSATS, INV_PENDING_LIMIT, USER_IDS_BALANCE_NO_LIMIT, BALANCE_LIMIT_MSATS, DEFAULT_INVOICE_TIMEOUT_MS | ||
} from '@/lib/constants' | ||
import { msatsToSats } from '@/lib/format' | ||
import { parse } from 'tldts' | ||
|
@@ -21,6 +21,9 @@ import { datePivot, whenRange } from '@/lib/time' | |
import { imageFeesInfo, uploadIdsFromText } from './image' | ||
import assertGofacYourself from './ofac' | ||
import assertApiKeyNotPermitted from './apiKey' | ||
import { createInvoice } from 'ln-service' | ||
import { Prisma } from '@prisma/client' | ||
import { createHmac } from './wallet' | ||
|
||
function commentsOrderByClause (me, models, sort) { | ||
if (sort === 'recent') { | ||
|
@@ -51,26 +54,29 @@ function commentsOrderByClause (me, models, sort) { | |
async function comments (me, models, id, sort) { | ||
const orderBy = commentsOrderByClause(me, models, sort) | ||
|
||
const filter = '' // empty filter as we filter clientside now | ||
let filter = 'AND "Item"."status" <> \'FAILED\'' | ||
if (me) { | ||
filter += `AND ("Item"."userId" = ${me.id} OR "Item"."status" <> 'PENDING')` | ||
const [{ item_comments_zaprank_with_me: comments }] = await models.$queryRawUnsafe( | ||
'SELECT item_comments_zaprank_with_me($1::INTEGER, $2::INTEGER, $3::INTEGER, $4::INTEGER, $5, $6)', Number(id), GLOBAL_SEED, Number(me.id), COMMENT_DEPTH_LIMIT, filter, orderBy) | ||
return comments | ||
} | ||
|
||
filter += 'AND "Item"."status" <> \'PENDING\'' | ||
Comment on lines
+57
to
+65
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Optimize the SQL query to prevent potential performance issues with large datasets. Consider using more efficient SQL constructs or adding appropriate indexes to improve the performance of this query. |
||
const [{ item_comments: comments }] = await models.$queryRawUnsafe( | ||
'SELECT item_comments($1::INTEGER, $2::INTEGER, $3, $4)', Number(id), COMMENT_DEPTH_LIMIT, filter, orderBy) | ||
return comments | ||
} | ||
|
||
export async function getItem (parent, { id }, { me, models }) { | ||
export async function getItem (parent, { id, status = true }, { me, models }) { | ||
const [item] = await itemQueryWithMeta({ | ||
me, | ||
models, | ||
query: ` | ||
${SELECT} | ||
FROM "Item" | ||
WHERE id = $1` | ||
WHERE id = $1 | ||
${status ? `AND ${statusClause(me)}` : ''}` | ||
Comment on lines
+71
to
+79
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tip Codebase Verification The search results indicate multiple instances where the To ensure the Action Required:
Suggested Code Change:export async function getItem (parent, { id, status = true }, { me, models }) {
// Validate the status parameter
const validStatuses = ['ACTIVE', 'PENDING', 'FAILED', 'STOPPED'];
if (status && !validStatuses.includes(status)) {
throw new Error('Invalid status parameter');
}
const [item] = await itemQueryWithMeta({
me,
models,
query: `
${SELECT}
FROM "Item"
WHERE id = $1
${status ? `AND ${statusClause(me)}` : ''}`
}, Number(id));
return item;
} Locations to Update:
Analysis chainValidate the Scripts ExecutedThe following scripts were executed for the analysis: Script: #!/bin/bash
# Description: Check for validation of `status` parameter in other parts of the codebase.
# Test: Search for validation patterns for `status`.
rg --type python 'status' --context 5
Length of output: 68 Script: #!/bin/bash
# Description: Check for validation of `status` parameter in other parts of the codebase.
# Test: Search for validation patterns for `status` in JavaScript files.
rg --type js 'status' --context 5
Length of output: 139868 |
||
}, Number(id)) | ||
return item | ||
} | ||
|
@@ -251,6 +257,18 @@ export async function filterClause (me, models, type) { | |
return [freebieClause, outlawClause] | ||
} | ||
|
||
export function statusClause (me) { | ||
let filter = '"Item"."status" <> \'FAILED\' AND ' | ||
|
||
if (me) { | ||
filter += `("Item"."userId" = ${me.id} OR "Item"."status" <> 'PENDING')` | ||
} else { | ||
filter += '"Item"."status" <> \'PENDING\'' | ||
} | ||
|
||
return filter | ||
} | ||
Comment on lines
+260
to
+270
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Refactor the Consider breaking down the function into smaller, more manageable parts or using helper functions to handle different conditions. |
||
|
||
function typeClause (type) { | ||
switch (type) { | ||
case 'links': | ||
|
@@ -344,7 +362,8 @@ export default { | |
await filterClause(me, models, type), | ||
nsfwClause(showNsfw), | ||
typeClause(type), | ||
whenClause(when || 'forever', table))} | ||
whenClause(when || 'forever', table), | ||
statusClause(me))} | ||
${orderByClause(by, me, models, type)} | ||
OFFSET $4 | ||
LIMIT $5`, | ||
|
@@ -364,7 +383,8 @@ export default { | |
activeOrMine(me), | ||
await filterClause(me, models, type), | ||
typeClause(type), | ||
muteClause(me) | ||
muteClause(me), | ||
statusClause(me) | ||
)} | ||
ORDER BY "Item".created_at DESC | ||
OFFSET $2 | ||
|
@@ -388,7 +408,8 @@ export default { | |
typeClause(type), | ||
whenClause(when, 'Item'), | ||
await filterClause(me, models, type), | ||
muteClause(me))} | ||
muteClause(me), | ||
statusClause(me))} | ||
ORDER BY rank DESC | ||
OFFSET $3 | ||
LIMIT $4`, | ||
|
@@ -407,7 +428,8 @@ export default { | |
typeClause(type), | ||
whenClause(when, 'Item'), | ||
await filterClause(me, models, type), | ||
muteClause(me))} | ||
muteClause(me), | ||
statusClause(me))} | ||
${orderByClause(by || 'zaprank', me, models, type)} | ||
OFFSET $3 | ||
LIMIT $4`, | ||
|
@@ -463,7 +485,8 @@ export default { | |
'"Item"."parentId" IS NULL', | ||
'"Item".bio = false', | ||
subClause(sub, 3, 'Item', me, showNsfw), | ||
muteClause(me))} | ||
muteClause(me), | ||
statusClause(me))} | ||
ORDER BY rank DESC | ||
OFFSET $1 | ||
LIMIT $2`, | ||
|
@@ -482,6 +505,7 @@ export default { | |
${whereClause( | ||
subClause(sub, 3, 'Item', me, showNsfw), | ||
muteClause(me), | ||
statusClause(me), | ||
// in "home" (sub undefined), we want to show pinned items (but without the pin icon) | ||
sub ? '"Item"."pinId" IS NULL' : '', | ||
'"Item"."deletedAt" IS NULL', | ||
|
@@ -514,7 +538,8 @@ export default { | |
'"pinId" IS NOT NULL', | ||
'"parentId" IS NULL', | ||
sub ? '"subName" = $1' : '"subName" IS NULL', | ||
muteClause(me))} | ||
muteClause(me), | ||
statusClause(me))} | ||
) rank_filter WHERE RANK = 1 | ||
ORDER BY position ASC`, | ||
orderBy: 'ORDER BY position ASC' | ||
|
@@ -899,7 +924,7 @@ export default { | |
WHERE act IN ('TIP', 'FEE') | ||
AND "itemId" = ${Number(id)}::INTEGER | ||
AND "userId" = ${me.id}::INTEGER)::INTEGER)`, | ||
{ models } | ||
{ models, lnd, hash, hmac } | ||
) | ||
} else { | ||
await serialize( | ||
|
@@ -1353,26 +1378,81 @@ export const createItem = async (parent, { forward, options, ...item }, { me, mo | |
fee = sub.baseCost * ANON_FEE_MULTIPLIER + (item.boost || 0) | ||
} | ||
} | ||
fee += imgFees; | ||
fee += imgFees | ||
|
||
([item] = await serialize( | ||
models.$queryRawUnsafe( | ||
try { | ||
([item] = await serialize( | ||
models.$queryRawUnsafe( | ||
`${SELECT} FROM create_item($1::JSONB, $2::JSONB, $3::JSONB, '${spamInterval}'::INTERVAL, $4::INTEGER[]) AS "Item"`, | ||
JSON.stringify(item), JSON.stringify(fwdUsers), JSON.stringify(options), uploadIds), | ||
{ models, lnd, me, hash, hmac, fee } | ||
)) | ||
{ models, lnd, me, hash, hmac, fee } | ||
)) | ||
} catch (err) { | ||
// post payment flow is only allowed for stackers, not anons since we can't show pending items to anons | ||
if (!me || !(err instanceof InsufficientFundsError)) { | ||
throw err | ||
} | ||
|
||
await createMentions(item, models) | ||
// create invoice and insert as pending item | ||
const invLimit = me ? INV_PENDING_LIMIT : ANON_INV_PENDING_LIMIT | ||
const balanceLimit = USER_IDS_BALANCE_NO_LIMIT.includes(Number(me?.id)) ? 0 : me ? BALANCE_LIMIT_MSATS : ANON_BALANCE_LIMIT_MSATS | ||
const description = 'Creating item on stacker.news' | ||
// TODO: allow users to configure expiration | ||
const expiresAt = datePivot(new Date(), { milliseconds: DEFAULT_INVOICE_TIMEOUT_MS }) | ||
// we don't do partial payments yet, users need to pay full amount via invoice | ||
const mtokens = err.cost | ||
|
||
const lndInv = await createInvoice({ | ||
description: me.hideInvoiceDesc ? undefined : description, | ||
lnd, | ||
mtokens, | ||
expires_at: expiresAt | ||
}) | ||
|
||
await enqueueDeletionJob(item, models) | ||
let invoice | ||
// need to use interactive tx here to set invoice.actionId = item.id | ||
await models.$transaction( | ||
async (tx) => { | ||
// insert pending item | ||
([item] = await tx.$queryRawUnsafe( | ||
`${SELECT} FROM create_item($1::JSONB, $2::JSONB, $3::JSONB, '${spamInterval}'::INTERVAL, $4::INTEGER[]) AS "Item"`, | ||
JSON.stringify({ ...item, status: 'PENDING' }), JSON.stringify(fwdUsers), JSON.stringify(options), uploadIds)) | ||
|
||
// set required invoice data to update item on payment | ||
const actionData = { cost: err.cost, credits: err.balance }; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If these fields need to be saved, might we do better storing them as columns with types and naming them in the db layer? |
||
([invoice] = await tx.$queryRaw` | ||
SELECT * FROM create_invoice(${lndInv.id}, NULL, ${lndInv.request}, | ||
${expiresAt}::timestamp, ${mtokens}, ${item.userId}::INTEGER, ${description}, NULL, NULL, | ||
${invLimit}::INTEGER, ${balanceLimit}, 'ITEM'::"ActionType", ${item.id}::INTEGER, ${JSON.stringify(actionData)}::JSONB)`) | ||
|
||
// since SubscribeToInvoices does not trigger if an invoice expired, we need a job that handles expired invoices | ||
await tx.$queryRaw` | ||
INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter) | ||
VALUES ('finalizeAction', jsonb_build_object('hash', ${invoice.hash}::TEXT), 21, true, ${expiresAt})` | ||
}, { isolationLevel: Prisma.TransactionIsolationLevel.Serializable }) | ||
|
||
// hmac is not required to submit action again but to allow user to cancel payment | ||
invoice.hmac = createHmac(invoice.hash) | ||
|
||
item.invoice = invoice | ||
} | ||
|
||
item.comments = [] | ||
|
||
// we need to insert these even for pending items to show toasts | ||
// we will delete the jobs if payment failed | ||
await enqueueDeletionJob(item, models) | ||
await createReminderAndJob({ me, item, models }) | ||
|
||
notifyUserSubscribers({ models, item }) | ||
const isPending = !!item.invoice | ||
if (isPending) { | ||
return item | ||
} | ||
|
||
await createMentions(item, models) | ||
notifyUserSubscribers({ models, item }) | ||
notifyTerritorySubscribers({ models, item }) | ||
|
||
item.comments = [] | ||
return item | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
import { GraphQLError } from 'graphql' | ||
import { decodeCursor, LIMIT, nextNoteCursorEncoded } from '@/lib/cursor' | ||
import { getItem, filterClause, whereClause, muteClause } from './item' | ||
import { getItem, filterClause, whereClause, muteClause, statusClause } from './item' | ||
import { getInvoice, getWithdrawl } from './wallet' | ||
import { pushSubscriptionSchema, ssValidate } from '@/lib/validate' | ||
import { replyToSubscription } from '@/lib/webPush' | ||
|
@@ -151,7 +151,8 @@ export default { | |
${whereClause( | ||
'"Item".created_at < $2', | ||
await filterClause(me, models), | ||
muteClause(me))} | ||
muteClause(me), | ||
statusClause(me))} | ||
ORDER BY id ASC, CASE | ||
WHEN type = 'Mention' THEN 1 | ||
WHEN type = 'Reply' THEN 2 | ||
|
@@ -216,6 +217,7 @@ export default { | |
WHERE "Invoice"."userId" = $1 | ||
AND "confirmedAt" IS NOT NULL | ||
AND "isHeld" IS NULL | ||
AND "actionType" IS NULL | ||
AND created_at < $2 | ||
ORDER BY "sortTime" DESC | ||
LIMIT ${LIMIT})` | ||
|
@@ -313,6 +315,16 @@ export default { | |
LIMIT ${LIMIT})` | ||
) | ||
|
||
queries.push( | ||
`(SELECT "Item".id::text, "Item"."created_at" AS "sortTime", NULL as "earnedSats", 'FailedItem' AS type | ||
FROM "Item" | ||
WHERE "Item"."userId" = $1 | ||
AND "Item"."created_at" < $2 | ||
AND "Item"."status" = 'FAILED' | ||
ORDER BY "sortTime" DESC | ||
LIMIT ${LIMIT})` | ||
) | ||
|
||
const notifications = await models.$queryRawUnsafe( | ||
`SELECT id, "sortTime", "earnedSats", type, | ||
"sortTime" AS "minSortTime" | ||
|
@@ -396,6 +408,9 @@ export default { | |
return await getItem(n, { id: itemId }, { models, me }) | ||
} | ||
}, | ||
FailedItem: { | ||
item: async (n, args, { models, me }) => getItem(n, { id: n.id, status: false }, { models, me }) | ||
}, | ||
Comment on lines
+412
to
+413
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Review the handling of failed items in resolvers. The resolver for - getItem(n, { id: n.id, status: false }, { models, me })
+ // Correct the status condition based on intended logic
|
||
TerritoryTransfer: { | ||
sub: async (n, args, { models, me }) => { | ||
const transfer = await models.territoryTransfer.findUnique({ where: { id: Number(n.id) }, include: { sub: true } }) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,7 +4,7 @@ import { GraphQLError } from 'graphql' | |
import { decodeCursor, LIMIT, nextCursorEncoded } from '@/lib/cursor' | ||
import { msatsToSats } from '@/lib/format' | ||
import { bioSchema, emailSchema, settingsSchema, ssValidate, userSchema } from '@/lib/validate' | ||
import { getItem, updateItem, filterClause, createItem, whereClause, muteClause } from './item' | ||
import { getItem, updateItem, filterClause, createItem, whereClause, muteClause, statusClause } from './item' | ||
import { ANON_USER_ID, DELETE_USER_ID, RESERVED_MAX_USER_ID, SN_NO_REWARDS_IDS } from '@/lib/constants' | ||
import { viewGroup } from './growth' | ||
import { timeUnitForRange, whenRange } from '@/lib/time' | ||
|
@@ -285,6 +285,7 @@ export default { | |
'r.created_at >= "ThreadSubscription".created_at', | ||
await filterClause(me, models), | ||
muteClause(me), | ||
statusClause(me), | ||
...(user.noteAllDescendants ? [] : ['r.level = 1']) | ||
)})`, me.id, lastChecked) | ||
if (newThreadSubReply.exists) { | ||
|
@@ -305,7 +306,8 @@ export default { | |
OR ("Item"."parentId" IS NOT NULL AND "UserSubscription"."commentsSubscribedAt" IS NOT NULL AND "Item".created_at >= "UserSubscription"."commentsSubscribedAt") | ||
)`, | ||
await filterClause(me, models), | ||
muteClause(me))})`, me.id, lastChecked) | ||
muteClause(me), | ||
statusClause(me))})`, me.id, lastChecked) | ||
if (newUserSubs.exists) { | ||
foundNotes() | ||
return true | ||
|
@@ -339,7 +341,8 @@ export default { | |
'"Mention".created_at > $2', | ||
'"Item"."userId" <> $1', | ||
await filterClause(me, models), | ||
muteClause(me) | ||
muteClause(me), | ||
statusClause(me) | ||
)})`, me.id, lastChecked) | ||
if (newMentions.exists) { | ||
foundNotes() | ||
|
@@ -503,6 +506,20 @@ export default { | |
return true | ||
} | ||
|
||
const newFailedItems = await models.item.findFirst({ | ||
where: { | ||
userId: me.id, | ||
status: 'FAILED', | ||
updatedAt: { | ||
gt: lastChecked | ||
} | ||
} | ||
}) | ||
if (newFailedItems) { | ||
foundNotes() | ||
return true | ||
} | ||
Comment on lines
+509
to
+521
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider optimizing the query for checking new failed items. The current implementation performs a query for each type of notification. This could potentially be optimized by combining some of these queries into a single query, especially if they are checking similar conditions or can be grouped logically. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This comment reminds me that I probably need to manually update There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ekzyis, you're absolutely right about the need to manually update the If you're updating multiple records or have multiple places where this needs to be handled, you might consider abstracting this logic into a helper function or middleware to keep your code DRY and maintainable. Here’s a quick example of how you might implement such a function in JavaScript: const updateWithTimestamp = async (model, where, data) => {
const updatedAt = new Date();
const updatedData = { ...data, updatedAt };
return await model.update({ where, data: updatedData });
}; This function takes the model, the conditions ( Let me know if you need further assistance with this or any other part of your implementation! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mhh, even though I don't see a trigger that does this, These are the existing item triggers:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ekzyis, it's interesting to see that the
If you need to confirm or inspect this behavior further, you might consider checking the definition of the Let me know if you need more detailed information on this or any other queries! |
||
|
||
// update checkedNotesAt to prevent rechecking same time period | ||
models.user.update({ | ||
where: { id: me.id }, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tip
Codebase Verification
Ensure proper error handling for external calls to
createInvoice
.The search results indicate multiple instances of
createInvoice
calls across various files. However, not all instances show explicit error handling. Specifically, inapi/resolvers/item.js
, there is no clear indication of error handling around thecreateInvoice
call.api/resolvers/item.js
: Ensure that thecreateInvoice
call is wrapped in a try-catch block or has appropriate error handling mechanisms.Analysis chain
Ensure proper error handling for external calls to
createInvoice
.Scripts Executed
The following scripts were executed for the analysis:
Script:
Length of output: 75
Script:
Length of output: 13248