Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions .github/workflows/worker-preview.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
name: Worker Preview

on:
pull_request:
types: [opened, synchronize, reopened, closed]

permissions:
contents: read
pull-requests: write

jobs:
preview:
if: github.event.action != 'closed' && github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile --prefer-offline
- run: cp wrangler.example.jsonc wrangler.jsonc
- name: Configure preview auth URL
env:
PREVIEW_NAME: pr-${{ github.event.pull_request.number }}
run: |
preview_url="https://${PREVIEW_NAME}-splitpro.andrepascoa.workers.dev"
sed -i "s|https://replace-with-this-preview-url|${preview_url}|g" wrangler.jsonc
- run: SKIP_ENV_VALIDATION=1 pnpm build
- name: Apply preview database migrations
run: pnpm exec wrangler d1 migrations apply DB --remote --preview --config wrangler.jsonc
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_PREVIEW_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: 4c52a530606031af4f6ca06e32ca1ec5
- name: Deploy Worker Preview
id: preview
run: |
output="$(pnpm exec wrangler preview --name "pr-${{ github.event.pull_request.number }}" --json)"
printf '%s\n' "$output"
url="$(printf '%s' "$output" | node -e 'let output = ""; process.stdin.on("data", chunk => output += chunk); process.stdin.on("end", () => { const match = output.match(/"preview":\s*\{[\s\S]*?"urls":\s*\[\s*"([^"]+)"/); if (!match) process.exit(1); process.stdout.write(match[1]); });')"
echo "url=$url" >> "$GITHUB_OUTPUT"
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_PREVIEW_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: 4c52a530606031af4f6ca06e32ca1ec5
- name: Add or update preview comment
uses: actions/github-script@v7
env:
PREVIEW_URL: ${{ steps.preview.outputs.url }}
with:
script: |
const marker = '<!-- worker-preview -->';
const body = `${marker}\n🚀 Worker preview: ${process.env.PREVIEW_URL}`;
const { owner, repo } = context.repo;
const issue_number = context.issue.number;
const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number });
const existing = comments.find((comment) => comment.body?.includes(marker));
if (existing) {
await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body });
} else {
await github.rest.issues.createComment({ owner, repo, issue_number, body });
}

cleanup:
if: github.event.action == 'closed' && github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile --prefer-offline
- run: cp wrangler.example.jsonc wrangler.jsonc
- run: pnpm exec wrangler preview delete --name "pr-${{ github.event.pull_request.number }}" --skip-confirmation
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_PREVIEW_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: 4c52a530606031af4f6ca06e32ca1ec5
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,29 @@ setting belongs only in `previews.vars`, not production `vars`.
- `drizzle-kit` and `wrangler d1 migrations` own the schema. `migrations/0001_balance_view_and_triggers.sql`
is hand-written (the `BalanceView` view and a trigger). Back it up before regenerating migrations.

## Backups

- **Time Travel:** D1 keeps point-in-time history for 30 days on the Workers Paid plan.
Restore with `npx wrangler d1 time-travel restore splitpro --timestamp=<unix>`.
- **Daily backup to R2:** the `splitpro-backup` Workflow (03:00 UTC) exports D1 through the REST API
to `d1/splitpro-<date>-<time>.sql`, then writes a JSON manifest beside it. It also paginates the
`UPLOADS` bucket and copies new or changed objects to `uploads/<original-key>` in the private
`splitpro-backups` bucket, preserving HTTP and custom metadata. Existing upload copies are retained
even if their source object is later deleted, so R2 upload backups are not pruned automatically.
SQL dumps and manifests older than `BACKUP_RETENTION_DAYS` (default 90) are pruned. The D1 export
endpoint may block database requests while an export is running. It needs the `D1_EXPORT_TOKEN`
secret (an API token with **D1 Write**; the endpoint rejects D1 Read), and the `CF_ACCOUNT_ID` and
`D1_DATABASE_ID` vars.
- **Restore:** load a SQL dump into a database with
`npx wrangler d1 execute <db> --remote --file <dump>.sql`, then copy objects under the backup
bucket's `uploads/` prefix back to `UPLOADS`, removing that prefix from each key. The application
code, migrations, and non-secret Worker configuration are in Git. Recreate Worker secrets from a
secure password manager; they cannot be recovered from a database or R2 dump.
- Both buckets are in the same Cloudflare account. This protects against application-level deletion
from `UPLOADS`, but not account compromise or account-wide deletion; keep an additional copy in a
separate account or provider for that threat model.
- Trigger one by hand: `npx wrangler workflows trigger splitpro-backup`.

