Initial donationReceipts data types

This commit is contained in:
Scott Nonnenberg
2025-06-17 05:33:00 +10:00
committed by GitHub
parent 4347964030
commit 9ffee9d290
15 changed files with 472 additions and 5 deletions

View File

@@ -3,6 +3,9 @@
import type { Database } from '@signalapp/sqlcipher';
import type { ReadonlyDeep } from 'type-fest';
import { strictAssert } from '../util/assert';
import type {
ConversationAttributesType,
MessageAttributesType,
@@ -49,7 +52,7 @@ import type { SyncTaskType } from '../util/syncTasks';
import type { AttachmentBackupJobType } from '../types/AttachmentBackup';
import type { GifType } from '../components/fun/panels/FunPanelGifs';
import type { NotificationProfileType } from '../types/NotificationProfile';
import { strictAssert } from '../util/assert';
import type { DonationReceipt } from '../types/Donations';
export type ReadableDB = Database & { __readable_db: never };
export type WritableDB = ReadableDB & { __writable_db: never };
@@ -876,6 +879,9 @@ type ReadableInterface = {
getAllNotificationProfiles(): Array<NotificationProfileType>;
getNotificationProfileById(id: string): NotificationProfileType | undefined;
getAllDonationReceipts(): Array<DonationReceipt>;
getDonationReceiptById(id: string): DonationReceipt | undefined;
getMessagesNeedingUpgrade: (
limit: number,
options: { maxVersion: number }
@@ -1185,6 +1191,10 @@ type WritableInterface = {
createNotificationProfile(profile: NotificationProfileType): void;
updateNotificationProfile(profile: NotificationProfileType): void;
_deleteAllDonationReceipts(): void;
deleteDonationReceiptById(id: string): void;
createDonationReceipt(profile: DonationReceipt): void;
removeAll: () => void;
removeAllConfiguration: () => void;
eraseStorageServiceState: () => void;

View File

@@ -218,6 +218,13 @@ import {
updateCallLinkState,
updateDefunctCallLink,
} from './server/callLinks';
import {
_deleteAllDonationReceipts,
createDonationReceipt,
deleteDonationReceiptById,
getAllDonationReceipts,
getDonationReceiptById,
} from './server/donationReceipts';
import {
deleteAllEndorsementsForGroup,
getGroupSendCombinedEndorsementExpiration,
@@ -391,6 +398,9 @@ export const DataReader: ServerReadableInterface = {
getAllNotificationProfiles,
getNotificationProfileById,
getAllDonationReceipts,
getDonationReceiptById,
callLinkExists,
defunctCallLinkExists,
getAllCallLinks,
@@ -624,6 +634,10 @@ export const DataWriter: ServerWritableInterface = {
markNotificationProfileDeleted,
updateNotificationProfile,
_deleteAllDonationReceipts,
deleteDonationReceiptById,
createDonationReceipt,
removeAll,
removeAllConfiguration,
eraseStorageServiceState,
@@ -7495,6 +7509,7 @@ function removeAll(db: WritableDB): void {
DELETE FROM callsHistory;
DELETE FROM conversations;
DELETE FROM defunctCallLinks;
DELETE FROM donationReceipts;
DELETE FROM emojis;
DELETE FROM groupCallRingCancellations;
DELETE FROM groupSendCombinedEndorsement;

View File

@@ -0,0 +1,35 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { LoggerType } from '../../types/Logging';
import type { WritableDB } from '../Interface';
export const version = 1380;
export function updateToSchemaVersion1380(
currentVersion: number,
db: WritableDB,
logger: LoggerType
): void {
if (currentVersion >= 1380) {
return;
}
db.transaction(() => {
db.exec(`
CREATE TABLE donationReceipts(
id TEXT NOT NULL PRIMARY KEY,
currencyType TEXT NOT NULL,
paymentAmount INTEGER NOT NULL,
paymentDetailJson TEXT NOT NULL,
paymentType TEXT NOT NULL,
timestamp INTEGER NOT NULL
) STRICT;
CREATE INDEX donationReceipts_byTimestamp on donationReceipts(timestamp);
`);
db.pragma('user_version = 1380');
})();
logger.info('updateToSchemaVersion1380: success!');
}

View File

@@ -112,10 +112,11 @@ import { updateToSchemaVersion1330 } from './1330-sync-tasks-type-index';
import { updateToSchemaVersion1340 } from './1340-recent-gifs';
import { updateToSchemaVersion1350 } from './1350-notification-profiles';
import { updateToSchemaVersion1360 } from './1360-attachments';
import { updateToSchemaVersion1370 } from './1370-message-attachment-indexes';
import {
updateToSchemaVersion1370,
updateToSchemaVersion1380,
version as MAX_VERSION,
} from './1370-message-attachment-indexes';
} from './1380-donation-receipts';
import { DataWriter } from '../Server';
@@ -2106,6 +2107,7 @@ export const SCHEMA_VERSIONS = [
updateToSchemaVersion1350,
updateToSchemaVersion1360,
updateToSchemaVersion1370,
updateToSchemaVersion1380,
];
export class DBVersionFromFutureError extends Error {

View File

@@ -0,0 +1,108 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { omit } from 'lodash';
import * as Errors from '../../types/errors';
import { safeParseLoose } from '../../util/schemas';
import { sql } from '../util';
import { sqlLogger } from '../sqlLogger';
import { donationReceiptSchema } from '../../types/Donations';
import type { DonationReceipt } from '../../types/Donations';
import type { ReadableDB, WritableDB } from '../Interface';
type DonationReceiptForDatabase = Readonly<
{
paymentDetailJson: string;
paymentType: string;
} & Omit<DonationReceipt, 'paymentType' | 'paymentDetail'>
>;
function hydrateDonationReceipt(
receipt: DonationReceiptForDatabase
): DonationReceipt {
const readyForParse = {
...omit(receipt, ['paymentDetailJson']),
paymentDetail: JSON.parse(receipt.paymentDetailJson),
};
const result = safeParseLoose(donationReceiptSchema, readyForParse);
if (result.success) {
return result.data;
}
sqlLogger.error(
`hydrateDonationReceipt: Parse failed for payment type ${readyForParse.paymentType}:`,
Errors.toLogFormat(result.error)
);
const toFix = readyForParse as unknown as DonationReceipt;
toFix.paymentDetail = null;
return toFix;
}
export function freezeDonationReceipt(
receipt: DonationReceipt
): DonationReceiptForDatabase {
return {
...omit(receipt, ['paymentDetail']),
paymentDetailJson: JSON.stringify(receipt.paymentDetail),
};
}
export function getAllDonationReceipts(db: ReadableDB): Array<DonationReceipt> {
const donationReceipts = db
.prepare('SELECT * FROM donationReceipts ORDER BY timestamp DESC;')
.all<DonationReceiptForDatabase>();
return donationReceipts.map(hydrateDonationReceipt);
}
export function getDonationReceiptById(
db: ReadableDB,
id: string
): DonationReceipt | undefined {
const [query, parameters] =
sql`SELECT * FROM donationReceipts WHERE id = ${id}`;
const fromDatabase = db
.prepare(query)
.get<DonationReceiptForDatabase>(parameters);
if (fromDatabase) {
return hydrateDonationReceipt(fromDatabase);
}
return undefined;
}
export function _deleteAllDonationReceipts(db: WritableDB): void {
db.prepare('DELETE FROM donationReceipts;').run();
}
export function deleteDonationReceiptById(db: WritableDB, id: string): void {
const [query, parameters] =
sql`DELETE FROM donationReceipts WHERE id = ${id};`;
db.prepare(query).run(parameters);
}
export function createDonationReceipt(
db: WritableDB,
receipt: DonationReceipt
): void {
const forDatabase = freezeDonationReceipt(receipt);
db.prepare(
`
INSERT INTO donationReceipts(
id,
currencyType,
paymentAmount,
paymentDetailJson,
paymentType,
timestamp
) VALUES (
$id,
$currencyType,
$paymentAmount,
$paymentDetailJson,
$paymentType,
$timestamp
);
`
).run(forDatabase);
}