From 97d0f2ab512ea596b4b2055428f068c6957e1168 Mon Sep 17 00:00:00 2001 From: Adam Fraser Date: Tue, 3 Sep 2024 15:24:59 -0400 Subject: [PATCH] Add FirebaseNotificationToken to Postgres This reverts commit 730d099e2fe85335cf00340d28a8400ab35957c3. --- .../__tests__/helpers/mock-generators.ts | 5 + .../firebase-notification-token-table.test.ts | 89 ++++++++++++ ...eate_firebase_notification_tokens_table.ts | 16 +++ indexer/packages/postgres/src/index.ts | 1 + .../firebase-notification-token-model.ts | 27 ++++ .../firebase-notification-token-table.ts | 132 ++++++++++++++++++ .../postgres/src/types/db-model-types.ts | 7 + .../firebase-notification-token-types.ts | 24 ++++ indexer/packages/postgres/src/types/index.ts | 1 + .../postgres/src/types/query-types.ts | 6 + 10 files changed, 308 insertions(+) create mode 100644 indexer/packages/postgres/__tests__/stores/firebase-notification-token-table.test.ts create mode 100644 indexer/packages/postgres/src/db/migrations/migration_files/20240809153326_create_firebase_notification_tokens_table.ts create mode 100644 indexer/packages/postgres/src/models/firebase-notification-token-model.ts create mode 100644 indexer/packages/postgres/src/stores/firebase-notification-token-table.ts create mode 100644 indexer/packages/postgres/src/types/firebase-notification-token-types.ts diff --git a/indexer/packages/postgres/__tests__/helpers/mock-generators.ts b/indexer/packages/postgres/__tests__/helpers/mock-generators.ts index 86d6f6e3b74..34d9ada9e2f 100644 --- a/indexer/packages/postgres/__tests__/helpers/mock-generators.ts +++ b/indexer/packages/postgres/__tests__/helpers/mock-generators.ts @@ -1,5 +1,6 @@ import * as AssetTable from '../../src/stores/asset-table'; import * as BlockTable from '../../src/stores/block-table'; +import * as FirebaseNotificationTokenTable from '../../src/stores/firebase-notification-token-table'; import * as LiquidityTiersTable from '../../src/stores/liquidity-tiers-table'; import * as MarketTable from '../../src/stores/market-table'; import * as PerpetualMarketTable from '../../src/stores/perpetual-market-table'; @@ -26,6 +27,7 @@ import { defaultTendermintEvent2, defaultTendermintEvent3, defaultTendermintEvent4, + defaultFirebaseNotificationToken, defaultWallet, isolatedMarket, isolatedMarket2, @@ -78,4 +80,7 @@ export async function seedData() { await Promise.all([ WalletTable.create(defaultWallet), ]); + await Promise.all([ + FirebaseNotificationTokenTable.create(defaultFirebaseNotificationToken), + ]); } diff --git a/indexer/packages/postgres/__tests__/stores/firebase-notification-token-table.test.ts b/indexer/packages/postgres/__tests__/stores/firebase-notification-token-table.test.ts new file mode 100644 index 00000000000..a5260cfdb18 --- /dev/null +++ b/indexer/packages/postgres/__tests__/stores/firebase-notification-token-table.test.ts @@ -0,0 +1,89 @@ +import { FirebaseNotificationTokenFromDatabase } from '../../src/types'; +import { clearData, migrate, teardown } from '../../src/helpers/db-helpers'; +import { defaultAddress2, defaultFirebaseNotificationToken, defaultWallet } from '../helpers/constants'; +import * as FirebaseNotificationTokenTable from '../../src/stores/firebase-notification-token-table'; +import * as WalletTable from '../../src/stores/wallet-table'; + +describe('FirebaseNotificationToken store', () => { + beforeAll(async () => { + await migrate(); + }); + + beforeEach(async () => { + // Default wallet is required in the DB for token creation + // As token has a foreign key constraint on wallet + await WalletTable.create(defaultWallet); + }); + + afterEach(async () => { + await clearData(); + }); + + afterAll(async () => { + await teardown(); + }); + + it('Successfully creates a Token', async () => { + await FirebaseNotificationTokenTable.create(defaultFirebaseNotificationToken); + const token = await FirebaseNotificationTokenTable.findByToken( + defaultFirebaseNotificationToken.token, + ); + expect(token).toEqual(expect.objectContaining(defaultFirebaseNotificationToken)); + }); + + it('Successfully upserts a Token multiple times', async () => { + await FirebaseNotificationTokenTable.upsert(defaultFirebaseNotificationToken); + let token: FirebaseNotificationTokenFromDatabase | undefined = await + FirebaseNotificationTokenTable.findByToken( + defaultFirebaseNotificationToken.token, + ); + + expect(token).toEqual(expect.objectContaining(defaultFirebaseNotificationToken)); + + // Upsert again to test update functionality + const updatedToken = { ...defaultFirebaseNotificationToken, updatedAt: new Date().toISOString(), language: 'es' }; + await FirebaseNotificationTokenTable.upsert(updatedToken); + token = await FirebaseNotificationTokenTable.findByToken( + defaultFirebaseNotificationToken.token, + ); + + expect(token).toEqual(expect.objectContaining(updatedToken)); + }); + + it('Successfully finds all Tokens', async () => { + await WalletTable.create({ ...defaultWallet, address: defaultAddress2 }); + const additionalToken = { + token: 'fake_token', + address: defaultAddress2, + language: 'en', + updatedAt: new Date().toISOString(), + }; + + await Promise.all([ + FirebaseNotificationTokenTable.create(defaultFirebaseNotificationToken), + FirebaseNotificationTokenTable.create(additionalToken), + ]); + + const tokens: FirebaseNotificationTokenFromDatabase[] = await FirebaseNotificationTokenTable + .findAll( + {}, + [], + { readReplica: true }, + ); + + expect(tokens.length).toEqual(2); + expect(tokens[0]).toEqual(expect.objectContaining(defaultFirebaseNotificationToken)); + expect(tokens[1]).toEqual(expect.objectContaining(additionalToken)); + }); + + it('Successfully finds a Token by token', async () => { + await FirebaseNotificationTokenTable.create(defaultFirebaseNotificationToken); + + const token: FirebaseNotificationTokenFromDatabase | undefined = await + FirebaseNotificationTokenTable.findByToken( + defaultFirebaseNotificationToken.token, + ); + + expect(token).toEqual(expect.objectContaining(defaultFirebaseNotificationToken)); + }); +}); diff --git a/indexer/packages/postgres/src/db/migrations/migration_files/20240809153326_create_firebase_notification_tokens_table.ts b/indexer/packages/postgres/src/db/migrations/migration_files/20240809153326_create_firebase_notification_tokens_table.ts new file mode 100644 index 00000000000..bb42cea94db --- /dev/null +++ b/indexer/packages/postgres/src/db/migrations/migration_files/20240809153326_create_firebase_notification_tokens_table.ts @@ -0,0 +1,16 @@ +import * as Knex from 'knex'; + +export async function up(knex: Knex): Promise { + return knex.schema.createTable('firebase_notification_tokens', (table) => { + table.increments('id').primary(); + table.string('token').notNullable().unique(); + table.string('address').notNullable(); + table.foreign('address').references('wallets.address').onDelete('CASCADE'); + table.string('language').notNullable(); + table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now()); + }); +} + +export async function down(knex: Knex): Promise { + return knex.schema.dropTable('firebase_notification_tokens'); +} diff --git a/indexer/packages/postgres/src/index.ts b/indexer/packages/postgres/src/index.ts index 34979ef0c60..0fc5413e597 100644 --- a/indexer/packages/postgres/src/index.ts +++ b/indexer/packages/postgres/src/index.ts @@ -45,6 +45,7 @@ export * as TradingRewardAggregationTable from './stores/trading-reward-aggregat export * as LeaderboardPnlTable from './stores/leaderboard-pnl-table'; export * as SubaccountUsernamesTable from './stores/subaccount-usernames-table'; export * as PersistentCacheTable from './stores/persistent-cache-table'; +export * as FirebaseNotificationTokenTable from './stores/firebase-notification-token-table'; export * as perpetualMarketRefresher from './loops/perpetual-market-refresher'; export * as assetRefresher from './loops/asset-refresher'; diff --git a/indexer/packages/postgres/src/models/firebase-notification-token-model.ts b/indexer/packages/postgres/src/models/firebase-notification-token-model.ts new file mode 100644 index 00000000000..9206962945d --- /dev/null +++ b/indexer/packages/postgres/src/models/firebase-notification-token-model.ts @@ -0,0 +1,27 @@ +import { Model } from 'objection'; + +import { IsoString } from '../types'; +import WalletModel from './wallet-model'; + +class FirebaseNotificationTokenModel extends Model { + static tableName = 'firebase_notification_tokens'; + + id!: number; + token!: string; + address!: string; + updatedAt!: IsoString; + language!: string; + + static relationMappings = { + wallet: { + relation: Model.BelongsToOneRelation, + modelClass: WalletModel, + join: { + from: 'firebase_notification_tokens.address', + to: 'wallets.address', + }, + }, + }; +} + +export default FirebaseNotificationTokenModel; diff --git a/indexer/packages/postgres/src/stores/firebase-notification-token-table.ts b/indexer/packages/postgres/src/stores/firebase-notification-token-table.ts new file mode 100644 index 00000000000..6b11c7e535d --- /dev/null +++ b/indexer/packages/postgres/src/stores/firebase-notification-token-table.ts @@ -0,0 +1,132 @@ +import { DateTime } from 'luxon'; +import { PartialModelObject, QueryBuilder } from 'objection'; + +import { DEFAULT_POSTGRES_OPTIONS } from '../constants'; +import { setupBaseQuery, verifyAllRequiredFields } from '../helpers/stores-helpers'; +import Transaction from '../helpers/transaction'; +import TokenModel from '../models/firebase-notification-token-model'; +import { + Options, + Ordering, + QueryableField, + QueryConfig, + FirebaseNotificationTokenColumns, + FirebaseNotificationTokenCreateObject, + FirebaseNotificationTokenFromDatabase, + FirebaseNotificationTokenQueryConfig, + FirebaseNotificationTokenUpdateObject, +} from '../types'; + +export async function findAll( + { + address, + limit, + }: FirebaseNotificationTokenQueryConfig, + requiredFields: QueryableField[], + options: Options = DEFAULT_POSTGRES_OPTIONS, +): Promise { + verifyAllRequiredFields( + { + address, + limit, + } as QueryConfig, + requiredFields, + ); + + let baseQuery: QueryBuilder = setupBaseQuery( + TokenModel, + options, + ); + + if (address) { + baseQuery = baseQuery.where(FirebaseNotificationTokenColumns.address, address); + } + + if (options.orderBy !== undefined) { + for (const [column, order] of options.orderBy) { + baseQuery = baseQuery.orderBy( + column, + order, + ); + } + } else { + baseQuery = baseQuery.orderBy( + FirebaseNotificationTokenColumns.updatedAt, + Ordering.ASC, + ); + } + + if (limit) { + baseQuery = baseQuery.limit(limit); + } + + return baseQuery.returning('*'); +} + +export async function create( + tokenToCreate: FirebaseNotificationTokenCreateObject, + options: Options = { txId: undefined }, +): Promise { + return TokenModel.query( + Transaction.get(options.txId), + ).insert(tokenToCreate).returning('*'); +} + +export async function update( + { + token, + ...fields + }: FirebaseNotificationTokenUpdateObject, + options: Options = { txId: undefined }, +): Promise { + const existingToken = await TokenModel.query( + Transaction.get(options.txId), + ).findOne({ token }); + const updatedToken = await existingToken.$query().patch(fields as PartialModelObject).returning('*'); + return updatedToken as unknown as FirebaseNotificationTokenFromDatabase; +} + +export async function upsert( + tokenToUpsert: FirebaseNotificationTokenCreateObject, + options: Options = { txId: undefined }, +): Promise { + const existingToken = await TokenModel.query( + Transaction.get(options.txId), + ).findOne({ token: tokenToUpsert.token }); + + if (existingToken) { + return update(tokenToUpsert, options); + } else { + return create(tokenToUpsert, options); + } +} + +export async function findByToken( + token: string, + options: Options = DEFAULT_POSTGRES_OPTIONS, +): Promise { + const baseQuery: QueryBuilder = setupBaseQuery( + TokenModel, + options, + ); + return baseQuery + .findOne({ token }) + .returning('*'); +} + +export async function registerToken( + token: string, + address: string, + language: string, + options: Options = { txId: undefined }, +): Promise { + return upsert( + { + token, + address, + updatedAt: DateTime.now().toISO(), + language, + }, + options, + ); +} diff --git a/indexer/packages/postgres/src/types/db-model-types.ts b/indexer/packages/postgres/src/types/db-model-types.ts index a81ee86d992..870e0f0a9b4 100644 --- a/indexer/packages/postgres/src/types/db-model-types.ts +++ b/indexer/packages/postgres/src/types/db-model-types.ts @@ -295,6 +295,13 @@ export interface AffiliateReferredUserFromDatabase { referredAtBlock: string, } +export interface FirebaseNotificationTokenFromDatabase { + address: WalletFromDatabase['address'], + token: string, + updatedAt: IsoString, + language: string, +} + export type SubaccountAssetNetTransferMap = { [subaccountId: string]: { [assetId: string]: string }, }; export type SubaccountToPerpetualPositionsMap = { [subaccountId: string]: diff --git a/indexer/packages/postgres/src/types/firebase-notification-token-types.ts b/indexer/packages/postgres/src/types/firebase-notification-token-types.ts new file mode 100644 index 00000000000..ee64b5dfe5d --- /dev/null +++ b/indexer/packages/postgres/src/types/firebase-notification-token-types.ts @@ -0,0 +1,24 @@ +/* ------- TOKEN TYPES ------- */ + +type IsoString = string; + +export interface FirebaseNotificationTokenCreateObject { + token: string, + address: string, + language: string, + updatedAt: IsoString, +} + +export interface FirebaseNotificationTokenUpdateObject { + token: string, + address: string, + language: string, + updatedAt: IsoString, +} + +export enum FirebaseNotificationTokenColumns { + token = 'token', + address = 'address', + language = 'language', + updatedAt = 'updatedAt', +} diff --git a/indexer/packages/postgres/src/types/index.ts b/indexer/packages/postgres/src/types/index.ts index 8c81f46fff6..c5d6bff805f 100644 --- a/indexer/packages/postgres/src/types/index.ts +++ b/indexer/packages/postgres/src/types/index.ts @@ -31,4 +31,5 @@ export * from './leaderboard-pnl-types'; export * from './affiliate-referred-users-types'; export * from './persistent-cache-types'; export * from './affiliate-info-types'; +export * from './firebase-notification-token-types'; export { PositionSide } from './position-types'; diff --git a/indexer/packages/postgres/src/types/query-types.ts b/indexer/packages/postgres/src/types/query-types.ts index f3435ff0b94..0b485287307 100644 --- a/indexer/packages/postgres/src/types/query-types.ts +++ b/indexer/packages/postgres/src/types/query-types.ts @@ -92,6 +92,7 @@ export enum QueryableField { REFEREE_ADDRESS = 'refereeAddress', KEY = 'key', IS_WHITELIST_AFFILIATE = 'isWhitelistAffiliate', + TOKEN = 'token', } export interface QueryConfig { @@ -339,3 +340,8 @@ export interface PersistentCacheQueryConfig extends QueryConfig { export interface AffiliateInfoQueryConfig extends QueryConfig { [QueryableField.ADDRESS]?: string, } + +export interface FirebaseNotificationTokenQueryConfig extends QueryConfig { + [QueryableField.ADDRESS]?: string, + [QueryableField.TOKEN]?: string, +}