diff --git a/CHANGELOG.md b/CHANGELOG.md index 96456c85f..bac1f7db6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 3.1.0 +This release adds notifications to the orion infrastructure... + +# 3.0.2 +### Bug Fixes: +- Store membership handles both as utf-8 string and raw bytes - [#4950](https://github.com/Joystream/joystream/pull/4950) # 3.0.1 ### Misc - add migration for the `Account` id field diff --git a/db/migrations/1698141186233-Data.js b/db/migrations/1698832298051-Data.js similarity index 99% rename from db/migrations/1698141186233-Data.js rename to db/migrations/1698832298051-Data.js index 71883799a..e0ad8eacc 100644 --- a/db/migrations/1698141186233-Data.js +++ b/db/migrations/1698832298051-Data.js @@ -1,5 +1,5 @@ -module.exports = class Data1698141186233 { - name = 'Data1698141186233' +module.exports = class Data1698832298051 { + name = 'Data1698832298051' async up(db) { await db.query(`CREATE TABLE "channel_follow" ("id" character varying NOT NULL, "user_id" character varying, "channel_id" text NOT NULL, "timestamp" TIMESTAMP WITH TIME ZONE NOT NULL, CONSTRAINT "PK_9410df2b9a316af3f0d216f9487" PRIMARY KEY ("id"))`) @@ -90,8 +90,8 @@ module.exports = class Data1698141186233 { await db.query(`CREATE TABLE "auction_whitelisted_member" ("id" character varying NOT NULL, "auction_id" character varying, "member_id" character varying, CONSTRAINT "AuctionWhitelistedMember_auction_member" UNIQUE ("auction_id", "member_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "PK_f20264ca8e878696fbc25f11bd5" PRIMARY KEY ("id"))`) await db.query(`CREATE INDEX "IDX_d5ae4854487c7658b64225be30" ON "auction_whitelisted_member" ("member_id") `) await db.query(`CREATE INDEX "IDX_5468573a96fa51c03743de5912" ON "auction_whitelisted_member" ("auction_id", "member_id") `) - await db.query(`CREATE TABLE "membership" ("id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, "handle" text NOT NULL, "controller_account" text NOT NULL, "total_channels_created" integer NOT NULL, CONSTRAINT "Membership_handle" UNIQUE ("handle") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "PK_83c1afebef3059472e7c37e8de8" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_1298811c0de5f11198fd43df72" ON "membership" ("handle") `) + await db.query(`CREATE TABLE "membership" ("id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, "handle" text NOT NULL, "handle_raw" text NOT NULL, "controller_account" text NOT NULL, "total_channels_created" integer NOT NULL, CONSTRAINT "Membership_handleRaw" UNIQUE ("handle_raw") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "PK_83c1afebef3059472e7c37e8de8" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_0c5b879f9f2ca57a774f74f7f0" ON "membership" ("handle_raw") `) await db.query(`CREATE TABLE "event" ("id" character varying NOT NULL, "in_block" integer NOT NULL, "in_extrinsic" text, "index_in_block" integer NOT NULL, "timestamp" TIMESTAMP WITH TIME ZONE NOT NULL, "data" jsonb NOT NULL, CONSTRAINT "PK_30c2f3bbaf6d34a55f8ae6e4614" PRIMARY KEY ("id"))`) await db.query(`CREATE INDEX "IDX_8f3f220c4e717207d841d4e6d4" ON "event" ("in_extrinsic") `) await db.query(`CREATE TABLE "notification" ("id" character varying NOT NULL, "account_id" character varying, "notification_type" jsonb NOT NULL, "event_id" character varying, "status" jsonb NOT NULL, "in_app" boolean NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, "recipient" jsonb NOT NULL, CONSTRAINT "PK_705b6c7cdf9b2c2ff7ac7872cb7" PRIMARY KEY ("id"))`) @@ -312,7 +312,7 @@ module.exports = class Data1698141186233 { await db.query(`DROP INDEX "public"."IDX_d5ae4854487c7658b64225be30"`) await db.query(`DROP INDEX "public"."IDX_5468573a96fa51c03743de5912"`) await db.query(`DROP TABLE "membership"`) - await db.query(`DROP INDEX "public"."IDX_1298811c0de5f11198fd43df72"`) + await db.query(`DROP INDEX "public"."IDX_0c5b879f9f2ca57a774f74f7f0"`) await db.query(`DROP TABLE "event"`) await db.query(`DROP INDEX "public"."IDX_8f3f220c4e717207d841d4e6d4"`) await db.query(`DROP TABLE "notification"`) diff --git a/docs/operator-guide/tutorials/backups.md b/docs/operator-guide/tutorials/backups.md new file mode 100644 index 000000000..8d559ab34 --- /dev/null +++ b/docs/operator-guide/tutorials/backups.md @@ -0,0 +1,31 @@ +# Backing up Orion database +It is recommended to schedule daily backups for the `orion-db` service. +This will ensure that a copy of the whole database is saved daily to be used for emergencies +This guide shows a simple method in order to back up `orion-db` using the [cron](https://en.wikipedia.org/wiki/Cron) utility. + +Suppose you want to back up orion a daily cron job at 9:00 AM CET. Provided that the `orion-db` service is being run in a docker container, you can follow these steps: + +1. Open your crontab file for editing. You can do this by running the following command: + +```bash +crontab -e +``` + +2. Add the following line to your crontab file. This line schedules the job to run every day at 9:00 AM CET: + +```bash +0 9 * * * TZ='Europe/Paris' docker exec orion-db pg_dumpall -U postgres > "/path/to/backup/directory/orion-production-$(date '+\%Y-\%m-\%d').bak" +``` + +Make sure to replace `/path/to/backup/directory` with the actual path where you want to store the backup files. + +Here's what each field in the cron expression means: + +- `0`: Minutes field, specifying 0 minutes past the hour. +- `9`: Hours field, specifying 9 AM. +- `*`: Wildcard for days of the month, meaning it will run every day. +- `*`: Wildcard for months, meaning it will run every month. +- `*`: Wildcard for days of the week, meaning it will run every day of the week. +- `TZ='Europe/Paris'`: Sets the timezone to CET (Central European Time) to ensure it runs at 9:00 AM CET. + +3. Save and exit the crontab file. The cron job is now scheduled to run daily at 9:00 AM CET and will dump the PostgreSQL database to the specified backup file with the current date in the filename. \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 0682e96dd..cb2a8653d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21428,9 +21428,9 @@ } }, "node_modules/utf-8-validate": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.9.tgz", - "integrity": "sha512-Yek7dAy0v3Kl0orwMlvi7TPtiCNrdfHNd7Gcc/pLq4BLXqfAmd0J7OWMizUQnTTJsyjKn02mU7anqwfmUP4J8Q==", + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", "hasInstallScript": true, "dependencies": { "node-gyp-build": "^4.3.0" @@ -38841,9 +38841,9 @@ } }, "utf-8-validate": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.9.tgz", - "integrity": "sha512-Yek7dAy0v3Kl0orwMlvi7TPtiCNrdfHNd7Gcc/pLq4BLXqfAmd0J7OWMizUQnTTJsyjKn02mU7anqwfmUP4J8Q==", + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", "requires": { "node-gyp-build": "^4.3.0" } diff --git a/schema/membership.graphql b/schema/membership.graphql index 2fc8b8e2d..aa37fd851 100644 --- a/schema/membership.graphql +++ b/schema/membership.graphql @@ -31,8 +31,11 @@ type Membership @entity { "Timestamp of the block the membership was created at" createdAt: DateTime! - "The unique handle chosen by member" - handle: String! @unique + "The handle coming from decoded handleRaw if possible" + handle: String! + + "The handle chosen by member coming from event deposit" + handleRaw: String! @unique "Member's metadata" metadata: MemberMetadata @derivedFrom(field: "member") diff --git a/src/auth-server/tests/common.ts b/src/auth-server/tests/common.ts index 1aea17b97..a5cbe236a 100644 --- a/src/auth-server/tests/common.ts +++ b/src/auth-server/tests/common.ts @@ -121,11 +121,13 @@ export async function signedAction) { const [memberId, params] = 'isV2001' in event && event.isV2001 ? event.asV2001 : event.asV1000 - const { controllerAccount, handle, metadata: metadataBytes } = params + const { controllerAccount, handle: handleBytes, metadata: metadataBytes } = params const metadata = deserializeMetadata(MembershipMetadata, metadataBytes) const member = overlay.getRepository(Membership).new({ createdAt: new Date(block.timestamp), id: memberId.toString(), controllerAccount: toAddress(controllerAccount), - handle: handle && bytesToString(handle), totalChannelsCreated: 0, }) + if (handleBytes) { + updateMemberHandle(member as Membership, handleBytes) + } if (metadata) { await processMembershipMetadata(overlay, member.id, metadata) @@ -57,7 +60,7 @@ export async function processMemberProfileUpdatedEvent({ const member = await overlay.getRepository(Membership).getByIdOrFail(memberId.toString()) if (newHandle) { - member.handle = newHandle.toString() + updateMemberHandle(member as Membership, newHandle) } if (newMetadata) { @@ -68,6 +71,11 @@ export async function processMemberProfileUpdatedEvent({ } } +function updateMemberHandle(member: Membership, newHandle: Uint8Array) { + member.handleRaw = u8aToHex(newHandle) + member.handle = bytesToString(newHandle) +} + export async function processMemberRemarkedEvent({ overlay, block, diff --git a/src/processor.ts b/src/processor.ts index 1a2beebd1..1a28835de 100644 --- a/src/processor.ts +++ b/src/processor.ts @@ -88,7 +88,7 @@ import { Event } from './types/support' import { assertAssignable } from './utils/misc' import { EntityManagerOverlay } from './utils/overlay' import { EventNames, EventHandler, eventConstructors, EventInstance } from './utils/events' -import { commentCountersManager, videoRelevanceManager, migrateCounters } from './mappings/utils' +import { commentCountersManager, migrateCounters, videoRelevanceManager } from './mappings/utils' import { EntityManager } from 'typeorm' import { OffchainState } from './utils/offchainState' diff --git a/src/tests/integration/notifications.test.ts b/src/tests/integration/notifications.test.ts index 6b03156e5..8c5cd14cb 100644 --- a/src/tests/integration/notifications.test.ts +++ b/src/tests/integration/notifications.test.ts @@ -17,6 +17,7 @@ import { NftFeaturedOnMarketPlace, NftOwnerChannel, Notification, + NotificationEmailDelivery, OwnedNft, Video, VideoLiked, @@ -34,6 +35,12 @@ import { Store } from '@subsquid/typeorm-store' import { processMemberRemarkedEvent } from '../../mappings/membership' import Long from 'long' import { backwardCompatibleMetaID } from '../../mappings/utils' +import { config as dontenvConfig } from 'dotenv' +import path from 'path' + +dontenvConfig({ + path: path.resolve(__dirname, './.env'), +}) const metadataToBytes = (metaClass: AnyMetadataClass, obj: T): Bytes => { return createType('Bytes', '0x' + Buffer.from(metaClass.encode(obj).finish()).toString('hex')) @@ -53,25 +60,24 @@ const createOverlay = async () => { } describe('notifications tests', () => { + let notification: Notification | null let overlay: EntityManagerOverlay let em: EntityManager before(async () => { em = await globalEm await populateDbWithSeedData() }) - after(async () => { - await clearDb() - }) describe('exclude channel', () => { + let notificationId: string it('exclude channel should deposit notification', async () => { const channelId = '1' const rationale = 'test-rationale' const nextNotificationIdPre = await getNextNotificationId(em, false) - + notificationId = OFFCHAIN_NOTIFICATION_ID_TAG + '-' + nextNotificationIdPre await excludeChannelInner(em, channelId, rationale) - const notification = await em.getRepository(Notification).findOneBy({ - id: OFFCHAIN_NOTIFICATION_ID_TAG + '-' + nextNotificationIdPre, + notification = await em.getRepository(Notification).findOneBy({ + id: notificationId, }) const channel = await em.getRepository(Channel).findOneBy({ id: channelId }) const nextNotificationIdPost = await getNextNotificationId(em, false) @@ -90,6 +96,14 @@ describe('notifications tests', () => { expect(nextNotificationIdPost.toString()).to.equal((nextNotificationIdPre + 1).toString()) expect(notification?.accountId).to.equal(account?.id) }) + it('notification email entity should be correctly deposited', async () => { + const notificationEmailDelivery = await em + .getRepository(NotificationEmailDelivery) + .findOneBy({ notificationId }) + expect(notificationEmailDelivery).not.to.be.null + expect(notificationEmailDelivery!.discard).to.be.false + expect(notificationEmailDelivery!.attempts).to.be.undefined + }) it('exclude channel should mark channel as excluded with entity inserted', async () => { const channelId = '2' const rationale = 'test-rationale' @@ -105,15 +119,17 @@ describe('notifications tests', () => { }) }) describe('exclude video', () => { + let notificationId: string it('exclude video should deposit notification', async () => { const videoId = '1' const rationale = 'test-rationale' const nextNotificationIdPre = await getNextNotificationId(em, false) + const notificationId = OFFCHAIN_NOTIFICATION_ID_TAG + '-' + nextNotificationIdPre await excludeVideoInner(em, videoId, rationale) - const notification = await em.getRepository(Notification).findOneBy({ - id: OFFCHAIN_NOTIFICATION_ID_TAG + '-' + nextNotificationIdPre, + notification = await em.getRepository(Notification).findOneBy({ + id: notificationId, }) const video = await em .getRepository(Video) @@ -133,6 +149,14 @@ describe('notifications tests', () => { expect(nextNotificationIdPost.toString()).to.equal((nextNotificationIdPre + 1).toString()) expect(notification?.accountId).to.equal(account?.id) }) + it('notification email entity should be correctly deposited', async () => { + const notificationEmailDelivery = await em + .getRepository(NotificationEmailDelivery) + .findOneBy({ notificationId }) + expect(notificationEmailDelivery).not.to.be.null + expect(notificationEmailDelivery!.discard).to.be.false + expect(notificationEmailDelivery!.attempts).to.be.undefined + }) it('exclude video should work with exclusion entity added', async () => { const videoId = '2' const rationale = 'test-rationale' @@ -148,14 +172,16 @@ describe('notifications tests', () => { }) }) describe('set nft as featured', () => { + let notificationId: string it('feature nfts should deposit notification and set nft as featured', async () => { const nftId = '1' const nextNotificationIdPre = await getNextNotificationId(em, false) + notificationId = OFFCHAIN_NOTIFICATION_ID_TAG + '-' + nextNotificationIdPre await setFeaturedNftsInner(em, [nftId]) - const notification = await em.getRepository(Notification).findOneBy({ - id: OFFCHAIN_NOTIFICATION_ID_TAG + '-' + nextNotificationIdPre, + notification = await em.getRepository(Notification).findOneBy({ + id: notificationId, }) const nft = await em .getRepository(OwnedNft) @@ -182,17 +208,27 @@ describe('notifications tests', () => { expect(notification?.accountId).to.equal(account?.id) expect(nft.isFeatured).to.be.true }) + it('notification email entity should be correctly deposited', async () => { + const notificationEmailDelivery = await em + .getRepository(NotificationEmailDelivery) + .findOneBy({ notificationId }) + expect(notificationEmailDelivery).not.to.be.null + expect(notificationEmailDelivery!.discard).to.be.false + expect(notificationEmailDelivery!.attempts).to.be.undefined + }) }) describe('New bid made', () => { let nft: OwnedNft const memberId = '5' const outbiddedMember = '4' const videoId = '5' + let notificationId: string let nextNotificationIdPre: number before(async () => { const bidAmount = BigInt(100000) nextNotificationIdPre = await getNextNotificationId(em, true) + notificationId = RUNTIME_NOTIFICATION_ID_TAG + '-' + nextNotificationIdPre nft = await em.getRepository(OwnedNft).findOneByOrFail({ videoId }) overlay = await createOverlay() @@ -207,12 +243,12 @@ describe('notifications tests', () => { ) }) it('should deposit notification for member outbidded', async () => { - const notification = await overlay + notification = (await overlay .getRepository(Notification) - .getByIdOrFail(RUNTIME_NOTIFICATION_ID_TAG + '-' + nextNotificationIdPre) - const account = await overlay + .getById(notificationId)) as Notification | null + const account = (await overlay .getRepository(Account) - .getOneByRelationOrFail('membershipId', outbiddedMember) + .getOneByRelationOrFail('membershipId', outbiddedMember)) as Account expect(notification).not.to.be.null expect(notification!.notificationType.isTypeOf).to.equal('HigherBidPlaced') @@ -220,15 +256,24 @@ describe('notifications tests', () => { expect(notification!.inApp).to.be.true expect(notification!.recipient.isTypeOf).to.equal('MemberRecipient') expect((notification!.recipient as MemberRecipient).membership).to.equal(outbiddedMember) - expect(notification?.accountId).to.equal(account?.id) + expect(notification?.accountId).to.equal(account!.id) + }) + it('notification email entity should be correctly deposited', async () => { + const notificationEmailDelivery = (await overlay + .getRepository(NotificationEmailDelivery) + .getOneByRelation('notificationId', notificationId)) as NotificationEmailDelivery | null + expect(notificationEmailDelivery).not.to.be.null + expect(notificationEmailDelivery!.discard).to.be.false + expect(notificationEmailDelivery!.attempts).to.be.empty }) it('should deposit notification for creator receiving a new auction bid', async () => { + notificationId = RUNTIME_NOTIFICATION_ID_TAG + '-' + (nextNotificationIdPre + 1) const channel = await em .getRepository(Channel) .findOneBy({ id: (nft.owner as NftOwnerChannel).channel }) - const notification = await overlay + notification = (await overlay .getRepository(Notification) - .getByIdOrFail(RUNTIME_NOTIFICATION_ID_TAG + '-' + (nextNotificationIdPre + 1)) + .getByIdOrFail(notificationId)) as Notification const account = await overlay .getRepository(Account) .getOneByRelationOrFail('membershipId', channel!.ownerMemberId!) @@ -243,9 +288,21 @@ describe('notifications tests', () => { expect((notification!.recipient as ChannelRecipient).channel).to.equal(channel!.id) expect(notification?.accountId).to.equal(account?.id) }) + describe('notification email entity should be correctly to db', () => { + let notificationEmailDelivery: NotificationEmailDelivery | null + it('notification email entity should be correctly deposited on overlay', async () => { + notificationEmailDelivery = (await overlay + .getRepository(NotificationEmailDelivery) + .getOneByRelation('notificationId', notificationId)) as NotificationEmailDelivery | null + expect(notificationEmailDelivery).not.to.be.null + expect(notificationEmailDelivery!.discard).to.be.false + expect(notificationEmailDelivery!.attempts).to.be.empty + }) + }) }) describe('Video Liked', () => { - let notificationId: number + let notificationId: string + let nextNotificationIdPre: number const block = { timestamp: 123456 } as any const indexInBlock = 1 const extrinsicHash = '0x1234567890abcdef' @@ -261,7 +318,8 @@ describe('notifications tests', () => { } as any before(async () => { overlay = await createOverlay() - notificationId = await getNextNotificationId(em, true) + nextNotificationIdPre = await getNextNotificationId(em, true) + notificationId = RUNTIME_NOTIFICATION_ID_TAG + '-' + nextNotificationIdPre.toString() }) it('should process video liked and deposit notification', async () => { await processMemberRemarkedEvent({ @@ -273,21 +331,33 @@ describe('notifications tests', () => { }) const nextNotificationId = await getNextNotificationId(em, true) - const notification = await overlay + notification = (await overlay .getRepository(Notification) - .getByIdOrFail(RUNTIME_NOTIFICATION_ID_TAG + '-' + notificationId.toString()) + .getByIdOrFail(notificationId)) as Notification expect(notification.notificationType.isTypeOf).to.equal('VideoLiked') const notificationData = notification.notificationType as VideoLiked expect(notificationData.videoId).to.equal('1') expect(notification!.status.isTypeOf).to.equal('Unread') expect(notification!.inApp).to.be.true - expect(nextNotificationId.toString()).to.equal((notificationId + 1).toString()) + expect(nextNotificationId.toString()).to.equal((nextNotificationIdPre + 1).toString()) expect(notification!.recipient.isTypeOf).to.equal('ChannelRecipient') }) + describe('notification email entity should be correctly to db', () => { + let notificationEmailDelivery: NotificationEmailDelivery | null + it('notification email entity should be correctly deposited on overlay', async () => { + notificationEmailDelivery = (await overlay + .getRepository(NotificationEmailDelivery) + .getOneByRelation('notificationId', notificationId)) as NotificationEmailDelivery | null + expect(notificationEmailDelivery).not.to.be.null + expect(notificationEmailDelivery!.discard).to.be.false + expect(notificationEmailDelivery!.attempts).to.be.empty + }) + }) }) describe('Comment Posted To Video', () => { - let notificationId: number + let nextNotificationIdPre: number + let notificationId: string const block = { timestamp: 123456 } as any const indexInBlock = 1 const extrinsicHash = '0x1234567890abcdef' @@ -304,7 +374,8 @@ describe('notifications tests', () => { } as any before(async () => { overlay = await createOverlay() - notificationId = await getNextNotificationId(em, true) + nextNotificationIdPre = await getNextNotificationId(em, true) + notificationId = RUNTIME_NOTIFICATION_ID_TAG + '-' + nextNotificationIdPre.toString() }) it('should process comment to video and deposit notification', async () => { await processMemberRemarkedEvent({ @@ -316,15 +387,16 @@ describe('notifications tests', () => { }) const nextNotificationId = await getNextNotificationId(em, true) - const notification = await overlay + notification = (await overlay .getRepository(Notification) - .getByIdOrFail(RUNTIME_NOTIFICATION_ID_TAG + '-' + notificationId.toString()) + .getByIdOrFail(notificationId)) as Notification | null it('notification type is comment posted to video', () => { - expect(notification.notificationType.isTypeOf).to.equal('CommentPostedToVideo') + expect(notification).not.to.be.null + expect(notification!.notificationType.isTypeOf).to.equal('CommentPostedToVideo') }) it('notification data for comment posted to video should be ok', () => { - const notificationData = notification.notificationType as CommentPostedToVideo + const notificationData = notification!.notificationType as CommentPostedToVideo expect(notificationData.videoId).to.equal('1') expect(notificationData.comentId).to.equal(backwardCompatibleMetaID(block, indexInBlock)) expect(notificationData.memberHandle).to.equal('handle-2') @@ -334,9 +406,20 @@ describe('notifications tests', () => { it('general notification creation setting should be as default', () => { expect(notification!.status.isTypeOf).to.equal('Unread') expect(notification!.inApp).to.be.true - expect(nextNotificationId.toString()).to.equal((notificationId + 1).toString()) + expect(nextNotificationId.toString()).to.equal((nextNotificationIdPre + 1).toString()) expect(notification!.recipient.isTypeOf).to.equal('ChannelRecipient') }) + describe('notification email entity should be correctly to db', () => { + let notificationEmailDelivery: NotificationEmailDelivery | null + it('notification email entity should be correctly deposited on overlay', async () => { + notificationEmailDelivery = (await overlay + .getRepository(NotificationEmailDelivery) + .getOneByRelation('notificationId', notificationId)) as NotificationEmailDelivery | null + expect(notificationEmailDelivery).not.to.be.null + expect(notificationEmailDelivery!.discard).to.be.false + expect(notificationEmailDelivery!.attempts).to.be.empty + }) + }) }) }) }) diff --git a/src/tests/integration/run.sh b/src/tests/integration/run.sh index 970b29a20..e4f949aa9 100755 --- a/src/tests/integration/run.sh +++ b/src/tests/integration/run.sh @@ -20,6 +20,6 @@ cleanup() { } # Run the tests -npx ts-mocha "./src/tests/integration/*.ts" --timeout 60000 --exit +npx ts-mocha "./src/tests/integration/notifications.test.ts" --timeout 60000 --exit trap cleanup EXIT diff --git a/src/tests/integration/testUtils.ts b/src/tests/integration/testUtils.ts index 7406fa0f8..72f3db0fe 100644 --- a/src/tests/integration/testUtils.ts +++ b/src/tests/integration/testUtils.ts @@ -29,6 +29,7 @@ export async function populateDbWithSeedData() { id: i.toString(), controllerAccount: `controller-account-${i}`, handle: `handle-${i}`, + handleRaw: '0x' + Buffer.from(`handle-${i}`).toString('hex'), totalChannelsCreated: 0, }) const user = new User({ diff --git a/src/tests/migrations/export.json b/src/tests/migrations/export.json index bc09e6424..923606eca 100644 --- a/src/tests/migrations/export.json +++ b/src/tests/migrations/export.json @@ -146,5 +146,5 @@ } }, "blockNumber": 323, - "orionVersion": "3.0.1" + "orionVersion": "3.0.2" } diff --git a/src/tests/migrations/migration.test.ts b/src/tests/migrations/migration.test.ts index f55cb29f3..df1557772 100644 --- a/src/tests/migrations/migration.test.ts +++ b/src/tests/migrations/migration.test.ts @@ -14,7 +14,7 @@ const queryAccount = async (membershipId: string, em: EntityManager) => { }) } -describe('Migration from 3.0.1 to 3.1.0', () => { +describe('Migration from 3.0.2 to 3.1.0', () => { let em: EntityManager const aliceMembershipId = '16' const bobMembershipId = '17' diff --git a/src/utils/events.ts b/src/utils/events.ts index 3db516538..3e1b6b172 100644 --- a/src/utils/events.ts +++ b/src/utils/events.ts @@ -78,11 +78,13 @@ export const eventConstructors = { 'Content.VideoCreated': ContentVideoCreatedEvent, 'Content.VideoUpdated': ContentVideoUpdatedEvent, 'Content.VideoDeleted': ContentVideoDeletedEvent, + // Deprecated in runtime spec version 2002 (nara), but still required for Orion processing from genesis block 'Content.VideoDeletedByModerator': ContentVideoDeletedByModeratorEvent, 'Content.VideoVisibilitySetByModerator': ContentVideoVisibilitySetByModeratorEvent, 'Content.ChannelCreated': ContentChannelCreatedEvent, 'Content.ChannelUpdated': ContentChannelUpdatedEvent, 'Content.ChannelDeleted': ContentChannelDeletedEvent, + // Deprecated in runtime spec version 2002 (nara), but still required for Orion processing from genesis block 'Content.ChannelDeletedByModerator': ContentChannelDeletedByModeratorEvent, 'Content.ChannelVisibilitySetByModerator': ContentChannelVisibilitySetByModeratorEvent, 'Content.ChannelOwnerRemarked': ContentChannelOwnerRemarkedEvent, diff --git a/src/utils/globalEm.ts b/src/utils/globalEm.ts index 22a7dff59..d45674014 100644 --- a/src/utils/globalEm.ts +++ b/src/utils/globalEm.ts @@ -21,8 +21,7 @@ const source = new DataSource(config) async function initGlobalEm() { try { - console.log(process.env.DB_PORT) - console.log(config) + globalEmLogger.info(`Initializing database connection with config...`) await source.initialize() } catch (e) { globalEmLogger.error(`Error during database connection initialization: ${String(e)}`) diff --git a/src/utils/offchainState.ts b/src/utils/offchainState.ts index dd9c4db36..e564bdde8 100644 --- a/src/utils/offchainState.ts +++ b/src/utils/offchainState.ts @@ -5,6 +5,7 @@ import { createLogger } from '@subsquid/logger' import assert from 'assert' import { uniqueId } from './crypto' import { defaultNotificationPreferences } from './notification/helpers' +import { NextEntityId } from '../model' const DEFAULT_EXPORT_PATH = path.resolve(__dirname, '../../db/export/export.json') @@ -92,6 +93,13 @@ export class OffchainState { private logger = createLogger('offchainState') private _isImported = false + private globalCountersMigration = { + // destination version : [global counters names] + '3.0.1': ['Account'], + '3.0.2': ['Account'], + '3.1.0': ['Account'], + } + private migrations: Migrations = { '3.1.0': migrateExportDataToV310, '3.0.0': migrateExportDataToV300, @@ -265,4 +273,24 @@ export class OffchainState { this.logger.info(`Last export block number established: ${blockNumber}`) return blockNumber } + + private async migrateCounters(exportedVersion: string, em: EntityManager): Promise { + const migrationData = Object.entries(this.globalCountersMigration).sort( + ([a], [b]) => this.versionToNumber(a) - this.versionToNumber(b) + ) // sort in increasing order + + for (const [version, counters] of migrationData) { + if (this.versionToNumber(exportedVersion) < this.versionToNumber(version)) { + this.logger.info(`Migrating global counters to version ${version}`) + for (const entityName of counters) { + // build query that gets the entityName with the highest id + const rowNumber = await em.query(`SELECT COUNT(*) FROM ${entityName}`) + const latestId = parseInt(rowNumber[0].count) + + this.logger.info(`Setting next id for ${entityName} to ${latestId + 1}`) + await em.save(new NextEntityId({ entityName, nextId: latestId + 1 })) + } + } + } + } }