## Tests

```sh
Expand Down
6 changes: 5 additions & 1 deletion src/components/AddExpense/AddBankTransactions.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useCallback } from 'react';
import { useInvalidateExpenseQueries } from '~/hooks/useInvalidateExpenseQueries';
import { calculateParticipantSplit, useAddExpenseStore } from '~/store/addStore';
import type { TransactionAddInputModel } from '~/types';
import { BankingTransactionList } from './BankTransactions/BankingTransactionList';
Expand Down Expand Up @@ -35,6 +36,7 @@ const AddBankTransactions: React.FC<{
} = useAddExpenseStore((s) => s.actions);

const addExpenseMutation = api.expense.addOrEditExpense.useMutation();
const invalidateExpenseQueries = useInvalidateExpenseQueries();

const router = useRouter();

Expand Down Expand Up @@ -99,7 +101,8 @@ const AddBankTransactions: React.FC<{
}) as CreateExpense[];

await addExpenseMutation.mutateAsync(expenses, {
onSuccess: () => {
onSuccess: async () => {
await invalidateExpenseQueries();
setMultipleTransactions([]);
setIsTransactionLoading(false);
router.back();
Expand All @@ -114,6 +117,7 @@ const AddBankTransactions: React.FC<{
router,
resetState,
addExpenseMutation,
invalidateExpenseQueries,
group,
paidBy,
splitType,
Expand Down
7 changes: 6 additions & 1 deletion src/components/AddExpense/AddExpensePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useRouter } from 'next/router';
import React, { useCallback } from 'react';

import { type CurrencyCode } from '~/lib/currency';
import { useInvalidateExpenseQueries } from '~/hooks/useInvalidateExpenseQueries';
import { useAddExpenseStore } from '~/store/addStore';
import { api } from '~/utils/api';

Expand Down Expand Up @@ -72,6 +73,7 @@ export const AddOrEditExpensePage: React.FC<{
} = useAddExpenseStore((s) => s.actions);

const addExpenseMutation = api.expense.addOrEditExpense.useMutation();
const invalidateExpenseQueries = useInvalidateExpenseQueries();
const updateProfile = api.user.updateUserDetail.useMutation();
const { update } = useSession();

Expand Down Expand Up @@ -137,8 +139,10 @@ export const AddOrEditExpensePage: React.FC<{
},
],
{
onSuccess: (d) => {
onSuccess: async (d) => {
if (d) {
await invalidateExpenseQueries();

if (multipleTransactions.length > 0) {
const allTransactions = [...multipleTransactions];
const transactionToAdd = allTransactions.pop();
Expand Down Expand Up @@ -198,6 +202,7 @@ export const AddOrEditExpensePage: React.FC<{
expenseId,
router,
addExpenseMutation,
invalidateExpenseQueries,
group,
paidBy,
splitType,
Expand Down
5 changes: 4 additions & 1 deletion src/components/Expense/DeleteExpense.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useTranslation } from 'next-i18next';
import { toast } from 'sonner';

import { extractTemplateExpenseId } from '~/lib/cron';
import { useInvalidateExpenseQueries } from '~/hooks/useInvalidateExpenseQueries';
import { api } from '~/utils/api';

import { Button } from '../ui/button';
Expand All @@ -26,6 +27,7 @@ export const DeleteExpense: React.FC<DeleteExpenseProps> = ({ expenseId, recurre
const router = useRouter();

const deleteExpenseMutation = api.expense.deleteExpense.useMutation();
const invalidateExpenseQueries = useInvalidateExpenseQueries();

const isTemplate = useMemo(
() => (recurrence ? extractTemplateExpenseId(recurrence.job.command) === expenseId : false),
Expand All @@ -36,6 +38,7 @@ export const DeleteExpense: React.FC<DeleteExpenseProps> = ({ expenseId, recurre
const onDeleteExpense = useCallback(async () => {
try {
await deleteExpenseMutation.mutateAsync({ expenseId });
await invalidateExpenseQueries();
} catch (error) {
if (error instanceof Error) {
toast.error(`Error: ${error.message}`);
Expand All @@ -46,7 +49,7 @@ export const DeleteExpense: React.FC<DeleteExpenseProps> = ({ expenseId, recurre
return;
}
router.back();
}, [expenseId, deleteExpenseMutation, router]);
}, [expenseId, deleteExpenseMutation, invalidateExpenseQueries, router]);

const description = useMemo(() => {
if (!isPartOfRecurrence) {
Expand Down
18 changes: 14 additions & 4 deletions src/components/Expense/ExpenseDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import React, { type ComponentProps, useCallback, useMemo, useState } from 'reac
import { toast } from 'sonner';

import { useIntlCronParser } from '~/hooks/useIntlCronParser';
import { useInvalidateExpenseQueries } from '~/hooks/useInvalidateExpenseQueries';
import { useTranslationWithUtils } from '~/hooks/useTranslationWithUtils';
import { cronFromBackend } from '~/lib/cron';
import { DEFAULT_CATEGORY } from '~/lib/category';
Expand Down Expand Up @@ -272,7 +273,7 @@ export const EditSettlement: React.FC<{ expense: ExpenseDetailsOutput }> = ({ ex
);

const addExpenseMutation = api.expense.addOrEditExpense.useMutation();
const apiUtils = api.useUtils();
const invalidateExpenseQueries = useInvalidateExpenseQueries();

const onCurrencyInputValueChange = useCallback(
({ strValue, bigIntValue }: { strValue?: string; bigIntValue?: bigint }) => {
Expand Down Expand Up @@ -314,16 +315,25 @@ export const EditSettlement: React.FC<{ expense: ExpenseDetailsOutput }> = ({ ex
expenseDate,
},
{
onSuccess: () => {
apiUtils.invalidate().catch(console.error);
onSuccess: async () => {
await invalidateExpenseQueries();
},
onError: (error) => {
console.error('Error while saving expense:', error);
toast.error(t('errors.saving_expense'));
},
},
);
}, [amount, sender, receiver, expense, addExpenseMutation, expenseDate, apiUtils, t]);
}, [
amount,
sender,
receiver,
expense,
addExpenseMutation,
expenseDate,
invalidateExpenseQueries,
t,
]);

if (!sender || !receiver) {
return null;
Expand Down
18 changes: 14 additions & 4 deletions src/components/Friend/GroupSettleup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { SplitType, type User } from '~/server/db/types';
import { ArrowRightIcon } from 'lucide-react';
import React, { type ReactNode, useState } from 'react';
import { toast } from 'sonner';
import { useInvalidateExpenseQueries } from '~/hooks/useInvalidateExpenseQueries';
import { useTranslationWithUtils } from '~/hooks/useTranslationWithUtils';
import { DEFAULT_CATEGORY } from '~/lib/category';
import { api } from '~/utils/api';
Expand Down Expand Up @@ -38,7 +39,7 @@ export const GroupSettleUp: React.FC<{
);

const addExpenseMutation = api.expense.addOrEditExpense.useMutation();
const utils = api.useUtils();
const invalidateExpenseQueries = useInvalidateExpenseQueries();

const sender = 0 > _amount ? user : friend;
const receiver = 0 > _amount ? friend : user;
Expand Down Expand Up @@ -69,16 +70,25 @@ export const GroupSettleUp: React.FC<{
category: DEFAULT_CATEGORY,
},
{
onSuccess: () => {
utils.group.invalidate().catch(console.error);
onSuccess: async () => {
await invalidateExpenseQueries();
},
onError: (error) => {
console.error('Error while saving expense:', error);
toast.error(t('errors.saving_expense'));
},
},
);
}, [sender, receiver, amount, utils, addExpenseMutation, currency, groupId, t]);
}, [
sender,
receiver,
amount,
invalidateExpenseQueries,
addExpenseMutation,
currency,
groupId,
t,
]);

return (
<AppDrawer
Expand Down
Loading
Loading