diff --git a/.env b/.env index 4842ada32..ca7be1ece 100644 --- a/.env +++ b/.env @@ -1,5 +1,4 @@ # LOCAL DEV ENVIRONMENT - ORION_ENV=development DEV_DISABLE_SAME_SITE=true @@ -17,6 +16,9 @@ PROCESSOR_PROMETHEUS_PORT=3337 GQL_PORT=4350 # Auth api port AUTH_API_PORT=4074 +# RabbitMQ +RABBITMQ_PORT=5672 +RABBITMQ_URL=amqp://orion_rabbitmq # Archive gateway url ARCHIVE_GATEWAY_URL=${CUSTOM_ARCHIVE_GATEWAY_URL:-http://localhost:8888/graphql} @@ -33,17 +35,10 @@ KILL_SWITCH_ON=false VIDEO_VIEW_PER_USER_TIME_LIMIT=10 # Operator API secret OPERATOR_SECRET=this-is-not-so-secret-change-it -# every 50 views video relevance score will be recalculated -VIDEO_RELEVANCE_VIEWS_TICK=50 -# [ -# newness (negative number of days since created) weight, -# views weight, -# comments weight, -# rections weights, -# [joystream creation weight, YT creation weight], -# Default channel weight/bias -# ] -RELEVANCE_WEIGHTS="[1, 0.03, 0.3, 0.5, [7,3], 1]" +# every 10 views video relevance score will be recalculated +VIDEO_RELEVANCE_VIEWS_TICK=10 +# every time a channel is followed / unfollowed, its weight will be recalculated +CHANNEL_WEIGHT_FOLLOWS_TICK=1 COMMENT_TIP_TIERS='{"SILVER": 100, "GOLD": 500, "DIAMOND": 1000}' MAX_CACHED_ENTITIES=5000 APP_PRIVATE_KEY=this-is-not-so-secret-change-it diff --git a/CHANGELOG.md b/CHANGELOG.md index 3af14b3f8..14f43fc14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,168 @@ +# 5.0.0 + +## Changes + +### Channel weights and video relevance calculation + +- **Automatic channel weights:** The new _Relevance Service_ supports automatic calculation of channel weights based on factors such as channel's _cumulative revenue_, _YPP tier_, _number of followers_, _CRT liquidity_ and _CRT volume_ (the weight of which can be configured). +- **Improved video relevance formula:** Factors such as number of _views_ / _comments_ / _reactions_ are now represented as `0 - 1` range rates, which simplifies assigning adequate weights. +- **Fixed video relevance calculation bugs** (such as https://github.com/Joystream/orion/issues/361) +- **More flexible configuration** (updates interval or number of scored videos per channel can now be configured via the GraphQL API) + +### Improved permissions system + +- **New view/read permission levels: `VIEW_CURATOR_SCHEMA`, `VIEW_ADMIN_SCHEMA`** . Thanks to the introduction of a new `curator` database schema, the view permissions to access hidden content (hidden / censored channels and videos) can be separated from view permissions to access more sensitive data (Orion account emails, session data, configuration etc.) and other root-level permissions. +- Changing YPP status of a channel now requires `SET_CHANNEL_YPP_STATUS` permission. +- Changing other app-scoped configuration settings which don't have a separate permission category now requires `SET_APP_CONFIGS` permission. +- Changing the new relevance service weights and configuration requires `SET_RELEVANCE_WEIGHTS` and `SET_RELEVANCE_CONFIG` permissions respectively. + +### Support for setting channel YPP tiers and sync status + +- `Channel.yppStatus` now includes YPP tier +- `Channel` now has a new `isYtSyncEnabled` field +- Those values can be set via the new `setChannelYppStatus` and `setChannelYoutubeSyncEnabled` mutations respectively (given sufficient permissions) + +### Fixed _Offchain state_ migrations + +- Fixed https://github.com/Joystream/orion/issues/360 + +### Performance impovements (indexes, faster sync etc.) + +- **New indexes:** New PostgreSQL indexes have been added to support faster filtering / ordering of channels and videos in Atlas and [Content admin dashboard](https://github.com/Joystream/gleev/issues/72). +- **Post-sync index creation:** The creation of some custom PostgreSQL indexes has been delayed to post-sync phase (ie. the time when processor catches up to chain head) to allow faster from-scratch syncing of new Orion instances and faster migrations. + + +## Upgrade instructions + +TBD. + +## Affected components: +- **(M)** `assets/patches/@subsquid+typeorm-config+2.0.2.patch` (support for custom TypeORM subscribers) +- Processor + - **(M)** `processor.ts` + - support for new `RelevanceService` + - support for post-sync index generation (via `model/indexes.ts`) + - improved logging + - **(A)** `mappings/subscribers/TransactionCommitSubscriber.ts` + - **(M)** `mappings/utils.ts` (added `relevanceQueuePublisher` instance) + - Event handlers: + - **(M)** `Content.ChannelCreated` + - **(M)** `Content.VideoCreated` + - Events triggering re-calculation of channel / video relevance: + - **(M)** `Content.ChannelRewardUpdated` + - **(M)** `Content.ChannelRewardClaimedAndWithdrawn` + - **(M)** `Content.EnglishAuctionSettled` + - **(M)** `Content.BidMadeCompletingAuction` + - **(M)** `Content.OpenAuctionBidAccepted` + - **(M)** `Content.OfferAccepted` + - **(M)** `Content.NftBought` + - **(M)** `Members.MemberRemarked` + - `ReactVideo` (meta-action) + - `CreateComment` (meta-action) + - `DeleteComment` (meta-action) + - `MakeChannelPayment` (meta-action) +- Migrations: + - **(!) Re-generated all data migrations** + - **(!) Custom indexes are no longer part of migrations, moved to `model/indexes.ts`** + - **(M)** `db/migrations/1000000000000-Admin.js` + - **(D)** `db/migrations/2200000000000-Indexes.js` + - **(R)** `db/viewDefinitions.js` => `model/views.ts` + - **(M)** `db/generateViewsMigration.js` +- Schema / Models: + - Entities moved from `admin` to `curator` schema + - **(M)** `OwnedNft` + - **(M)** `Auction` + - **(M)** `Bid` + - **(M)** `Channel` + - **(M)** `BannedMember` + - **(M)** `Event` + - **(M)** `NftHistoryEntry` + - **(M)** `NftActivity` + - **(M)** `UserInteractionCount` + - **(M)** `VideoViewEvent` + - **(M)** `Report` + - **(M)** `NftFeaturingRequest` + - **(M)** `ChannelFollow` + - **(M)** `StorageDataObject` + - **(M)** `MarketplaceToken` + - **(M)** `CommentReaction` + - **(M)** `Comment` + - **(M)** `VideoCategory` + - **(M)** `Video` + - **(M)** `VideoFeaturedInCategory` + - **(M)** `VideoHero` + - **(M)** `VideoMediaMetadata` + - **(M)** `VideoMediaEncoding` + - **(M)** `License` + - **(M)** `VideoSubtitle` + - **(M)** `VideoReaction` + - Entities with removed `@index` decorators (index defs moved to `model/indexes.ts`): + - **(M)** `Channel` (`createdAt`, `language`) + - **(M)** `Video` (`createdAt`, `orionLanguage`, `videoRelevance`) + - Auth server: + - **(M)** `auth-server/handlers/createAccount.ts` (dependency on `NextEntityId` removed) + - Entities removed: + - **(D)** `ChannelVerification` + - **(D)** `ChannelSuspension` + - **(D)** `Exclusion` + - **(M)** `OperatorPermission` + - **(M)** `Channel` + - added `isYtSyncEnabled` + - extended `yppStatus` + - **(M)** `ChannelYppStatus` + - **(A)** `model/views.ts` (PostgreSQL view creation utils) + - **(A)** `model/indexes.ts` (PostgreSQL index creation utils) +- Auth server: + - **(M)** `openapi.yml` spec + - **(M)** `anonymousAuth` route +- GraphQL server: + - **(M)** `server-extension/check.ts` (more granular view permissions, `curator` / `admin` schema) + - **(M)** `server-extension/utils.ts` + - **(A)** `server-extension/subscribers/TransactionCommitSubscriber.ts` + - Fixed permissions: + - **(M)** `setAppAssetStorage` + - **(M)** `setAppNameAlt` + - setNewNotificationAssetRoot + - setMaxAttemptsOnMailDelivery + - **(A)** `setChannelYppStatus` + - **(M)** `followChannel` + - **(M)** `addVideoView` + - **(D)** `suspendChannels` (replaced by `setChannelYppStatus`) + - **(D)** `verifyChannel` (replaced by `setChannelYppStatus`) + - **(D)** `excludeChannel` (duplicate of `excludeContent`) + - **(D)** `excludeVideo` (duplicate of `excludeContent`) + - **(D)** `setVideoWeights` + - **(D)** `setNewNotificationCenterPath` (duplicate of `setMaxAttemptsOnMailDelivery`) + - **(D)** `setChannelsWeights` + - **(M)** `processCommentsCensorshipStatusUpdate` + - **(A)** `setRelevanceWeights` mutation + - **(A)** `setRelevanceServiceConfig` mutation +- **(A) NEW RELEVANCE SERVICE (`relevance-service`)** +- **(D)** `utils/VideoRelevanceManager.ts` +- Config: + - **Defaults now can be specified directly in `src/utils/config.ts`** (but can still be overriden by `env` variables and/or db config) + - **(A)** `RELEVANCE_SERVICE_CONFIG` + - **(A)** `CHANNEL_WEIGHT_FOLLOWS_TICK` +- Environment: + - **(A)** `RABBITMQ_PORT` (required) + - **(A)** `RABBITMQ_URL` (required) + - **(A)** `CHANNEL_WEIGHT_FOLLOWS_TICK` (required) + - **(M)** `VIDEO_RELEVANCE_VIEWS_TICK` (`50` => `10`) + - **(D)** `RELEVANCE_WEIGHTS` +- Utils: + - **(M)** `utils/OrionVideoLanguageManager.ts` (`admin` => `curator` schema) + - **(M)** `utils/customMigrations/setOrionLanguageProvider.ts` (`admin` => `curator` schema) + - **(D)** `utils/nextEntityId.ts` + - **(M)** `notification/helpers.ts` (dependency on `NextEntityId` removed) + - **(M)** `utils/offchainState.ts` + - **(M)** `utils/overlay.ts` (prevent deadlocks) +- **(M)** `docker-compose.yml` + - Added RabbitMQ queue + - Added relevance service +- **(M)** `Makefile` + - Turned off `SQD_DEBUG` for mappings in `process` + - Added `relevance-service` + # 4.5.0 ## Affected components: - Auth server: diff --git a/Makefile b/Makefile index 34382be07..3d16707a2 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ process: migrate - @SQD_DEBUG=sqd:processor:mapping node -r dotenv-expand/config lib/processor.js + @node -r dotenv-expand/config lib/processor.js install: @rm -rf node_modules # clean up node_modules to avoid issues with patch-package @@ -18,6 +18,9 @@ serve: serve-auth-api: @npm run auth-server-start +relevance-service: + @npm run relevance-service-start + migrate: @npx squid-typeorm-migration apply diff --git a/assets/patches/@subsquid+typeorm-config+2.0.2.patch b/assets/patches/@subsquid+typeorm-config+2.0.2.patch index a95c6d1b5..b51d69f99 100644 --- a/assets/patches/@subsquid+typeorm-config+2.0.2.patch +++ b/assets/patches/@subsquid+typeorm-config+2.0.2.patch @@ -1,8 +1,8 @@ diff --git a/node_modules/@subsquid/typeorm-config/lib/config.js b/node_modules/@subsquid/typeorm-config/lib/config.js -index 046611f..28fd4e2 100644 +index 046611f..04a0ad2 100644 --- a/node_modules/@subsquid/typeorm-config/lib/config.js +++ b/node_modules/@subsquid/typeorm-config/lib/config.js -@@ -28,22 +28,22 @@ const path = __importStar(require("path")); +@@ -28,22 +28,26 @@ const path = __importStar(require("path")); const process = __importStar(require("process")); const connectionOptions_1 = require("./connectionOptions"); const namingStrategy_1 = require("./namingStrategy"); @@ -13,6 +13,9 @@ index 046611f..28fd4e2 100644 - let model = resolveModel(path.join(dir, 'lib/model')); + let model = resolveModel(path.join(dir, "lib/model")); let migrationsDir = path.join(dir, exports.MIGRATIONS_DIR); ++ let subscribers = process.env.TYPEORM_SUBSCRIBERS_DIR ++ ? [`${path.join(dir, process.env.TYPEORM_SUBSCRIBERS_DIR)}/*.js`] ++ : undefined return { - type: 'postgres', + type: "postgres", @@ -21,6 +24,7 @@ index 046611f..28fd4e2 100644 - migrations: [migrationsDir + '/*.js'], - ...(0, connectionOptions_1.createConnectionOptions)() + migrations: [migrationsDir + `/${options?.file || "*.js"}`], ++ subscribers, + ...(0, connectionOptions_1.createConnectionOptions)(), }; } diff --git a/db/generateViewsMigration.js b/db/generateViewsMigration.js index cecf5321a..8709b4199 100644 --- a/db/generateViewsMigration.js +++ b/db/generateViewsMigration.js @@ -8,7 +8,7 @@ const generateViewsMigration = (versionNumber) => { const className = `Views${versionNumber}` const fileName = `${versionNumber}-Views.js` const fileContent = ` -const { getViewDefinitions } = require('../viewDefinitions') +const { createViews } = require('../../lib/model/views') module.exports = class ${className} { name = '${className}' @@ -21,31 +21,11 @@ module.exports = class ${className} { id SERIAL PRIMARY KEY, height INT );\`) - const viewDefinitions = getViewDefinitions(db); - for (const [tableName, viewConditions] of Object.entries(viewDefinitions)) { - if (Array.isArray(viewConditions)) { - await db.query(\` - DROP VIEW IF EXISTS "\${tableName}" CASCADE - \`) - await db.query(\` - CREATE OR REPLACE VIEW "\${tableName}" AS - SELECT * - FROM "admin"."\${tableName}" AS "this" - WHERE \${viewConditions.map(cond => \`(\${cond})\`).join(' AND ')} - \`); - } else { - await db.query(\` - CREATE OR REPLACE VIEW "\${tableName}" AS (\${viewConditions}) - \`); - } - } - } + await createViews(db); + } async down(db) { - const viewDefinitions = this.getViewDefinitions(db) - for (const viewName of Object.keys(viewDefinitions)) { - await db.query(\`DROP VIEW "\${viewName}"\`) - } + await dropViews(db); } } ` diff --git a/db/migrations/1000000000000-Admin.js b/db/migrations/1000000000000-Admin.js index 574bf6550..84e5d3990 100644 --- a/db/migrations/1000000000000-Admin.js +++ b/db/migrations/1000000000000-Admin.js @@ -2,15 +2,18 @@ module.exports = class Admin1000000000000 { name = 'Admin1000000000000' async up(db) { - // Create a new "admin" schema through which the "hidden" entities can be accessed + // Create "admin" and "curator" schemas, through which some of the "hidden" entities can be accessed await db.query(`CREATE SCHEMA "admin"`) - // Create admin user with "admin" schema in default "search_path" + await db.query(`CREATE SCHEMA "curator"`) + // Create admin user with "admin" and "curator" schemas in default "search_path" await db.query( `CREATE USER "${process.env.DB_ADMIN_USER}" WITH PASSWORD '${process.env.DB_ADMIN_PASS}'` ) await db.query(`GRANT pg_read_all_data TO "${process.env.DB_ADMIN_USER}"`) await db.query(`GRANT pg_write_all_data TO "${process.env.DB_ADMIN_USER}"`) - await db.query(`ALTER USER "${process.env.DB_ADMIN_USER}" SET search_path TO admin,public`) + await db.query( + `ALTER USER "${process.env.DB_ADMIN_USER}" SET search_path TO admin,curator,public` + ) } async down(db) { diff --git a/db/migrations/1708169663879-Data.js b/db/migrations/1708169663879-Data.js deleted file mode 100644 index 0057bff07..000000000 --- a/db/migrations/1708169663879-Data.js +++ /dev/null @@ -1,453 +0,0 @@ -module.exports = class Data1708169663879 { - name = 'Data1708169663879' - - async up(db) { - await db.query(`CREATE TABLE "admin"."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"))`) - await db.query(`CREATE INDEX "IDX_822778b4b1ea8e3b60b127cb8b" ON "admin"."channel_follow" ("user_id") `) - await db.query(`CREATE INDEX "IDX_9bc0651dda94437ec18764a260" ON "admin"."channel_follow" ("channel_id") `) - await db.query(`CREATE TABLE "admin"."video_view_event" ("id" character varying NOT NULL, "video_id" text NOT NULL, "user_id" character varying, "timestamp" TIMESTAMP WITH TIME ZONE NOT NULL, CONSTRAINT "PK_2efd85597a6a7a704fc4d0f7701" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_2e29fba63e12a2b1818e0782d7" ON "admin"."video_view_event" ("video_id") `) - await db.query(`CREATE INDEX "IDX_31e1e798ec387ad905cf98d33b" ON "admin"."video_view_event" ("user_id") `) - await db.query(`CREATE TABLE "admin"."report" ("id" character varying NOT NULL, "user_id" character varying, "channel_id" text, "video_id" text, "timestamp" TIMESTAMP WITH TIME ZONE NOT NULL, "rationale" text NOT NULL, CONSTRAINT "PK_99e4d0bea58cba73c57f935a546" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_c6686efa4cd49fa9a429f01bac" ON "admin"."report" ("user_id") `) - await db.query(`CREATE INDEX "IDX_893057921f4b5cc37a0ef36684" ON "admin"."report" ("channel_id") `) - await db.query(`CREATE INDEX "IDX_f732b6f82095a935db68c9491f" ON "admin"."report" ("video_id") `) - await db.query(`CREATE TABLE "admin"."nft_featuring_request" ("id" character varying NOT NULL, "user_id" character varying, "nft_id" text NOT NULL, "timestamp" TIMESTAMP WITH TIME ZONE NOT NULL, "rationale" text NOT NULL, CONSTRAINT "PK_d0b1ccb74336b30b9575387d328" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_519be2a41216c278c35f254dcb" ON "admin"."nft_featuring_request" ("user_id") `) - await db.query(`CREATE INDEX "IDX_76d87e26cce72ac2e7ffa28dfb" ON "admin"."nft_featuring_request" ("nft_id") `) - await db.query(`CREATE TABLE "admin"."user" ("id" character varying NOT NULL, "is_root" boolean NOT NULL, "permissions" character varying(34) array, CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))`) - await db.query(`CREATE TABLE "storage_bucket" ("id" character varying NOT NULL, "operator_status" jsonb NOT NULL, "accepting_new_bags" boolean NOT NULL, "data_objects_size_limit" numeric NOT NULL, "data_object_count_limit" numeric NOT NULL, "data_objects_count" numeric NOT NULL, "data_objects_size" numeric NOT NULL, CONSTRAINT "PK_97cd0c3fe7f51e34216822e5f91" PRIMARY KEY ("id"))`) - await db.query(`CREATE TABLE "storage_bucket_bag" ("id" character varying NOT NULL, "storage_bucket_id" character varying, "bag_id" character varying, CONSTRAINT "StorageBucketBag_storageBucket_bag" UNIQUE ("storage_bucket_id", "bag_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "PK_9d54c04557134225652d566cc82" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_aaf00b2c7d0cba49f97da14fbb" ON "storage_bucket_bag" ("bag_id") `) - await db.query(`CREATE INDEX "IDX_4c475f6c9300284b095859eec3" ON "storage_bucket_bag" ("storage_bucket_id", "bag_id") `) - await db.query(`CREATE TABLE "distribution_bucket_family" ("id" character varying NOT NULL, CONSTRAINT "PK_8cb7454d1ec34b0d3bb7ecdee4e" PRIMARY KEY ("id"))`) - await db.query(`CREATE TABLE "distribution_bucket_operator" ("id" character varying NOT NULL, "distribution_bucket_id" character varying, "worker_id" integer NOT NULL, "status" character varying(7) NOT NULL, CONSTRAINT "PK_03b87e6e972f414bab94c142285" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_678dc5427cdde0cd4fef2c07a4" ON "distribution_bucket_operator" ("distribution_bucket_id") `) - await db.query(`CREATE TABLE "distribution_bucket" ("id" character varying NOT NULL, "family_id" character varying, "bucket_index" integer NOT NULL, "accepting_new_bags" boolean NOT NULL, "distributing" boolean NOT NULL, CONSTRAINT "PK_c90d25fff461f2f5fa9082e2fb7" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_8cb7454d1ec34b0d3bb7ecdee4" ON "distribution_bucket" ("family_id") `) - await db.query(`CREATE TABLE "distribution_bucket_bag" ("id" character varying NOT NULL, "distribution_bucket_id" character varying, "bag_id" character varying, CONSTRAINT "DistributionBucketBag_distributionBucket_bag" UNIQUE ("distribution_bucket_id", "bag_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "PK_02cb97c17ccabf42e8f5154d002" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_a9810100aee7584680f197c8ff" ON "distribution_bucket_bag" ("bag_id") `) - await db.query(`CREATE INDEX "IDX_32e552d352848d64ab82d38e9a" ON "distribution_bucket_bag" ("distribution_bucket_id", "bag_id") `) - await db.query(`CREATE TABLE "storage_bag" ("id" character varying NOT NULL, "owner" jsonb NOT NULL, CONSTRAINT "PK_242aecdc788d9b22bcbb9ade19a" PRIMARY KEY ("id"))`) - await db.query(`CREATE TABLE "admin"."storage_data_object" ("id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, "is_accepted" boolean NOT NULL, "size" numeric NOT NULL, "storage_bag_id" character varying, "ipfs_hash" text NOT NULL, "type" jsonb, "state_bloat_bond" numeric NOT NULL, "unset_at" TIMESTAMP WITH TIME ZONE, "resolved_urls" text array NOT NULL, CONSTRAINT "PK_61f224a4aef08f580a5ab4aadf0" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_ff8014300b8039dbaed764f51b" ON "admin"."storage_data_object" ("storage_bag_id") `) - await db.query(`CREATE TABLE "admin"."banned_member" ("id" character varying NOT NULL, "member_id" character varying, "channel_id" character varying, CONSTRAINT "BannedMember_member_channel" UNIQUE ("member_id", "channel_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "PK_ebdf9a9c6d88f1116a5f2d0815d" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_ed36c6c26bf5410796c2fc21f7" ON "admin"."banned_member" ("channel_id") `) - await db.query(`CREATE INDEX "IDX_f29ff095bdb945975deca021ad" ON "admin"."banned_member" ("member_id", "channel_id") `) - await db.query(`CREATE TABLE "app" ("id" character varying NOT NULL, "name" text NOT NULL, "owner_member_id" character varying, "website_url" text, "use_uri" text, "small_icon" text, "medium_icon" text, "big_icon" text, "one_liner" text, "description" text, "terms_of_service" text, "platforms" text array, "category" text, "auth_key" text, CONSTRAINT "App_name" UNIQUE ("name") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "PK_9478629fc093d229df09e560aea" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_f36adbb7b096ceeb6f3e80ad14" ON "app" ("name") `) - await db.query(`CREATE INDEX "IDX_c9cc395bbc485f70a15be64553" ON "app" ("owner_member_id") `) - await db.query(`CREATE TABLE "admin"."channel" ("id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, "owner_member_id" character varying, "title" text, "description" text, "cover_photo_id" character varying, "avatar_photo_id" character varying, "is_public" boolean, "is_censored" boolean NOT NULL, "is_excluded" boolean NOT NULL, "language" text, "created_in_block" integer NOT NULL, "reward_account" text NOT NULL, "channel_state_bloat_bond" numeric NOT NULL, "follows_num" integer NOT NULL, "video_views_num" integer NOT NULL, "entry_app_id" character varying, "total_videos_created" integer NOT NULL, "cumulative_reward_claimed" numeric NOT NULL, "cumulative_reward" numeric NOT NULL, "channel_weight" numeric, "ypp_status" jsonb NOT NULL, CONSTRAINT "PK_590f33ee6ee7d76437acf362e39" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_a4752a0a0899dedc4d18077dd0" ON "admin"."channel" ("created_at") `) - await db.query(`CREATE INDEX "IDX_25c85bc448b5e236a4c1a5f789" ON "admin"."channel" ("owner_member_id") `) - await db.query(`CREATE INDEX "IDX_a77e12f3d8c6ced020e179a5e9" ON "admin"."channel" ("cover_photo_id") `) - await db.query(`CREATE INDEX "IDX_6997e94413b3f2f25a84e4a96f" ON "admin"."channel" ("avatar_photo_id") `) - await db.query(`CREATE INDEX "IDX_e58a2e1d78b8eccf40531a7fdb" ON "admin"."channel" ("language") `) - await db.query(`CREATE INDEX "IDX_118ecfa0199aeb5a014906933e" ON "admin"."channel" ("entry_app_id") `) - await db.query(`CREATE TABLE "admin"."video_featured_in_category" ("id" character varying NOT NULL, "video_id" character varying, "category_id" character varying, "video_cut_url" text, CONSTRAINT "VideoFeaturedInCategory_category_video" UNIQUE ("category_id", "video_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "PK_f84d38b5cdb7567ac04d6e9d209" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_7b16ddad43901921a8d3c8eab7" ON "admin"."video_featured_in_category" ("video_id") `) - await db.query(`CREATE INDEX "IDX_6d0917e1ac0cc06c8075bcf256" ON "admin"."video_featured_in_category" ("category_id", "video_id") `) - await db.query(`CREATE TABLE "admin"."video_category" ("id" character varying NOT NULL, "name" text, "description" text, "parent_category_id" character varying, "is_supported" boolean NOT NULL, "created_in_block" integer NOT NULL, CONSTRAINT "PK_2a5c61f32e9636ee10821e9a58d" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_cbe7e5d162a819e4ee2e2f6105" ON "admin"."video_category" ("name") `) - await db.query(`CREATE INDEX "IDX_da26b34f037c0d59d3c0d0646e" ON "admin"."video_category" ("parent_category_id") `) - await db.query(`CREATE TABLE "admin"."license" ("id" character varying NOT NULL, "code" integer, "attribution" text, "custom_text" text, CONSTRAINT "PK_f168ac1ca5ba87286d03b2ef905" PRIMARY KEY ("id"))`) - await db.query(`CREATE TABLE "admin"."video_subtitle" ("id" character varying NOT NULL, "video_id" character varying, "type" text NOT NULL, "language" text, "mime_type" text NOT NULL, "asset_id" character varying, CONSTRAINT "PK_2ac3e585fc608e673e7fbf94d8e" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_2203674f18d8052ed6bac39625" ON "admin"."video_subtitle" ("video_id") `) - await db.query(`CREATE INDEX "IDX_ffa63c28188eecc32af921bfc3" ON "admin"."video_subtitle" ("language") `) - await db.query(`CREATE INDEX "IDX_b6eabfb8de4128b28d73681020" ON "admin"."video_subtitle" ("asset_id") `) - await db.query(`CREATE TABLE "admin"."comment_reaction" ("id" character varying NOT NULL, "reaction_id" integer NOT NULL, "member_id" character varying, "comment_id" character varying, "video_id" character varying, CONSTRAINT "PK_87f27d282c06eb61b1e0cde2d24" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_15080d9fb7cf8b563103dd9d90" ON "admin"."comment_reaction" ("member_id") `) - await db.query(`CREATE INDEX "IDX_962582f04d3f639e33f43c54bb" ON "admin"."comment_reaction" ("comment_id") `) - await db.query(`CREATE INDEX "IDX_d7995b1d57614a6fbd0c103874" ON "admin"."comment_reaction" ("video_id") `) - await db.query(`CREATE TABLE "admin"."comment" ("id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, "author_id" character varying, "text" text NOT NULL, "video_id" character varying, "status" character varying(9) NOT NULL, "reactions_count_by_reaction_id" jsonb, "parent_comment_id" character varying, "replies_count" integer NOT NULL, "reactions_count" integer NOT NULL, "reactions_and_replies_count" integer NOT NULL, "is_edited" boolean NOT NULL, "is_excluded" boolean NOT NULL, CONSTRAINT "PK_0b0e4bbc8415ec426f87f3a88e2" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_3ce66469b26697baa097f8da92" ON "admin"."comment" ("author_id") `) - await db.query(`CREATE INDEX "IDX_1ff03403fd31dfeaba0623a89c" ON "admin"."comment" ("video_id") `) - await db.query(`CREATE INDEX "IDX_c3c2abe750c76c7c8e305f71f2" ON "admin"."comment" ("status") `) - await db.query(`CREATE INDEX "IDX_ac69bddf8202b7c0752d9dc8f3" ON "admin"."comment" ("parent_comment_id") `) - await db.query(`CREATE TABLE "admin"."video_reaction" ("id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, "reaction" character varying(6) NOT NULL, "member_id" character varying, "video_id" character varying, CONSTRAINT "PK_504876585c394f4ab33665dd44b" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_73dda64f53bbc7ec7035d5e7f0" ON "admin"."video_reaction" ("member_id") `) - await db.query(`CREATE INDEX "IDX_436a3836eb47acb5e1e3c88dde" ON "admin"."video_reaction" ("video_id") `) - await db.query(`CREATE TABLE "admin"."video" ("id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, "channel_id" character varying, "category_id" character varying, "title" text, "description" text, "duration" integer, "thumbnail_photo_id" character varying, "language" text, "orion_language" text, "has_marketing" boolean, "published_before_joystream" TIMESTAMP WITH TIME ZONE, "is_public" boolean, "is_censored" boolean NOT NULL, "is_excluded" boolean NOT NULL, "is_explicit" boolean, "license_id" character varying, "media_id" character varying, "video_state_bloat_bond" numeric NOT NULL, "created_in_block" integer NOT NULL, "is_comment_section_enabled" boolean NOT NULL, "pinned_comment_id" character varying, "comments_count" integer NOT NULL, "is_reaction_feature_enabled" boolean NOT NULL, "reactions_count_by_reaction_id" jsonb, "reactions_count" integer NOT NULL, "views_num" integer NOT NULL, "entry_app_id" character varying, "yt_video_id" text, "video_relevance" numeric NOT NULL, CONSTRAINT "PK_1a2f3856250765d72e7e1636c8e" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_fe2b4b6aace15f1b6610830846" ON "admin"."video" ("created_at") `) - await db.query(`CREATE INDEX "IDX_81b11ef99a9db9ef1aed040d75" ON "admin"."video" ("channel_id") `) - await db.query(`CREATE INDEX "IDX_2a5c61f32e9636ee10821e9a58" ON "admin"."video" ("category_id") `) - await db.query(`CREATE INDEX "IDX_8530d052cc79b420f7ce2b4e09" ON "admin"."video" ("thumbnail_photo_id") `) - await db.query(`CREATE INDEX "IDX_57b335fa0a960877caf6d2fc29" ON "admin"."video" ("orion_language") `) - await db.query(`CREATE INDEX "IDX_3ec633ae5d0477f512b4ed957d" ON "admin"."video" ("license_id") `) - await db.query(`CREATE INDEX "IDX_2db879ed42e3308fe65e679672" ON "admin"."video" ("media_id") `) - await db.query(`CREATE INDEX "IDX_54f88a7decf7d22fd9bd9fa439" ON "admin"."video" ("pinned_comment_id") `) - await db.query(`CREATE INDEX "IDX_6c49ad08c44d36d11f77c426e4" ON "admin"."video" ("entry_app_id") `) - await db.query(`CREATE INDEX "IDX_f33816960d690ac836f5d5c28a" ON "admin"."video" ("video_relevance") `) - await db.query(`CREATE TABLE "admin"."bid" ("id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, "auction_id" character varying, "nft_id" character varying, "bidder_id" character varying, "amount" numeric NOT NULL, "is_canceled" boolean NOT NULL, "created_in_block" integer NOT NULL, "index_in_block" integer NOT NULL, "previous_top_bid_id" character varying, CONSTRAINT "PK_ed405dda320051aca2dcb1a50bb" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_9e594e5a61c0f3cb25679f6ba8" ON "admin"."bid" ("auction_id") `) - await db.query(`CREATE INDEX "IDX_3caf2d6b31d2fe45a2b85b8191" ON "admin"."bid" ("nft_id") `) - await db.query(`CREATE INDEX "IDX_e7618559409a903a897164156b" ON "admin"."bid" ("bidder_id") `) - await db.query(`CREATE INDEX "IDX_32cb73025ec49c87f4c594a265" ON "admin"."bid" ("previous_top_bid_id") `) - await db.query(`CREATE TABLE "admin"."owned_nft" ("id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, "video_id" character varying NOT NULL, "owner" jsonb NOT NULL, "transactional_status" jsonb, "creator_royalty" numeric, "last_sale_price" numeric, "last_sale_date" TIMESTAMP WITH TIME ZONE, "is_featured" boolean NOT NULL, CONSTRAINT "OwnedNft_video" UNIQUE ("video_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "REL_466896e39b9ec953f4f2545622" UNIQUE ("video_id"), CONSTRAINT "PK_5e0c289b350e863668fff44bb56" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_8c7201ed7d4765dcbcc3609356" ON "admin"."owned_nft" ("created_at") `) - await db.query(`CREATE INDEX "IDX_466896e39b9ec953f4f2545622" ON "admin"."owned_nft" ("video_id") `) - await db.query(`CREATE TABLE "admin"."auction" ("id" character varying NOT NULL, "nft_id" character varying, "winning_member_id" character varying, "starting_price" numeric NOT NULL, "buy_now_price" numeric, "auction_type" jsonb NOT NULL, "top_bid_id" character varying, "starts_at_block" integer NOT NULL, "ended_at_block" integer, "is_canceled" boolean NOT NULL, "is_completed" boolean NOT NULL, CONSTRAINT "PK_9dc876c629273e71646cf6dfa67" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_cfb47e97e60c9d1462576f85a8" ON "admin"."auction" ("nft_id") `) - await db.query(`CREATE INDEX "IDX_a3127ec87cccc5696b92cac4e0" ON "admin"."auction" ("winning_member_id") `) - await db.query(`CREATE INDEX "IDX_1673ad4b059742fbabfc40b275" ON "admin"."auction" ("top_bid_id") `) - 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, "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 "admin"."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 "admin"."event" ("in_extrinsic") `) - await db.query(`CREATE INDEX "IDX_2c15918ff289396205521c5f3c" ON "admin"."event" ("timestamp") `) - 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"))`) - await db.query(`CREATE INDEX "IDX_6bfa96ab97f1a09d73091294ef" ON "notification" ("account_id") `) - await db.query(`CREATE INDEX "IDX_122be1f0696e0255acf95f9e33" ON "notification" ("event_id") `) - await db.query(`CREATE TABLE "admin"."account" ("id" character varying NOT NULL, "user_id" character varying NOT NULL, "email" text NOT NULL, "is_email_confirmed" boolean NOT NULL, "is_blocked" boolean NOT NULL, "registered_at" TIMESTAMP WITH TIME ZONE NOT NULL, "membership_id" character varying NOT NULL, "joystream_account" text NOT NULL, "notification_preferences" jsonb NOT NULL, "referrer_channel_id" text, CONSTRAINT "Account_joystreamAccount" UNIQUE ("joystream_account") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "Account_membership" UNIQUE ("membership_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "Account_email" UNIQUE ("email") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "Account_user" UNIQUE ("user_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "REL_efef1e5fdbe318a379c06678c5" UNIQUE ("user_id"), CONSTRAINT "REL_601b93655bcbe73cb58d8c80cd" UNIQUE ("membership_id"), CONSTRAINT "PK_54115ee388cdb6d86bb4bf5b2ea" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_efef1e5fdbe318a379c06678c5" ON "admin"."account" ("user_id") `) - await db.query(`CREATE INDEX "IDX_4c8f96ccf523e9a3faefd5bdd4" ON "admin"."account" ("email") `) - await db.query(`CREATE INDEX "IDX_601b93655bcbe73cb58d8c80cd" ON "admin"."account" ("membership_id") `) - await db.query(`CREATE INDEX "IDX_df4da05a7a80c1afd18b8f0990" ON "admin"."account" ("joystream_account") `) - await db.query(`CREATE TABLE "encryption_artifacts" ("id" character varying NOT NULL, "account_id" character varying NOT NULL, "cipher_iv" text NOT NULL, "encrypted_seed" text NOT NULL, CONSTRAINT "EncryptionArtifacts_account" UNIQUE ("account_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "REL_ec8f68a544aadc4fbdadefe4a0" UNIQUE ("account_id"), CONSTRAINT "PK_6441471581ba6d149ad75655bd0" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_ec8f68a544aadc4fbdadefe4a0" ON "encryption_artifacts" ("account_id") `) - await db.query(`CREATE TABLE "admin"."session" ("id" character varying NOT NULL, "browser" text NOT NULL, "os" text NOT NULL, "device" text NOT NULL, "device_type" text, "user_id" character varying, "account_id" character varying, "ip" text NOT NULL, "started_at" TIMESTAMP WITH TIME ZONE NOT NULL, "expiry" TIMESTAMP WITH TIME ZONE NOT NULL, CONSTRAINT "PK_f55da76ac1c3ac420f444d2ff11" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_30e98e8746699fb9af235410af" ON "admin"."session" ("user_id") `) - await db.query(`CREATE INDEX "IDX_fae5a6b4a57f098e9af8520d49" ON "admin"."session" ("account_id") `) - await db.query(`CREATE INDEX "IDX_213b5a19bfdbe0ab6e06b1dede" ON "admin"."session" ("ip") `) - await db.query(`CREATE TABLE "session_encryption_artifacts" ("id" character varying NOT NULL, "session_id" character varying NOT NULL, "cipher_iv" text NOT NULL, "cipher_key" text NOT NULL, CONSTRAINT "SessionEncryptionArtifacts_session" UNIQUE ("session_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "REL_3612880efd8926a17eba5ab0e1" UNIQUE ("session_id"), CONSTRAINT "PK_e328da2643599e265a848219885" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_3612880efd8926a17eba5ab0e1" ON "session_encryption_artifacts" ("session_id") `) - await db.query(`CREATE TABLE "admin"."token" ("id" character varying NOT NULL, "type" character varying(18) NOT NULL, "issued_at" TIMESTAMP WITH TIME ZONE NOT NULL, "expiry" TIMESTAMP WITH TIME ZONE NOT NULL, "issued_for_id" character varying, CONSTRAINT "PK_82fae97f905930df5d62a702fc9" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_a6fe18c105f85a63d761ccb078" ON "admin"."token" ("issued_for_id") `) - await db.query(`CREATE TABLE "admin"."nft_history_entry" ("id" character varying NOT NULL, "nft_id" character varying, "event_id" character varying, CONSTRAINT "PK_9018e80b335a965a54959c4c6e2" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_57f51d35ecab042478fe2e31c1" ON "admin"."nft_history_entry" ("nft_id") `) - await db.query(`CREATE INDEX "IDX_d1a28b178f5d028d048d40ce20" ON "admin"."nft_history_entry" ("event_id") `) - await db.query(`CREATE TABLE "admin"."nft_activity" ("id" character varying NOT NULL, "member_id" character varying, "event_id" character varying, CONSTRAINT "PK_1553b1bbf8000039875a6e31536" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_18a65713a9fd0715c7a980f5d5" ON "admin"."nft_activity" ("member_id") `) - await db.query(`CREATE INDEX "IDX_94d325a753f2c08fdd416eb095" ON "admin"."nft_activity" ("event_id") `) - await db.query(`CREATE TABLE "admin"."email_delivery_attempt" ("id" character varying NOT NULL, "notification_delivery_id" character varying, "status" jsonb NOT NULL, "timestamp" TIMESTAMP WITH TIME ZONE NOT NULL, CONSTRAINT "PK_876948339083a2f1092245f7a32" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_f985b9b362249af72cac0f52a3" ON "admin"."email_delivery_attempt" ("notification_delivery_id") `) - await db.query(`CREATE TABLE "admin"."notification_email_delivery" ("id" character varying NOT NULL, "notification_id" character varying, "discard" boolean NOT NULL, CONSTRAINT "PK_60dc7ff42a7abf7b0d44bf60516" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_3b756627c3146db150d66d1292" ON "admin"."notification_email_delivery" ("notification_id") `) - await db.query(`CREATE TABLE "admin"."video_hero" ("id" character varying NOT NULL, "video_id" character varying, "hero_title" text NOT NULL, "hero_video_cut_url" text NOT NULL, "hero_poster_url" text NOT NULL, "activated_at" TIMESTAMP WITH TIME ZONE, CONSTRAINT "PK_f3b63979879773378afac0b9495" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_9feac5d9713a9f07e32eb8ba7a" ON "admin"."video_hero" ("video_id") `) - await db.query(`CREATE TABLE "admin"."video_media_encoding" ("id" character varying NOT NULL, "codec_name" text, "container" text, "mime_media_type" text, CONSTRAINT "PK_52e25874f8d8a381e154d1125e0" PRIMARY KEY ("id"))`) - await db.query(`CREATE TABLE "admin"."video_media_metadata" ("id" character varying NOT NULL, "encoding_id" character varying, "pixel_width" integer, "pixel_height" integer, "size" numeric, "video_id" character varying NOT NULL, "created_in_block" integer NOT NULL, CONSTRAINT "VideoMediaMetadata_video" UNIQUE ("video_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "REL_4dc101240e8e1536b770aee202" UNIQUE ("video_id"), CONSTRAINT "PK_86a13815734e589cd86d0465e2d" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_5944dc5896cb16bd395414a0ce" ON "admin"."video_media_metadata" ("encoding_id") `) - await db.query(`CREATE INDEX "IDX_4dc101240e8e1536b770aee202" ON "admin"."video_media_metadata" ("video_id") `) - await db.query(`CREATE TABLE "admin"."gateway_config" ("id" character varying NOT NULL, "value" text NOT NULL, "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL, CONSTRAINT "PK_db1fa5a857fb6292eee4c493e6f" PRIMARY KEY ("id"))`) - await db.query(`CREATE TABLE "admin"."exclusion" ("id" character varying NOT NULL, "channel_id" text, "video_id" text, "timestamp" TIMESTAMP WITH TIME ZONE NOT NULL, "rationale" text NOT NULL, CONSTRAINT "PK_7f8dcde2e607a96d66dce002e74" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_efba55b85909822c2b7655dfb8" ON "admin"."exclusion" ("channel_id") `) - await db.query(`CREATE INDEX "IDX_2729041b2f528a6c5833fdb3e5" ON "admin"."exclusion" ("video_id") `) - await db.query(`CREATE TABLE "storage_bucket_operator_metadata" ("id" character varying NOT NULL, "storage_bucket_id" character varying NOT NULL, "node_endpoint" text, "node_location" jsonb, "extra" text, CONSTRAINT "StorageBucketOperatorMetadata_storageBucket" UNIQUE ("storage_bucket_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "REL_7beffc9530b3f307bc1169cb52" UNIQUE ("storage_bucket_id"), CONSTRAINT "PK_9846a397400ae1a39b21fbd02d4" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_7beffc9530b3f307bc1169cb52" ON "storage_bucket_operator_metadata" ("storage_bucket_id") `) - await db.query(`CREATE TABLE "distribution_bucket_family_metadata" ("id" character varying NOT NULL, "family_id" character varying NOT NULL, "region" text, "description" text, "areas" jsonb, "latency_test_targets" text array, CONSTRAINT "DistributionBucketFamilyMetadata_family" UNIQUE ("family_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "REL_dd93ca0ea24f3e7a02f11c4c14" UNIQUE ("family_id"), CONSTRAINT "PK_df7a270835bb313d3ef17bdee2f" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_dd93ca0ea24f3e7a02f11c4c14" ON "distribution_bucket_family_metadata" ("family_id") `) - await db.query(`CREATE INDEX "IDX_5510d3b244a63d6ec702faa426" ON "distribution_bucket_family_metadata" ("region") `) - await db.query(`CREATE TABLE "distribution_bucket_operator_metadata" ("id" character varying NOT NULL, "distirbution_bucket_operator_id" character varying NOT NULL, "node_endpoint" text, "node_location" jsonb, "extra" text, CONSTRAINT "DistributionBucketOperatorMetadata_distirbutionBucketOperator" UNIQUE ("distirbution_bucket_operator_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "REL_69ec9bdc975b95f7dff94a7106" UNIQUE ("distirbution_bucket_operator_id"), CONSTRAINT "PK_9bbecaa12f30e3826922688274f" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_69ec9bdc975b95f7dff94a7106" ON "distribution_bucket_operator_metadata" ("distirbution_bucket_operator_id") `) - await db.query(`CREATE TABLE "admin"."channel_verification" ("id" character varying NOT NULL, "channel_id" character varying, "timestamp" TIMESTAMP WITH TIME ZONE NOT NULL, CONSTRAINT "PK_0a61c78b114ed3e92300e09afaa" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_f13d5d785670f46de668575139" ON "admin"."channel_verification" ("channel_id") `) - await db.query(`CREATE TABLE "admin"."channel_suspension" ("id" character varying NOT NULL, "channel_id" character varying, "timestamp" TIMESTAMP WITH TIME ZONE NOT NULL, CONSTRAINT "PK_226679cee9a8d0e5af18f70a1da" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_e30ebff1042c010ff88b87f4f7" ON "admin"."channel_suspension" ("channel_id") `) - await db.query(`CREATE TABLE "curator_group" ("id" character varying NOT NULL, "is_active" boolean NOT NULL, CONSTRAINT "PK_0b4c0ab279d72bcbf4e16b65ff1" PRIMARY KEY ("id"))`) - await db.query(`CREATE TABLE "curator" ("id" character varying NOT NULL, CONSTRAINT "PK_5791051a62d2c2dfc593d38ab57" PRIMARY KEY ("id"))`) - await db.query(`CREATE TABLE "member_metadata" ("id" character varying NOT NULL, "name" text, "avatar" jsonb, "about" text, "member_id" character varying NOT NULL, CONSTRAINT "MemberMetadata_member" UNIQUE ("member_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "REL_e7e4d350f82ae2383894f465ed" UNIQUE ("member_id"), CONSTRAINT "PK_d3fcc374696465f3c0ac3ba8708" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_e7e4d350f82ae2383894f465ed" ON "member_metadata" ("member_id") `) - await db.query(`CREATE TABLE "next_entity_id" ("entity_name" character varying NOT NULL, "next_id" bigint NOT NULL, CONSTRAINT "PK_09a3b40db622a65096e7344d7ae" PRIMARY KEY ("entity_name"))`) - await db.query(`ALTER TABLE "admin"."channel_follow" ADD CONSTRAINT "FK_822778b4b1ea8e3b60b127cb8b1" FOREIGN KEY ("user_id") REFERENCES "admin"."user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."video_view_event" ADD CONSTRAINT "FK_31e1e798ec387ad905cf98d33b0" FOREIGN KEY ("user_id") REFERENCES "admin"."user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."report" ADD CONSTRAINT "FK_c6686efa4cd49fa9a429f01bac8" FOREIGN KEY ("user_id") REFERENCES "admin"."user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."nft_featuring_request" ADD CONSTRAINT "FK_519be2a41216c278c35f254dcba" FOREIGN KEY ("user_id") REFERENCES "admin"."user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "storage_bucket_bag" ADD CONSTRAINT "FK_791e2f82e3919ffcef8712aa1b9" FOREIGN KEY ("storage_bucket_id") REFERENCES "storage_bucket"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "storage_bucket_bag" ADD CONSTRAINT "FK_aaf00b2c7d0cba49f97da14fbba" FOREIGN KEY ("bag_id") REFERENCES "storage_bag"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "distribution_bucket_operator" ADD CONSTRAINT "FK_678dc5427cdde0cd4fef2c07a43" FOREIGN KEY ("distribution_bucket_id") REFERENCES "distribution_bucket"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "distribution_bucket" ADD CONSTRAINT "FK_8cb7454d1ec34b0d3bb7ecdee4e" FOREIGN KEY ("family_id") REFERENCES "distribution_bucket_family"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "distribution_bucket_bag" ADD CONSTRAINT "FK_8a807921f1aae60d4ba94895826" FOREIGN KEY ("distribution_bucket_id") REFERENCES "distribution_bucket"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "distribution_bucket_bag" ADD CONSTRAINT "FK_a9810100aee7584680f197c8ff0" FOREIGN KEY ("bag_id") REFERENCES "storage_bag"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."storage_data_object" ADD CONSTRAINT "FK_ff8014300b8039dbaed764f51bc" FOREIGN KEY ("storage_bag_id") REFERENCES "storage_bag"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."banned_member" ADD CONSTRAINT "FK_b94ea874da235d9b6fbc35cf58e" FOREIGN KEY ("member_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."banned_member" ADD CONSTRAINT "FK_ed36c6c26bf5410796c2fc21f74" FOREIGN KEY ("channel_id") REFERENCES "admin"."channel"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "app" ADD CONSTRAINT "FK_c9cc395bbc485f70a15be64553e" FOREIGN KEY ("owner_member_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."channel" ADD CONSTRAINT "FK_25c85bc448b5e236a4c1a5f7895" FOREIGN KEY ("owner_member_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."channel" ADD CONSTRAINT "FK_a77e12f3d8c6ced020e179a5e94" FOREIGN KEY ("cover_photo_id") REFERENCES "admin"."storage_data_object"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."channel" ADD CONSTRAINT "FK_6997e94413b3f2f25a84e4a96f8" FOREIGN KEY ("avatar_photo_id") REFERENCES "admin"."storage_data_object"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."channel" ADD CONSTRAINT "FK_118ecfa0199aeb5a014906933e8" FOREIGN KEY ("entry_app_id") REFERENCES "app"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."video_featured_in_category" ADD CONSTRAINT "FK_7b16ddad43901921a8d3c8eab71" FOREIGN KEY ("video_id") REFERENCES "admin"."video"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."video_featured_in_category" ADD CONSTRAINT "FK_0e6bb49ce9d022cd872f3ab4288" FOREIGN KEY ("category_id") REFERENCES "admin"."video_category"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."video_category" ADD CONSTRAINT "FK_da26b34f037c0d59d3c0d0646e9" FOREIGN KEY ("parent_category_id") REFERENCES "admin"."video_category"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."video_subtitle" ADD CONSTRAINT "FK_2203674f18d8052ed6bac396252" FOREIGN KEY ("video_id") REFERENCES "admin"."video"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."video_subtitle" ADD CONSTRAINT "FK_b6eabfb8de4128b28d73681020f" FOREIGN KEY ("asset_id") REFERENCES "admin"."storage_data_object"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."comment_reaction" ADD CONSTRAINT "FK_15080d9fb7cf8b563103dd9d900" FOREIGN KEY ("member_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."comment_reaction" ADD CONSTRAINT "FK_962582f04d3f639e33f43c54bbc" FOREIGN KEY ("comment_id") REFERENCES "admin"."comment"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."comment_reaction" ADD CONSTRAINT "FK_d7995b1d57614a6fbd0c103874d" FOREIGN KEY ("video_id") REFERENCES "admin"."video"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."comment" ADD CONSTRAINT "FK_3ce66469b26697baa097f8da923" FOREIGN KEY ("author_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."comment" ADD CONSTRAINT "FK_1ff03403fd31dfeaba0623a89cf" FOREIGN KEY ("video_id") REFERENCES "admin"."video"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."comment" ADD CONSTRAINT "FK_ac69bddf8202b7c0752d9dc8f32" FOREIGN KEY ("parent_comment_id") REFERENCES "admin"."comment"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."video_reaction" ADD CONSTRAINT "FK_73dda64f53bbc7ec7035d5e7f09" FOREIGN KEY ("member_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."video_reaction" ADD CONSTRAINT "FK_436a3836eb47acb5e1e3c88ddea" FOREIGN KEY ("video_id") REFERENCES "admin"."video"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."video" ADD CONSTRAINT "FK_81b11ef99a9db9ef1aed040d750" FOREIGN KEY ("channel_id") REFERENCES "admin"."channel"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."video" ADD CONSTRAINT "FK_2a5c61f32e9636ee10821e9a58d" FOREIGN KEY ("category_id") REFERENCES "admin"."video_category"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."video" ADD CONSTRAINT "FK_8530d052cc79b420f7ce2b4e09d" FOREIGN KEY ("thumbnail_photo_id") REFERENCES "admin"."storage_data_object"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."video" ADD CONSTRAINT "FK_3ec633ae5d0477f512b4ed957d6" FOREIGN KEY ("license_id") REFERENCES "admin"."license"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."video" ADD CONSTRAINT "FK_2db879ed42e3308fe65e6796729" FOREIGN KEY ("media_id") REFERENCES "admin"."storage_data_object"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."video" ADD CONSTRAINT "FK_54f88a7decf7d22fd9bd9fa439a" FOREIGN KEY ("pinned_comment_id") REFERENCES "admin"."comment"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."video" ADD CONSTRAINT "FK_6c49ad08c44d36d11f77c426e43" FOREIGN KEY ("entry_app_id") REFERENCES "app"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."bid" ADD CONSTRAINT "FK_9e594e5a61c0f3cb25679f6ba8d" FOREIGN KEY ("auction_id") REFERENCES "admin"."auction"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."bid" ADD CONSTRAINT "FK_3caf2d6b31d2fe45a2b85b81912" FOREIGN KEY ("nft_id") REFERENCES "admin"."owned_nft"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."bid" ADD CONSTRAINT "FK_e7618559409a903a897164156b7" FOREIGN KEY ("bidder_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."bid" ADD CONSTRAINT "FK_32cb73025ec49c87f4c594a265f" FOREIGN KEY ("previous_top_bid_id") REFERENCES "admin"."bid"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."owned_nft" ADD CONSTRAINT "FK_466896e39b9ec953f4f2545622d" FOREIGN KEY ("video_id") REFERENCES "admin"."video"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."auction" ADD CONSTRAINT "FK_cfb47e97e60c9d1462576f85a88" FOREIGN KEY ("nft_id") REFERENCES "admin"."owned_nft"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."auction" ADD CONSTRAINT "FK_a3127ec87cccc5696b92cac4e09" FOREIGN KEY ("winning_member_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."auction" ADD CONSTRAINT "FK_1673ad4b059742fbabfc40b275c" FOREIGN KEY ("top_bid_id") REFERENCES "admin"."bid"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "auction_whitelisted_member" ADD CONSTRAINT "FK_aad797677bc7c7c7dc1f1d397f5" FOREIGN KEY ("auction_id") REFERENCES "admin"."auction"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "auction_whitelisted_member" ADD CONSTRAINT "FK_d5ae4854487c7658b64225be305" FOREIGN KEY ("member_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_6bfa96ab97f1a09d73091294efc" FOREIGN KEY ("account_id") REFERENCES "admin"."account"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_122be1f0696e0255acf95f9e336" FOREIGN KEY ("event_id") REFERENCES "admin"."event"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."account" ADD CONSTRAINT "FK_efef1e5fdbe318a379c06678c51" FOREIGN KEY ("user_id") REFERENCES "admin"."user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."account" ADD CONSTRAINT "FK_601b93655bcbe73cb58d8c80cd3" FOREIGN KEY ("membership_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "encryption_artifacts" ADD CONSTRAINT "FK_ec8f68a544aadc4fbdadefe4a0a" FOREIGN KEY ("account_id") REFERENCES "admin"."account"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."session" ADD CONSTRAINT "FK_30e98e8746699fb9af235410aff" FOREIGN KEY ("user_id") REFERENCES "admin"."user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."session" ADD CONSTRAINT "FK_fae5a6b4a57f098e9af8520d499" FOREIGN KEY ("account_id") REFERENCES "admin"."account"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "session_encryption_artifacts" ADD CONSTRAINT "FK_3612880efd8926a17eba5ab0e1a" FOREIGN KEY ("session_id") REFERENCES "admin"."session"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."token" ADD CONSTRAINT "FK_a6fe18c105f85a63d761ccb0780" FOREIGN KEY ("issued_for_id") REFERENCES "admin"."account"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."nft_history_entry" ADD CONSTRAINT "FK_57f51d35ecab042478fe2e31c19" FOREIGN KEY ("nft_id") REFERENCES "admin"."owned_nft"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."nft_history_entry" ADD CONSTRAINT "FK_d1a28b178f5d028d048d40ce208" FOREIGN KEY ("event_id") REFERENCES "admin"."event"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."nft_activity" ADD CONSTRAINT "FK_18a65713a9fd0715c7a980f5d54" FOREIGN KEY ("member_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."nft_activity" ADD CONSTRAINT "FK_94d325a753f2c08fdd416eb095f" FOREIGN KEY ("event_id") REFERENCES "admin"."event"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."email_delivery_attempt" ADD CONSTRAINT "FK_f985b9b362249af72cac0f52a3b" FOREIGN KEY ("notification_delivery_id") REFERENCES "admin"."notification_email_delivery"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."notification_email_delivery" ADD CONSTRAINT "FK_3b756627c3146db150d66d12929" FOREIGN KEY ("notification_id") REFERENCES "notification"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."video_hero" ADD CONSTRAINT "FK_9feac5d9713a9f07e32eb8ba7a1" FOREIGN KEY ("video_id") REFERENCES "admin"."video"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."video_media_metadata" ADD CONSTRAINT "FK_5944dc5896cb16bd395414a0ce0" FOREIGN KEY ("encoding_id") REFERENCES "admin"."video_media_encoding"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."video_media_metadata" ADD CONSTRAINT "FK_4dc101240e8e1536b770aee202a" FOREIGN KEY ("video_id") REFERENCES "admin"."video"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "storage_bucket_operator_metadata" ADD CONSTRAINT "FK_7beffc9530b3f307bc1169cb524" FOREIGN KEY ("storage_bucket_id") REFERENCES "storage_bucket"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "distribution_bucket_family_metadata" ADD CONSTRAINT "FK_dd93ca0ea24f3e7a02f11c4c149" FOREIGN KEY ("family_id") REFERENCES "distribution_bucket_family"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "distribution_bucket_operator_metadata" ADD CONSTRAINT "FK_69ec9bdc975b95f7dff94a71069" FOREIGN KEY ("distirbution_bucket_operator_id") REFERENCES "distribution_bucket_operator"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."channel_verification" ADD CONSTRAINT "FK_f13d5d785670f46de668575139c" FOREIGN KEY ("channel_id") REFERENCES "admin"."channel"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "admin"."channel_suspension" ADD CONSTRAINT "FK_e30ebff1042c010ff88b87f4f7a" FOREIGN KEY ("channel_id") REFERENCES "admin"."channel"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "member_metadata" ADD CONSTRAINT "FK_e7e4d350f82ae2383894f465ede" FOREIGN KEY ("member_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - } - - async down(db) { - await db.query(`DROP TABLE "admin"."channel_follow"`) - await db.query(`DROP INDEX "admin"."IDX_822778b4b1ea8e3b60b127cb8b"`) - await db.query(`DROP INDEX "admin"."IDX_9bc0651dda94437ec18764a260"`) - await db.query(`DROP TABLE "admin"."video_view_event"`) - await db.query(`DROP INDEX "admin"."IDX_2e29fba63e12a2b1818e0782d7"`) - await db.query(`DROP INDEX "admin"."IDX_31e1e798ec387ad905cf98d33b"`) - await db.query(`DROP TABLE "admin"."report"`) - await db.query(`DROP INDEX "admin"."IDX_c6686efa4cd49fa9a429f01bac"`) - await db.query(`DROP INDEX "admin"."IDX_893057921f4b5cc37a0ef36684"`) - await db.query(`DROP INDEX "admin"."IDX_f732b6f82095a935db68c9491f"`) - await db.query(`DROP TABLE "admin"."nft_featuring_request"`) - await db.query(`DROP INDEX "admin"."IDX_519be2a41216c278c35f254dcb"`) - await db.query(`DROP INDEX "admin"."IDX_76d87e26cce72ac2e7ffa28dfb"`) - await db.query(`DROP TABLE "admin"."user"`) - await db.query(`DROP TABLE "storage_bucket"`) - await db.query(`DROP TABLE "storage_bucket_bag"`) - await db.query(`DROP INDEX "public"."IDX_aaf00b2c7d0cba49f97da14fbb"`) - await db.query(`DROP INDEX "public"."IDX_4c475f6c9300284b095859eec3"`) - await db.query(`DROP TABLE "distribution_bucket_family"`) - await db.query(`DROP TABLE "distribution_bucket_operator"`) - await db.query(`DROP INDEX "public"."IDX_678dc5427cdde0cd4fef2c07a4"`) - await db.query(`DROP TABLE "distribution_bucket"`) - await db.query(`DROP INDEX "public"."IDX_8cb7454d1ec34b0d3bb7ecdee4"`) - await db.query(`DROP TABLE "distribution_bucket_bag"`) - await db.query(`DROP INDEX "public"."IDX_a9810100aee7584680f197c8ff"`) - await db.query(`DROP INDEX "public"."IDX_32e552d352848d64ab82d38e9a"`) - await db.query(`DROP TABLE "storage_bag"`) - await db.query(`DROP TABLE "admin"."storage_data_object"`) - await db.query(`DROP INDEX "admin"."IDX_ff8014300b8039dbaed764f51b"`) - await db.query(`DROP TABLE "admin"."banned_member"`) - await db.query(`DROP INDEX "admin"."IDX_ed36c6c26bf5410796c2fc21f7"`) - await db.query(`DROP INDEX "admin"."IDX_f29ff095bdb945975deca021ad"`) - await db.query(`DROP TABLE "app"`) - await db.query(`DROP INDEX "public"."IDX_f36adbb7b096ceeb6f3e80ad14"`) - await db.query(`DROP INDEX "public"."IDX_c9cc395bbc485f70a15be64553"`) - await db.query(`DROP TABLE "admin"."channel"`) - await db.query(`DROP INDEX "admin"."IDX_a4752a0a0899dedc4d18077dd0"`) - await db.query(`DROP INDEX "admin"."IDX_25c85bc448b5e236a4c1a5f789"`) - await db.query(`DROP INDEX "admin"."IDX_a77e12f3d8c6ced020e179a5e9"`) - await db.query(`DROP INDEX "admin"."IDX_6997e94413b3f2f25a84e4a96f"`) - await db.query(`DROP INDEX "admin"."IDX_e58a2e1d78b8eccf40531a7fdb"`) - await db.query(`DROP INDEX "admin"."IDX_118ecfa0199aeb5a014906933e"`) - await db.query(`DROP TABLE "admin"."video_featured_in_category"`) - await db.query(`DROP INDEX "admin"."IDX_7b16ddad43901921a8d3c8eab7"`) - await db.query(`DROP INDEX "admin"."IDX_6d0917e1ac0cc06c8075bcf256"`) - await db.query(`DROP TABLE "admin"."video_category"`) - await db.query(`DROP INDEX "admin"."IDX_cbe7e5d162a819e4ee2e2f6105"`) - await db.query(`DROP INDEX "admin"."IDX_da26b34f037c0d59d3c0d0646e"`) - await db.query(`DROP TABLE "admin"."license"`) - await db.query(`DROP TABLE "admin"."video_subtitle"`) - await db.query(`DROP INDEX "admin"."IDX_2203674f18d8052ed6bac39625"`) - await db.query(`DROP INDEX "admin"."IDX_ffa63c28188eecc32af921bfc3"`) - await db.query(`DROP INDEX "admin"."IDX_b6eabfb8de4128b28d73681020"`) - await db.query(`DROP TABLE "admin"."comment_reaction"`) - await db.query(`DROP INDEX "admin"."IDX_15080d9fb7cf8b563103dd9d90"`) - await db.query(`DROP INDEX "admin"."IDX_962582f04d3f639e33f43c54bb"`) - await db.query(`DROP INDEX "admin"."IDX_d7995b1d57614a6fbd0c103874"`) - await db.query(`DROP TABLE "admin"."comment"`) - await db.query(`DROP INDEX "admin"."IDX_3ce66469b26697baa097f8da92"`) - await db.query(`DROP INDEX "admin"."IDX_1ff03403fd31dfeaba0623a89c"`) - await db.query(`DROP INDEX "admin"."IDX_c3c2abe750c76c7c8e305f71f2"`) - await db.query(`DROP INDEX "admin"."IDX_ac69bddf8202b7c0752d9dc8f3"`) - await db.query(`DROP TABLE "admin"."video_reaction"`) - await db.query(`DROP INDEX "admin"."IDX_73dda64f53bbc7ec7035d5e7f0"`) - await db.query(`DROP INDEX "admin"."IDX_436a3836eb47acb5e1e3c88dde"`) - await db.query(`DROP TABLE "admin"."video"`) - await db.query(`DROP INDEX "admin"."IDX_fe2b4b6aace15f1b6610830846"`) - await db.query(`DROP INDEX "admin"."IDX_81b11ef99a9db9ef1aed040d75"`) - await db.query(`DROP INDEX "admin"."IDX_2a5c61f32e9636ee10821e9a58"`) - await db.query(`DROP INDEX "admin"."IDX_8530d052cc79b420f7ce2b4e09"`) - await db.query(`DROP INDEX "admin"."IDX_57b335fa0a960877caf6d2fc29"`) - await db.query(`DROP INDEX "admin"."IDX_3ec633ae5d0477f512b4ed957d"`) - await db.query(`DROP INDEX "admin"."IDX_2db879ed42e3308fe65e679672"`) - await db.query(`DROP INDEX "admin"."IDX_54f88a7decf7d22fd9bd9fa439"`) - await db.query(`DROP INDEX "admin"."IDX_6c49ad08c44d36d11f77c426e4"`) - await db.query(`DROP INDEX "admin"."IDX_f33816960d690ac836f5d5c28a"`) - await db.query(`DROP TABLE "admin"."bid"`) - await db.query(`DROP INDEX "admin"."IDX_9e594e5a61c0f3cb25679f6ba8"`) - await db.query(`DROP INDEX "admin"."IDX_3caf2d6b31d2fe45a2b85b8191"`) - await db.query(`DROP INDEX "admin"."IDX_e7618559409a903a897164156b"`) - await db.query(`DROP INDEX "admin"."IDX_32cb73025ec49c87f4c594a265"`) - await db.query(`DROP TABLE "admin"."owned_nft"`) - await db.query(`DROP INDEX "admin"."IDX_8c7201ed7d4765dcbcc3609356"`) - await db.query(`DROP INDEX "admin"."IDX_466896e39b9ec953f4f2545622"`) - await db.query(`DROP TABLE "admin"."auction"`) - await db.query(`DROP INDEX "admin"."IDX_cfb47e97e60c9d1462576f85a8"`) - await db.query(`DROP INDEX "admin"."IDX_a3127ec87cccc5696b92cac4e0"`) - await db.query(`DROP INDEX "admin"."IDX_1673ad4b059742fbabfc40b275"`) - await db.query(`DROP TABLE "auction_whitelisted_member"`) - 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_0c5b879f9f2ca57a774f74f7f0"`) - await db.query(`DROP TABLE "admin"."event"`) - await db.query(`DROP INDEX "admin"."IDX_8f3f220c4e717207d841d4e6d4"`) - await db.query(`DROP INDEX "admin"."IDX_2c15918ff289396205521c5f3c"`) - await db.query(`DROP TABLE "notification"`) - await db.query(`DROP INDEX "public"."IDX_6bfa96ab97f1a09d73091294ef"`) - await db.query(`DROP INDEX "public"."IDX_122be1f0696e0255acf95f9e33"`) - await db.query(`DROP TABLE "admin"."account"`) - await db.query(`DROP INDEX "admin"."IDX_efef1e5fdbe318a379c06678c5"`) - await db.query(`DROP INDEX "admin"."IDX_4c8f96ccf523e9a3faefd5bdd4"`) - await db.query(`DROP INDEX "admin"."IDX_601b93655bcbe73cb58d8c80cd"`) - await db.query(`DROP INDEX "admin"."IDX_df4da05a7a80c1afd18b8f0990"`) - await db.query(`DROP TABLE "encryption_artifacts"`) - await db.query(`DROP INDEX "public"."IDX_ec8f68a544aadc4fbdadefe4a0"`) - await db.query(`DROP TABLE "admin"."session"`) - await db.query(`DROP INDEX "admin"."IDX_30e98e8746699fb9af235410af"`) - await db.query(`DROP INDEX "admin"."IDX_fae5a6b4a57f098e9af8520d49"`) - await db.query(`DROP INDEX "admin"."IDX_213b5a19bfdbe0ab6e06b1dede"`) - await db.query(`DROP TABLE "session_encryption_artifacts"`) - await db.query(`DROP INDEX "public"."IDX_3612880efd8926a17eba5ab0e1"`) - await db.query(`DROP TABLE "admin"."token"`) - await db.query(`DROP INDEX "admin"."IDX_a6fe18c105f85a63d761ccb078"`) - await db.query(`DROP TABLE "admin"."nft_history_entry"`) - await db.query(`DROP INDEX "admin"."IDX_57f51d35ecab042478fe2e31c1"`) - await db.query(`DROP INDEX "admin"."IDX_d1a28b178f5d028d048d40ce20"`) - await db.query(`DROP TABLE "admin"."nft_activity"`) - await db.query(`DROP INDEX "admin"."IDX_18a65713a9fd0715c7a980f5d5"`) - await db.query(`DROP INDEX "admin"."IDX_94d325a753f2c08fdd416eb095"`) - await db.query(`DROP TABLE "admin"."email_delivery_attempt"`) - await db.query(`DROP INDEX "admin"."IDX_f985b9b362249af72cac0f52a3"`) - await db.query(`DROP TABLE "admin"."notification_email_delivery"`) - await db.query(`DROP INDEX "admin"."IDX_3b756627c3146db150d66d1292"`) - await db.query(`DROP TABLE "admin"."video_hero"`) - await db.query(`DROP INDEX "admin"."IDX_9feac5d9713a9f07e32eb8ba7a"`) - await db.query(`DROP TABLE "admin"."video_media_encoding"`) - await db.query(`DROP TABLE "admin"."video_media_metadata"`) - await db.query(`DROP INDEX "admin"."IDX_5944dc5896cb16bd395414a0ce"`) - await db.query(`DROP INDEX "admin"."IDX_4dc101240e8e1536b770aee202"`) - await db.query(`DROP TABLE "admin"."gateway_config"`) - await db.query(`DROP TABLE "admin"."exclusion"`) - await db.query(`DROP INDEX "admin"."IDX_efba55b85909822c2b7655dfb8"`) - await db.query(`DROP INDEX "admin"."IDX_2729041b2f528a6c5833fdb3e5"`) - await db.query(`DROP TABLE "storage_bucket_operator_metadata"`) - await db.query(`DROP INDEX "public"."IDX_7beffc9530b3f307bc1169cb52"`) - await db.query(`DROP TABLE "distribution_bucket_family_metadata"`) - await db.query(`DROP INDEX "public"."IDX_dd93ca0ea24f3e7a02f11c4c14"`) - await db.query(`DROP INDEX "public"."IDX_5510d3b244a63d6ec702faa426"`) - await db.query(`DROP TABLE "distribution_bucket_operator_metadata"`) - await db.query(`DROP INDEX "public"."IDX_69ec9bdc975b95f7dff94a7106"`) - await db.query(`DROP TABLE "admin"."channel_verification"`) - await db.query(`DROP INDEX "admin"."IDX_f13d5d785670f46de668575139"`) - await db.query(`DROP TABLE "admin"."channel_suspension"`) - await db.query(`DROP INDEX "admin"."IDX_e30ebff1042c010ff88b87f4f7"`) - await db.query(`DROP TABLE "curator_group"`) - await db.query(`DROP TABLE "curator"`) - await db.query(`DROP TABLE "member_metadata"`) - await db.query(`DROP INDEX "public"."IDX_e7e4d350f82ae2383894f465ed"`) - await db.query(`DROP TABLE "next_entity_id"`) - await db.query(`ALTER TABLE "admin"."channel_follow" DROP CONSTRAINT "FK_822778b4b1ea8e3b60b127cb8b1"`) - await db.query(`ALTER TABLE "admin"."video_view_event" DROP CONSTRAINT "FK_31e1e798ec387ad905cf98d33b0"`) - await db.query(`ALTER TABLE "admin"."report" DROP CONSTRAINT "FK_c6686efa4cd49fa9a429f01bac8"`) - await db.query(`ALTER TABLE "admin"."nft_featuring_request" DROP CONSTRAINT "FK_519be2a41216c278c35f254dcba"`) - await db.query(`ALTER TABLE "storage_bucket_bag" DROP CONSTRAINT "FK_791e2f82e3919ffcef8712aa1b9"`) - await db.query(`ALTER TABLE "storage_bucket_bag" DROP CONSTRAINT "FK_aaf00b2c7d0cba49f97da14fbba"`) - await db.query(`ALTER TABLE "distribution_bucket_operator" DROP CONSTRAINT "FK_678dc5427cdde0cd4fef2c07a43"`) - await db.query(`ALTER TABLE "distribution_bucket" DROP CONSTRAINT "FK_8cb7454d1ec34b0d3bb7ecdee4e"`) - await db.query(`ALTER TABLE "distribution_bucket_bag" DROP CONSTRAINT "FK_8a807921f1aae60d4ba94895826"`) - await db.query(`ALTER TABLE "distribution_bucket_bag" DROP CONSTRAINT "FK_a9810100aee7584680f197c8ff0"`) - await db.query(`ALTER TABLE "admin"."storage_data_object" DROP CONSTRAINT "FK_ff8014300b8039dbaed764f51bc"`) - await db.query(`ALTER TABLE "admin"."banned_member" DROP CONSTRAINT "FK_b94ea874da235d9b6fbc35cf58e"`) - await db.query(`ALTER TABLE "admin"."banned_member" DROP CONSTRAINT "FK_ed36c6c26bf5410796c2fc21f74"`) - await db.query(`ALTER TABLE "app" DROP CONSTRAINT "FK_c9cc395bbc485f70a15be64553e"`) - await db.query(`ALTER TABLE "admin"."channel" DROP CONSTRAINT "FK_25c85bc448b5e236a4c1a5f7895"`) - await db.query(`ALTER TABLE "admin"."channel" DROP CONSTRAINT "FK_a77e12f3d8c6ced020e179a5e94"`) - await db.query(`ALTER TABLE "admin"."channel" DROP CONSTRAINT "FK_6997e94413b3f2f25a84e4a96f8"`) - await db.query(`ALTER TABLE "admin"."channel" DROP CONSTRAINT "FK_118ecfa0199aeb5a014906933e8"`) - await db.query(`ALTER TABLE "admin"."video_featured_in_category" DROP CONSTRAINT "FK_7b16ddad43901921a8d3c8eab71"`) - await db.query(`ALTER TABLE "admin"."video_featured_in_category" DROP CONSTRAINT "FK_0e6bb49ce9d022cd872f3ab4288"`) - await db.query(`ALTER TABLE "admin"."video_category" DROP CONSTRAINT "FK_da26b34f037c0d59d3c0d0646e9"`) - await db.query(`ALTER TABLE "admin"."video_subtitle" DROP CONSTRAINT "FK_2203674f18d8052ed6bac396252"`) - await db.query(`ALTER TABLE "admin"."video_subtitle" DROP CONSTRAINT "FK_b6eabfb8de4128b28d73681020f"`) - await db.query(`ALTER TABLE "admin"."comment_reaction" DROP CONSTRAINT "FK_15080d9fb7cf8b563103dd9d900"`) - await db.query(`ALTER TABLE "admin"."comment_reaction" DROP CONSTRAINT "FK_962582f04d3f639e33f43c54bbc"`) - await db.query(`ALTER TABLE "admin"."comment_reaction" DROP CONSTRAINT "FK_d7995b1d57614a6fbd0c103874d"`) - await db.query(`ALTER TABLE "admin"."comment" DROP CONSTRAINT "FK_3ce66469b26697baa097f8da923"`) - await db.query(`ALTER TABLE "admin"."comment" DROP CONSTRAINT "FK_1ff03403fd31dfeaba0623a89cf"`) - await db.query(`ALTER TABLE "admin"."comment" DROP CONSTRAINT "FK_ac69bddf8202b7c0752d9dc8f32"`) - await db.query(`ALTER TABLE "admin"."video_reaction" DROP CONSTRAINT "FK_73dda64f53bbc7ec7035d5e7f09"`) - await db.query(`ALTER TABLE "admin"."video_reaction" DROP CONSTRAINT "FK_436a3836eb47acb5e1e3c88ddea"`) - await db.query(`ALTER TABLE "admin"."video" DROP CONSTRAINT "FK_81b11ef99a9db9ef1aed040d750"`) - await db.query(`ALTER TABLE "admin"."video" DROP CONSTRAINT "FK_2a5c61f32e9636ee10821e9a58d"`) - await db.query(`ALTER TABLE "admin"."video" DROP CONSTRAINT "FK_8530d052cc79b420f7ce2b4e09d"`) - await db.query(`ALTER TABLE "admin"."video" DROP CONSTRAINT "FK_3ec633ae5d0477f512b4ed957d6"`) - await db.query(`ALTER TABLE "admin"."video" DROP CONSTRAINT "FK_2db879ed42e3308fe65e6796729"`) - await db.query(`ALTER TABLE "admin"."video" DROP CONSTRAINT "FK_54f88a7decf7d22fd9bd9fa439a"`) - await db.query(`ALTER TABLE "admin"."video" DROP CONSTRAINT "FK_6c49ad08c44d36d11f77c426e43"`) - await db.query(`ALTER TABLE "admin"."bid" DROP CONSTRAINT "FK_9e594e5a61c0f3cb25679f6ba8d"`) - await db.query(`ALTER TABLE "admin"."bid" DROP CONSTRAINT "FK_3caf2d6b31d2fe45a2b85b81912"`) - await db.query(`ALTER TABLE "admin"."bid" DROP CONSTRAINT "FK_e7618559409a903a897164156b7"`) - await db.query(`ALTER TABLE "admin"."bid" DROP CONSTRAINT "FK_32cb73025ec49c87f4c594a265f"`) - await db.query(`ALTER TABLE "admin"."owned_nft" DROP CONSTRAINT "FK_466896e39b9ec953f4f2545622d"`) - await db.query(`ALTER TABLE "admin"."auction" DROP CONSTRAINT "FK_cfb47e97e60c9d1462576f85a88"`) - await db.query(`ALTER TABLE "admin"."auction" DROP CONSTRAINT "FK_a3127ec87cccc5696b92cac4e09"`) - await db.query(`ALTER TABLE "admin"."auction" DROP CONSTRAINT "FK_1673ad4b059742fbabfc40b275c"`) - await db.query(`ALTER TABLE "auction_whitelisted_member" DROP CONSTRAINT "FK_aad797677bc7c7c7dc1f1d397f5"`) - await db.query(`ALTER TABLE "auction_whitelisted_member" DROP CONSTRAINT "FK_d5ae4854487c7658b64225be305"`) - await db.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_6bfa96ab97f1a09d73091294efc"`) - await db.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_122be1f0696e0255acf95f9e336"`) - await db.query(`ALTER TABLE "admin"."account" DROP CONSTRAINT "FK_efef1e5fdbe318a379c06678c51"`) - await db.query(`ALTER TABLE "admin"."account" DROP CONSTRAINT "FK_601b93655bcbe73cb58d8c80cd3"`) - await db.query(`ALTER TABLE "encryption_artifacts" DROP CONSTRAINT "FK_ec8f68a544aadc4fbdadefe4a0a"`) - await db.query(`ALTER TABLE "admin"."session" DROP CONSTRAINT "FK_30e98e8746699fb9af235410aff"`) - await db.query(`ALTER TABLE "admin"."session" DROP CONSTRAINT "FK_fae5a6b4a57f098e9af8520d499"`) - await db.query(`ALTER TABLE "session_encryption_artifacts" DROP CONSTRAINT "FK_3612880efd8926a17eba5ab0e1a"`) - await db.query(`ALTER TABLE "admin"."token" DROP CONSTRAINT "FK_a6fe18c105f85a63d761ccb0780"`) - await db.query(`ALTER TABLE "admin"."nft_history_entry" DROP CONSTRAINT "FK_57f51d35ecab042478fe2e31c19"`) - await db.query(`ALTER TABLE "admin"."nft_history_entry" DROP CONSTRAINT "FK_d1a28b178f5d028d048d40ce208"`) - await db.query(`ALTER TABLE "admin"."nft_activity" DROP CONSTRAINT "FK_18a65713a9fd0715c7a980f5d54"`) - await db.query(`ALTER TABLE "admin"."nft_activity" DROP CONSTRAINT "FK_94d325a753f2c08fdd416eb095f"`) - await db.query(`ALTER TABLE "admin"."email_delivery_attempt" DROP CONSTRAINT "FK_f985b9b362249af72cac0f52a3b"`) - await db.query(`ALTER TABLE "admin"."notification_email_delivery" DROP CONSTRAINT "FK_3b756627c3146db150d66d12929"`) - await db.query(`ALTER TABLE "admin"."video_hero" DROP CONSTRAINT "FK_9feac5d9713a9f07e32eb8ba7a1"`) - await db.query(`ALTER TABLE "admin"."video_media_metadata" DROP CONSTRAINT "FK_5944dc5896cb16bd395414a0ce0"`) - await db.query(`ALTER TABLE "admin"."video_media_metadata" DROP CONSTRAINT "FK_4dc101240e8e1536b770aee202a"`) - await db.query(`ALTER TABLE "storage_bucket_operator_metadata" DROP CONSTRAINT "FK_7beffc9530b3f307bc1169cb524"`) - await db.query(`ALTER TABLE "distribution_bucket_family_metadata" DROP CONSTRAINT "FK_dd93ca0ea24f3e7a02f11c4c149"`) - await db.query(`ALTER TABLE "distribution_bucket_operator_metadata" DROP CONSTRAINT "FK_69ec9bdc975b95f7dff94a71069"`) - await db.query(`ALTER TABLE "admin"."channel_verification" DROP CONSTRAINT "FK_f13d5d785670f46de668575139c"`) - await db.query(`ALTER TABLE "admin"."channel_suspension" DROP CONSTRAINT "FK_e30ebff1042c010ff88b87f4f7a"`) - await db.query(`ALTER TABLE "member_metadata" DROP CONSTRAINT "FK_e7e4d350f82ae2383894f465ede"`) - } -} diff --git a/db/migrations/1708500753099-Data.js b/db/migrations/1708500753099-Data.js deleted file mode 100644 index dd199cfe4..000000000 --- a/db/migrations/1708500753099-Data.js +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = class Data1708500753099 { - name = 'Data1708500753099' - - async up(db) { - await db.query(`ALTER TABLE "admin"."video" ADD "is_short" boolean`) - } - - async down(db) { - await db.query(`ALTER TABLE "admin"."video" DROP COLUMN "is_short"`) - } -} diff --git a/db/migrations/1708791753999-Data.js b/db/migrations/1708791753999-Data.js deleted file mode 100644 index 470832d25..000000000 --- a/db/migrations/1708791753999-Data.js +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = class Data1708791753999 { - name = 'Data1708791753999' - - async up(db) { - await db.query(`ALTER TABLE "admin"."video" ADD "include_in_home_feed" boolean`) - } - - async down(db) { - await db.query(`ALTER TABLE "admin"."video" DROP COLUMN "include_in_home_feed"`) - } -} diff --git a/db/migrations/1709622091352-Data.js b/db/migrations/1709622091352-Data.js deleted file mode 100644 index b850e96e0..000000000 --- a/db/migrations/1709622091352-Data.js +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = class Data1709622091352 { - name = 'Data1709622091352' - - async up(db) { - await db.query(`ALTER TABLE "admin"."video" ADD "is_short_derived" boolean`) - } - - async down(db) { - await db.query(`ALTER TABLE "admin"."video" DROP COLUMN "is_short_derived"`) - } -} diff --git a/db/migrations/1709641962382-Data.js b/db/migrations/1709641962382-Data.js deleted file mode 100644 index 034e28180..000000000 --- a/db/migrations/1709641962382-Data.js +++ /dev/null @@ -1,149 +0,0 @@ -module.exports = class Data1709641962382 { - name = 'Data1709641962382' - - async up(db) { - await db.query(`CREATE TABLE "sale_transaction" ("id" character varying NOT NULL, "quantity" numeric NOT NULL, "sale_id" character varying, "account_id" character varying, "created_in" integer NOT NULL, CONSTRAINT "PK_06470e015a427563408e7e3661e" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_cfd7aa41d364144e6bbf677a48" ON "sale_transaction" ("account_id") `) - await db.query(`CREATE INDEX "IDX_b538792bd801ca0a1c77c03eff" ON "sale_transaction" ("sale_id", "account_id") `) - await db.query(`CREATE TABLE "sale" ("id" character varying NOT NULL, "token_id" character varying, "price_per_unit" numeric NOT NULL, "token_sale_allocation" numeric NOT NULL, "tokens_sold" numeric NOT NULL, "created_in" integer NOT NULL, "start_block" integer NOT NULL, "ends_at" integer NOT NULL, "terms_and_conditions" text NOT NULL, "max_amount_per_member" numeric, "finalized" boolean NOT NULL, "funds_source_account_id" character varying, CONSTRAINT "Sale_token_createdIn" UNIQUE ("token_id", "created_in") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "PK_d03891c457cbcd22974732b5de2" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_00468ff9c85265853384de0e1d" ON "sale" ("funds_source_account_id") `) - await db.query(`CREATE INDEX "IDX_5c5d611ec29439dc91eeea287b" ON "sale" ("token_id", "created_in") `) - await db.query(`CREATE TABLE "revenue_share_participation" ("id" character varying NOT NULL, "account_id" character varying, "revenue_share_id" character varying, "staked_amount" numeric NOT NULL, "earnings" numeric NOT NULL, "created_in" integer NOT NULL, "recovered" boolean NOT NULL, CONSTRAINT "RevenueShareParticipation_account_revenueShare" UNIQUE ("account_id", "revenue_share_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "PK_a6931d06b217f8055611ea26fc7" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_018b86600c0c1228ada1c928be" ON "revenue_share_participation" ("revenue_share_id") `) - await db.query(`CREATE INDEX "IDX_9ca49e1effab0c3543ae0839cf" ON "revenue_share_participation" ("account_id", "revenue_share_id") `) - await db.query(`CREATE TABLE "revenue_share" ("id" character varying NOT NULL, "token_id" character varying, "created_in" integer NOT NULL, "starting_at" integer NOT NULL, "ends_at" integer NOT NULL, "potential_participants_num" integer, "participants_num" integer NOT NULL, "allocation" numeric NOT NULL, "claimed" numeric NOT NULL, "finalized" boolean NOT NULL, CONSTRAINT "PK_6ef7c4be56b9290db1462885163" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_4e8bfc2037cececc86ba5192ea" ON "revenue_share" ("token_id") `) - await db.query(`CREATE TABLE "amm_transaction" ("id" character varying NOT NULL, "quantity" numeric NOT NULL, "price_paid" numeric NOT NULL, "amm_id" character varying, "account_id" character varying, "price_per_unit" numeric NOT NULL, "transaction_type" character varying(4) NOT NULL, "created_in" integer NOT NULL, CONSTRAINT "PK_783093757a6f260c72ded36d409" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_135d9555d7ea3e45dd78d8aede" ON "amm_transaction" ("amm_id") `) - await db.query(`CREATE INDEX "IDX_9109e9ce696736e0dd51d90fa7" ON "amm_transaction" ("account_id", "amm_id") `) - await db.query(`CREATE TABLE "amm_curve" ("id" character varying NOT NULL, "token_id" character varying, "burned_by_amm" numeric NOT NULL, "minted_by_amm" numeric NOT NULL, "amm_slope_parameter" numeric NOT NULL, "amm_init_price" numeric NOT NULL, "finalized" boolean NOT NULL, CONSTRAINT "PK_477b83cf84964aa40f38edf1db1" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_97bee00638822978784362d19f" ON "amm_curve" ("token_id") `) - await db.query(`CREATE TABLE "trailer_video" ("id" character varying NOT NULL, "video_id" character varying, "token_id" character varying NOT NULL, CONSTRAINT "TrailerVideo_token" UNIQUE ("token_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "TrailerVideo_token_video" UNIQUE ("token_id", "video_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "REL_0151a0342b10afcd1933f10656" UNIQUE ("token_id"), CONSTRAINT "PK_06ed751f0ca8164994ff327cacc" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_c73677538ef22a243568edac74" ON "trailer_video" ("video_id") `) - await db.query(`CREATE INDEX "IDX_0151a0342b10afcd1933f10656" ON "trailer_video" ("token_id") `) - await db.query(`CREATE INDEX "IDX_7eb550061f81d70d7c14b9368a" ON "trailer_video" ("token_id", "video_id") `) - await db.query(`CREATE TABLE "benefit" ("id" character varying NOT NULL, "token_id" character varying, "emoji_code" text, "title" text NOT NULL, "description" text NOT NULL, "display_order" integer NOT NULL, CONSTRAINT "Benefit_token_displayOrder" UNIQUE ("token_id", "display_order") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "PK_c024dccb30e6f4702adffe884d1" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_77ac3c1669ee14648626b078f9" ON "benefit" ("token_id", "display_order") `) - await db.query(`CREATE TABLE "creator_token" ("id" character varying NOT NULL, "status" character varying(6) NOT NULL, "avatar" jsonb, "total_supply" numeric NOT NULL, "is_featured" boolean NOT NULL, "symbol" text, "is_invite_only" boolean NOT NULL, "annual_creator_reward_permill" integer NOT NULL, "revenue_share_ratio_permill" integer NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, "description" text, "whitelist_applicant_note" text, "whitelist_applicant_link" text, "accounts_num" integer NOT NULL, "number_of_revenue_share_activations" integer NOT NULL, "deissued" boolean NOT NULL, "current_amm_sale_id" character varying, "current_sale_id" character varying, "current_revenue_share_id" character varying, "number_of_vested_transfer_issued" integer NOT NULL, "last_price" numeric, CONSTRAINT "PK_abbc66d13ff7d3828e4c830d325" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_790a6fc1f7aad3711c0672bb6b" ON "creator_token" ("symbol") `) - await db.query(`CREATE INDEX "IDX_64480ef90bda6c11650c3f4279" ON "creator_token" ("created_at") `) - await db.query(`CREATE INDEX "IDX_aabe40376c0eb47772b52780b1" ON "creator_token" ("current_amm_sale_id") `) - await db.query(`CREATE INDEX "IDX_5eca884f8728ff8f0c6a389c24" ON "creator_token" ("current_sale_id") `) - await db.query(`CREATE INDEX "IDX_df8c309ef364e49b9d2f17dc77" ON "creator_token" ("current_revenue_share_id") `) - await db.query(`CREATE TABLE "vesting_schedule" ("id" character varying NOT NULL, "cliff_ratio_permill" integer NOT NULL, "vesting_duration_blocks" integer NOT NULL, "cliff_duration_blocks" integer NOT NULL, "ends_at" integer NOT NULL, "cliff_block" integer NOT NULL, CONSTRAINT "PK_4818b05532ed9058110ed5b5b13" PRIMARY KEY ("id"))`) - await db.query(`CREATE TABLE "vested_account" ("id" character varying NOT NULL, "vesting_id" character varying, "account_id" character varying, "total_vesting_amount" numeric NOT NULL, "vesting_source" jsonb NOT NULL, "acquired_at" integer NOT NULL, CONSTRAINT "PK_23d64323d1b1b14ccbdb6ed2a64" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_745ee4e6a2dfd5de65fb8b9f44" ON "vested_account" ("vesting_id") `) - await db.query(`CREATE INDEX "IDX_6a0600f53023dca2c43b99a097" ON "vested_account" ("account_id") `) - await db.query(`CREATE TABLE "token_account" ("id" character varying NOT NULL, "member_id" character varying, "token_id" character varying, "staked_amount" numeric NOT NULL, "total_amount" numeric NOT NULL, "deleted" boolean NOT NULL, CONSTRAINT "PK_6121d7a5eafbe71fba146a98fd3" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_dc32b6b2efa86183e6329909e7" ON "token_account" ("member_id") `) - await db.query(`CREATE INDEX "IDX_b44e36e5b6093947ec28580a84" ON "token_account" ("token_id", "member_id") `) - await db.query(`CREATE TABLE "token_channel" ("id" character varying NOT NULL, "token_id" character varying NOT NULL, "channel_id" character varying NOT NULL, CONSTRAINT "TokenChannel_channel" UNIQUE ("channel_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "TokenChannel_token" UNIQUE ("token_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "TokenChannel_token_channel" UNIQUE ("token_id", "channel_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "REL_7105aa65a2d333bb2f66db129e" UNIQUE ("token_id"), CONSTRAINT "REL_b065bc433d65b0a6874073ea54" UNIQUE ("channel_id"), CONSTRAINT "PK_e5cd0127f70ee171db28af0293c" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_7105aa65a2d333bb2f66db129e" ON "token_channel" ("token_id") `) - await db.query(`CREATE INDEX "IDX_b065bc433d65b0a6874073ea54" ON "token_channel" ("channel_id") `) - await db.query(`CREATE INDEX "IDX_f13351e59524e009f99612af11" ON "token_channel" ("token_id", "channel_id") `) - await db.query(`CREATE TABLE "vested_sale" ("id" character varying NOT NULL, "sale_id" character varying NOT NULL, "vesting_id" character varying NOT NULL, CONSTRAINT "VestedSale_vesting" UNIQUE ("vesting_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "VestedSale_sale" UNIQUE ("sale_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "VestedSale_sale_vesting" UNIQUE ("sale_id", "vesting_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "REL_4b0d0d4f6a5ce72247ffe22324" UNIQUE ("sale_id"), CONSTRAINT "REL_ffa4428b95fc1c0e4df5b5f495" UNIQUE ("vesting_id"), CONSTRAINT "PK_223c9942cef9ded13304deb2488" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_4b0d0d4f6a5ce72247ffe22324" ON "vested_sale" ("sale_id") `) - await db.query(`CREATE INDEX "IDX_ffa4428b95fc1c0e4df5b5f495" ON "vested_sale" ("vesting_id") `) - await db.query(`CREATE INDEX "IDX_b2135c373c44a37e4e6842ead5" ON "vested_sale" ("sale_id", "vesting_id") `) - await db.query(`ALTER TABLE "admin"."channel" ADD "revenue_share_ratio_percent" integer`) - await db.query(`ALTER TABLE "admin"."channel" ADD "cumulative_revenue" numeric NOT NULL`) - await db.query(`ALTER TABLE "notification" ADD "dispatch_block" integer`) - await db.query(`ALTER TABLE "sale_transaction" ADD CONSTRAINT "FK_7c477ad14796b65a8e47214adc9" FOREIGN KEY ("sale_id") REFERENCES "sale"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "sale_transaction" ADD CONSTRAINT "FK_cfd7aa41d364144e6bbf677a488" FOREIGN KEY ("account_id") REFERENCES "token_account"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "sale" ADD CONSTRAINT "FK_53aae73a92bcdefd80d4bb94e7f" FOREIGN KEY ("token_id") REFERENCES "creator_token"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "sale" ADD CONSTRAINT "FK_00468ff9c85265853384de0e1dd" FOREIGN KEY ("funds_source_account_id") REFERENCES "token_account"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "revenue_share_participation" ADD CONSTRAINT "FK_7549dc863632b065f111a532551" FOREIGN KEY ("account_id") REFERENCES "token_account"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "revenue_share_participation" ADD CONSTRAINT "FK_018b86600c0c1228ada1c928be7" FOREIGN KEY ("revenue_share_id") REFERENCES "revenue_share"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "revenue_share" ADD CONSTRAINT "FK_4e8bfc2037cececc86ba5192ea9" FOREIGN KEY ("token_id") REFERENCES "creator_token"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "amm_transaction" ADD CONSTRAINT "FK_135d9555d7ea3e45dd78d8aedec" FOREIGN KEY ("amm_id") REFERENCES "amm_curve"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "amm_transaction" ADD CONSTRAINT "FK_51f006dbc040d62dc479adbee78" FOREIGN KEY ("account_id") REFERENCES "token_account"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "amm_curve" ADD CONSTRAINT "FK_97bee00638822978784362d19fc" FOREIGN KEY ("token_id") REFERENCES "creator_token"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "trailer_video" ADD CONSTRAINT "FK_c73677538ef22a243568edac74b" FOREIGN KEY ("video_id") REFERENCES "admin"."video"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "trailer_video" ADD CONSTRAINT "FK_0151a0342b10afcd1933f106564" FOREIGN KEY ("token_id") REFERENCES "creator_token"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "benefit" ADD CONSTRAINT "FK_b484e2182fc7a1910e84a5ae7ad" FOREIGN KEY ("token_id") REFERENCES "creator_token"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "creator_token" ADD CONSTRAINT "FK_aabe40376c0eb47772b52780b19" FOREIGN KEY ("current_amm_sale_id") REFERENCES "amm_curve"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "creator_token" ADD CONSTRAINT "FK_5eca884f8728ff8f0c6a389c24b" FOREIGN KEY ("current_sale_id") REFERENCES "sale"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "creator_token" ADD CONSTRAINT "FK_df8c309ef364e49b9d2f17dc778" FOREIGN KEY ("current_revenue_share_id") REFERENCES "revenue_share"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "vested_account" ADD CONSTRAINT "FK_745ee4e6a2dfd5de65fb8b9f44a" FOREIGN KEY ("vesting_id") REFERENCES "vesting_schedule"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "vested_account" ADD CONSTRAINT "FK_6a0600f53023dca2c43b99a0974" FOREIGN KEY ("account_id") REFERENCES "token_account"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "token_account" ADD CONSTRAINT "FK_dc32b6b2efa86183e6329909e73" FOREIGN KEY ("member_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "token_account" ADD CONSTRAINT "FK_02862fa18dececb99dd81a6a6a9" FOREIGN KEY ("token_id") REFERENCES "creator_token"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "token_channel" ADD CONSTRAINT "FK_7105aa65a2d333bb2f66db129e9" FOREIGN KEY ("token_id") REFERENCES "creator_token"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "token_channel" ADD CONSTRAINT "FK_b065bc433d65b0a6874073ea540" FOREIGN KEY ("channel_id") REFERENCES "admin"."channel"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "vested_sale" ADD CONSTRAINT "FK_4b0d0d4f6a5ce72247ffe223240" FOREIGN KEY ("sale_id") REFERENCES "sale"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - await db.query(`ALTER TABLE "vested_sale" ADD CONSTRAINT "FK_ffa4428b95fc1c0e4df5b5f4952" FOREIGN KEY ("vesting_id") REFERENCES "vesting_schedule"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) - } - - async down(db) { - await db.query(`DROP TABLE "sale_transaction"`) - await db.query(`DROP INDEX "public"."IDX_cfd7aa41d364144e6bbf677a48"`) - await db.query(`DROP INDEX "public"."IDX_b538792bd801ca0a1c77c03eff"`) - await db.query(`DROP TABLE "sale"`) - await db.query(`DROP INDEX "public"."IDX_00468ff9c85265853384de0e1d"`) - await db.query(`DROP INDEX "public"."IDX_5c5d611ec29439dc91eeea287b"`) - await db.query(`DROP TABLE "revenue_share_participation"`) - await db.query(`DROP INDEX "public"."IDX_018b86600c0c1228ada1c928be"`) - await db.query(`DROP INDEX "public"."IDX_9ca49e1effab0c3543ae0839cf"`) - await db.query(`DROP TABLE "revenue_share"`) - await db.query(`DROP INDEX "public"."IDX_4e8bfc2037cececc86ba5192ea"`) - await db.query(`DROP TABLE "amm_transaction"`) - await db.query(`DROP INDEX "public"."IDX_135d9555d7ea3e45dd78d8aede"`) - await db.query(`DROP INDEX "public"."IDX_9109e9ce696736e0dd51d90fa7"`) - await db.query(`DROP TABLE "amm_curve"`) - await db.query(`DROP INDEX "public"."IDX_97bee00638822978784362d19f"`) - await db.query(`DROP TABLE "trailer_video"`) - await db.query(`DROP INDEX "public"."IDX_c73677538ef22a243568edac74"`) - await db.query(`DROP INDEX "public"."IDX_0151a0342b10afcd1933f10656"`) - await db.query(`DROP INDEX "public"."IDX_7eb550061f81d70d7c14b9368a"`) - await db.query(`DROP TABLE "benefit"`) - await db.query(`DROP INDEX "public"."IDX_77ac3c1669ee14648626b078f9"`) - await db.query(`DROP TABLE "creator_token"`) - await db.query(`DROP INDEX "public"."IDX_790a6fc1f7aad3711c0672bb6b"`) - await db.query(`DROP INDEX "public"."IDX_64480ef90bda6c11650c3f4279"`) - await db.query(`DROP INDEX "public"."IDX_aabe40376c0eb47772b52780b1"`) - await db.query(`DROP INDEX "public"."IDX_5eca884f8728ff8f0c6a389c24"`) - await db.query(`DROP INDEX "public"."IDX_df8c309ef364e49b9d2f17dc77"`) - await db.query(`DROP TABLE "vesting_schedule"`) - await db.query(`DROP TABLE "vested_account"`) - await db.query(`DROP INDEX "public"."IDX_745ee4e6a2dfd5de65fb8b9f44"`) - await db.query(`DROP INDEX "public"."IDX_6a0600f53023dca2c43b99a097"`) - await db.query(`DROP TABLE "token_account"`) - await db.query(`DROP INDEX "public"."IDX_dc32b6b2efa86183e6329909e7"`) - await db.query(`DROP INDEX "public"."IDX_b44e36e5b6093947ec28580a84"`) - await db.query(`DROP TABLE "token_channel"`) - await db.query(`DROP INDEX "public"."IDX_7105aa65a2d333bb2f66db129e"`) - await db.query(`DROP INDEX "public"."IDX_b065bc433d65b0a6874073ea54"`) - await db.query(`DROP INDEX "public"."IDX_f13351e59524e009f99612af11"`) - await db.query(`DROP TABLE "vested_sale"`) - await db.query(`DROP INDEX "public"."IDX_4b0d0d4f6a5ce72247ffe22324"`) - await db.query(`DROP INDEX "public"."IDX_ffa4428b95fc1c0e4df5b5f495"`) - await db.query(`DROP INDEX "public"."IDX_b2135c373c44a37e4e6842ead5"`) - await db.query(`ALTER TABLE "admin"."channel" DROP COLUMN "revenue_share_ratio_percent"`) - await db.query(`ALTER TABLE "admin"."channel" DROP COLUMN "cumulative_revenue"`) - await db.query(`ALTER TABLE "notification" DROP COLUMN "dispatch_block"`) - await db.query(`ALTER TABLE "sale_transaction" DROP CONSTRAINT "FK_7c477ad14796b65a8e47214adc9"`) - await db.query(`ALTER TABLE "sale_transaction" DROP CONSTRAINT "FK_cfd7aa41d364144e6bbf677a488"`) - await db.query(`ALTER TABLE "sale" DROP CONSTRAINT "FK_53aae73a92bcdefd80d4bb94e7f"`) - await db.query(`ALTER TABLE "sale" DROP CONSTRAINT "FK_00468ff9c85265853384de0e1dd"`) - await db.query(`ALTER TABLE "revenue_share_participation" DROP CONSTRAINT "FK_7549dc863632b065f111a532551"`) - await db.query(`ALTER TABLE "revenue_share_participation" DROP CONSTRAINT "FK_018b86600c0c1228ada1c928be7"`) - await db.query(`ALTER TABLE "revenue_share" DROP CONSTRAINT "FK_4e8bfc2037cececc86ba5192ea9"`) - await db.query(`ALTER TABLE "amm_transaction" DROP CONSTRAINT "FK_135d9555d7ea3e45dd78d8aedec"`) - await db.query(`ALTER TABLE "amm_transaction" DROP CONSTRAINT "FK_51f006dbc040d62dc479adbee78"`) - await db.query(`ALTER TABLE "amm_curve" DROP CONSTRAINT "FK_97bee00638822978784362d19fc"`) - await db.query(`ALTER TABLE "trailer_video" DROP CONSTRAINT "FK_c73677538ef22a243568edac74b"`) - await db.query(`ALTER TABLE "trailer_video" DROP CONSTRAINT "FK_0151a0342b10afcd1933f106564"`) - await db.query(`ALTER TABLE "benefit" DROP CONSTRAINT "FK_b484e2182fc7a1910e84a5ae7ad"`) - await db.query(`ALTER TABLE "creator_token" DROP CONSTRAINT "FK_aabe40376c0eb47772b52780b19"`) - await db.query(`ALTER TABLE "creator_token" DROP CONSTRAINT "FK_5eca884f8728ff8f0c6a389c24b"`) - await db.query(`ALTER TABLE "creator_token" DROP CONSTRAINT "FK_df8c309ef364e49b9d2f17dc778"`) - await db.query(`ALTER TABLE "vested_account" DROP CONSTRAINT "FK_745ee4e6a2dfd5de65fb8b9f44a"`) - await db.query(`ALTER TABLE "vested_account" DROP CONSTRAINT "FK_6a0600f53023dca2c43b99a0974"`) - await db.query(`ALTER TABLE "token_account" DROP CONSTRAINT "FK_dc32b6b2efa86183e6329909e73"`) - await db.query(`ALTER TABLE "token_account" DROP CONSTRAINT "FK_02862fa18dececb99dd81a6a6a9"`) - await db.query(`ALTER TABLE "token_channel" DROP CONSTRAINT "FK_7105aa65a2d333bb2f66db129e9"`) - await db.query(`ALTER TABLE "token_channel" DROP CONSTRAINT "FK_b065bc433d65b0a6874073ea540"`) - await db.query(`ALTER TABLE "vested_sale" DROP CONSTRAINT "FK_4b0d0d4f6a5ce72247ffe223240"`) - await db.query(`ALTER TABLE "vested_sale" DROP CONSTRAINT "FK_ffa4428b95fc1c0e4df5b5f4952"`) - } -} diff --git a/db/migrations/1720623003671-Data.js b/db/migrations/1720623003671-Data.js deleted file mode 100644 index 5a14f9337..000000000 --- a/db/migrations/1720623003671-Data.js +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = class Data1720623003671 { - name = 'Data1720623003671' - - async up(db) { - await db.query(`CREATE TABLE "admin"."orion_offchain_cursor" ("cursor_name" character varying NOT NULL, "value" bigint NOT NULL, CONSTRAINT "PK_7083797352af5a21224b6c8ccbc" PRIMARY KEY ("cursor_name"))`) - } - - async down(db) { - await db.query(`DROP TABLE "admin"."orion_offchain_cursor"`) - } -} diff --git a/db/migrations/1721141313646-Data.js b/db/migrations/1721141313646-Data.js deleted file mode 100644 index b7b8dcf76..000000000 --- a/db/migrations/1721141313646-Data.js +++ /dev/null @@ -1,19 +0,0 @@ -module.exports = class Data1721141313646 { - name = 'Data1721141313646' - - async up(db) { - await db.query(`CREATE TABLE "admin"."user_interaction_count" ("id" character varying NOT NULL, "type" text, "entity_id" text, "day_timestamp" TIMESTAMP WITH TIME ZONE NOT NULL, "count" integer NOT NULL, CONSTRAINT "PK_8e334a51febcf02c54dff48147d" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_b5261af5f3fe48d77086ebc602" ON "admin"."user_interaction_count" ("day_timestamp") `) - await db.query(`CREATE TABLE "admin"."marketplace_token" ("liquidity" integer, "market_cap" numeric, "cumulative_revenue" numeric, "amm_volume" numeric, "price_change" numeric, "liquidity_change" numeric, "id" character varying NOT NULL, "status" character varying(6) NOT NULL, "avatar" jsonb, "total_supply" numeric NOT NULL, "is_featured" boolean NOT NULL, "symbol" text, "is_invite_only" boolean NOT NULL, "annual_creator_reward_permill" integer NOT NULL, "revenue_share_ratio_permill" integer NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, "channel_id" text, "description" text, "whitelist_applicant_note" text, "whitelist_applicant_link" text, "accounts_num" integer NOT NULL, "number_of_revenue_share_activations" integer NOT NULL, "deissued" boolean NOT NULL, "current_amm_sale_id" text, "current_sale_id" text, "current_revenue_share_id" text, "number_of_vested_transfer_issued" integer NOT NULL, "last_price" numeric, CONSTRAINT "PK_d836a8c3d907b67099c140c4d84" PRIMARY KEY ("id"))`) - await db.query(`CREATE INDEX "IDX_1268fd020cf195b2e8d5d85093" ON "admin"."marketplace_token" ("symbol") `) - await db.query(`CREATE INDEX "IDX_b99bb1ecee77f23016f6ef687c" ON "admin"."marketplace_token" ("created_at") `) - } - - async down(db) { - await db.query(`DROP TABLE "admin"."user_interaction_count"`) - await db.query(`DROP INDEX "admin"."IDX_b5261af5f3fe48d77086ebc602"`) - await db.query(`DROP TABLE "admin"."marketplace_token"`) - await db.query(`DROP INDEX "admin"."IDX_1268fd020cf195b2e8d5d85093"`) - await db.query(`DROP INDEX "admin"."IDX_b99bb1ecee77f23016f6ef687c"`) - } -} diff --git a/db/migrations/1733920148217-Data.js b/db/migrations/1733920148217-Data.js deleted file mode 100644 index 4b9e4e483..000000000 --- a/db/migrations/1733920148217-Data.js +++ /dev/null @@ -1,15 +0,0 @@ -module.exports = class Data1733920148217 { - name = 'Data1733920148217' - - async up(db) { - await db.query(`ALTER TABLE "admin"."comment" ADD "tip_tier" character varying(7)`) - await db.query(`ALTER TABLE "admin"."comment" ADD "tip_amount" numeric NOT NULL DEFAULT 0`) - await db.query(`ALTER TABLE "admin"."comment" ADD "sort_priority" integer NOT NULL DEFAULT 0`) - } - - async down(db) { - await db.query(`ALTER TABLE "admin"."comment" DROP COLUMN "tip_tier"`) - await db.query(`ALTER TABLE "admin"."comment" DROP COLUMN "tip_amount"`) - await db.query(`ALTER TABLE "admin"."comment" DROP COLUMN "sort_priority"`) - } -} diff --git a/db/migrations/1733921114970-Views.js b/db/migrations/1733921114970-Views.js deleted file mode 100644 index a411c3595..000000000 --- a/db/migrations/1733921114970-Views.js +++ /dev/null @@ -1,41 +0,0 @@ - -const { getViewDefinitions } = require('../viewDefinitions') - -module.exports = class Views1733921114970 { - name = 'Views1733921114970' - - async up(db) { - // these two queries will be invoked and the cleaned up by the squid itself - // we only do this to be able to reference processor height in mappings - await db.query(`CREATE SCHEMA IF NOT EXISTS squid_processor;`) - await db.query(`CREATE TABLE IF NOT EXISTS squid_processor.status ( - id SERIAL PRIMARY KEY, - height INT - );`) - const viewDefinitions = getViewDefinitions(db); - for (const [tableName, viewConditions] of Object.entries(viewDefinitions)) { - if (Array.isArray(viewConditions)) { - await db.query(` - DROP VIEW IF EXISTS "${tableName}" CASCADE - `) - await db.query(` - CREATE OR REPLACE VIEW "${tableName}" AS - SELECT * - FROM "admin"."${tableName}" AS "this" - WHERE ${viewConditions.map(cond => `(${cond})`).join(' AND ')} - `); - } else { - await db.query(` - CREATE OR REPLACE VIEW "${tableName}" AS (${viewConditions}) - `); - } - } - } - - async down(db) { - const viewDefinitions = this.getViewDefinitions(db) - for (const viewName of Object.keys(viewDefinitions)) { - await db.query(`DROP VIEW "${viewName}"`) - } - } -} diff --git a/db/migrations/1749123353665-Data.js b/db/migrations/1749123353665-Data.js new file mode 100644 index 000000000..9fb8b99ef --- /dev/null +++ b/db/migrations/1749123353665-Data.js @@ -0,0 +1,581 @@ +module.exports = class Data1749123353665 { + name = 'Data1749123353665' + + async up(db) { + await db.query(`CREATE TABLE "curator"."bid" ("id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, "auction_id" character varying, "nft_id" character varying, "bidder_id" character varying, "amount" numeric NOT NULL, "is_canceled" boolean NOT NULL, "created_in_block" integer NOT NULL, "index_in_block" integer NOT NULL, "previous_top_bid_id" character varying, CONSTRAINT "PK_ed405dda320051aca2dcb1a50bb" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_9e594e5a61c0f3cb25679f6ba8" ON "curator"."bid" ("auction_id") `) + await db.query(`CREATE INDEX "IDX_3caf2d6b31d2fe45a2b85b8191" ON "curator"."bid" ("nft_id") `) + await db.query(`CREATE INDEX "IDX_e7618559409a903a897164156b" ON "curator"."bid" ("bidder_id") `) + await db.query(`CREATE INDEX "IDX_32cb73025ec49c87f4c594a265" ON "curator"."bid" ("previous_top_bid_id") `) + await db.query(`CREATE TABLE "curator"."auction" ("id" character varying NOT NULL, "nft_id" character varying, "winning_member_id" character varying, "starting_price" numeric NOT NULL, "buy_now_price" numeric, "auction_type" jsonb NOT NULL, "top_bid_id" character varying, "starts_at_block" integer NOT NULL, "ended_at_block" integer, "is_canceled" boolean NOT NULL, "is_completed" boolean NOT NULL, CONSTRAINT "PK_9dc876c629273e71646cf6dfa67" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_cfb47e97e60c9d1462576f85a8" ON "curator"."auction" ("nft_id") `) + await db.query(`CREATE INDEX "IDX_a3127ec87cccc5696b92cac4e0" ON "curator"."auction" ("winning_member_id") `) + await db.query(`CREATE INDEX "IDX_1673ad4b059742fbabfc40b275" ON "curator"."auction" ("top_bid_id") `) + 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 "curator"."banned_member" ("id" character varying NOT NULL, "member_id" character varying, "channel_id" character varying, CONSTRAINT "BannedMember_member_channel" UNIQUE ("member_id", "channel_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "PK_ebdf9a9c6d88f1116a5f2d0815d" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_ed36c6c26bf5410796c2fc21f7" ON "curator"."banned_member" ("channel_id") `) + await db.query(`CREATE INDEX "IDX_f29ff095bdb945975deca021ad" ON "curator"."banned_member" ("member_id", "channel_id") `) + await db.query(`CREATE TABLE "sale_transaction" ("id" character varying NOT NULL, "quantity" numeric NOT NULL, "sale_id" character varying, "account_id" character varying, "created_in" integer NOT NULL, CONSTRAINT "PK_06470e015a427563408e7e3661e" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_cfd7aa41d364144e6bbf677a48" ON "sale_transaction" ("account_id") `) + await db.query(`CREATE INDEX "IDX_b538792bd801ca0a1c77c03eff" ON "sale_transaction" ("sale_id", "account_id") `) + await db.query(`CREATE TABLE "sale" ("id" character varying NOT NULL, "token_id" character varying, "price_per_unit" numeric NOT NULL, "token_sale_allocation" numeric NOT NULL, "tokens_sold" numeric NOT NULL, "created_in" integer NOT NULL, "start_block" integer NOT NULL, "ends_at" integer NOT NULL, "terms_and_conditions" text NOT NULL, "max_amount_per_member" numeric, "finalized" boolean NOT NULL, "funds_source_account_id" character varying, CONSTRAINT "Sale_token_createdIn" UNIQUE ("token_id", "created_in") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "PK_d03891c457cbcd22974732b5de2" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_00468ff9c85265853384de0e1d" ON "sale" ("funds_source_account_id") `) + await db.query(`CREATE INDEX "IDX_5c5d611ec29439dc91eeea287b" ON "sale" ("token_id", "created_in") `) + await db.query(`CREATE TABLE "revenue_share_participation" ("id" character varying NOT NULL, "account_id" character varying, "revenue_share_id" character varying, "staked_amount" numeric NOT NULL, "earnings" numeric NOT NULL, "created_in" integer NOT NULL, "recovered" boolean NOT NULL, CONSTRAINT "RevenueShareParticipation_account_revenueShare" UNIQUE ("account_id", "revenue_share_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "PK_a6931d06b217f8055611ea26fc7" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_018b86600c0c1228ada1c928be" ON "revenue_share_participation" ("revenue_share_id") `) + await db.query(`CREATE INDEX "IDX_9ca49e1effab0c3543ae0839cf" ON "revenue_share_participation" ("account_id", "revenue_share_id") `) + await db.query(`CREATE TABLE "revenue_share" ("id" character varying NOT NULL, "token_id" character varying, "created_in" integer NOT NULL, "starting_at" integer NOT NULL, "ends_at" integer NOT NULL, "potential_participants_num" integer, "participants_num" integer NOT NULL, "allocation" numeric NOT NULL, "claimed" numeric NOT NULL, "finalized" boolean NOT NULL, CONSTRAINT "PK_6ef7c4be56b9290db1462885163" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_4e8bfc2037cececc86ba5192ea" ON "revenue_share" ("token_id") `) + await db.query(`CREATE TABLE "amm_transaction" ("id" character varying NOT NULL, "quantity" numeric NOT NULL, "price_paid" numeric NOT NULL, "amm_id" character varying, "account_id" character varying, "price_per_unit" numeric NOT NULL, "transaction_type" character varying(4) NOT NULL, "created_in" integer NOT NULL, CONSTRAINT "PK_783093757a6f260c72ded36d409" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_135d9555d7ea3e45dd78d8aede" ON "amm_transaction" ("amm_id") `) + await db.query(`CREATE INDEX "IDX_9109e9ce696736e0dd51d90fa7" ON "amm_transaction" ("account_id", "amm_id") `) + await db.query(`CREATE TABLE "amm_curve" ("id" character varying NOT NULL, "token_id" character varying, "burned_by_amm" numeric NOT NULL, "minted_by_amm" numeric NOT NULL, "amm_slope_parameter" numeric NOT NULL, "amm_init_price" numeric NOT NULL, "finalized" boolean NOT NULL, CONSTRAINT "PK_477b83cf84964aa40f38edf1db1" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_97bee00638822978784362d19f" ON "amm_curve" ("token_id") `) + await db.query(`CREATE TABLE "benefit" ("id" character varying NOT NULL, "token_id" character varying, "emoji_code" text, "title" text NOT NULL, "description" text NOT NULL, "display_order" integer NOT NULL, CONSTRAINT "Benefit_token_displayOrder" UNIQUE ("token_id", "display_order") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "PK_c024dccb30e6f4702adffe884d1" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_77ac3c1669ee14648626b078f9" ON "benefit" ("token_id", "display_order") `) + await db.query(`CREATE TABLE "creator_token" ("id" character varying NOT NULL, "status" character varying(6) NOT NULL, "avatar" jsonb, "total_supply" numeric NOT NULL, "is_featured" boolean NOT NULL, "symbol" text, "is_invite_only" boolean NOT NULL, "annual_creator_reward_permill" integer NOT NULL, "revenue_share_ratio_permill" integer NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, "description" text, "whitelist_applicant_note" text, "whitelist_applicant_link" text, "accounts_num" integer NOT NULL, "number_of_revenue_share_activations" integer NOT NULL, "deissued" boolean NOT NULL, "current_amm_sale_id" character varying, "current_sale_id" character varying, "current_revenue_share_id" character varying, "number_of_vested_transfer_issued" integer NOT NULL, "last_price" numeric, CONSTRAINT "PK_abbc66d13ff7d3828e4c830d325" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_790a6fc1f7aad3711c0672bb6b" ON "creator_token" ("symbol") `) + await db.query(`CREATE INDEX "IDX_64480ef90bda6c11650c3f4279" ON "creator_token" ("created_at") `) + await db.query(`CREATE INDEX "IDX_aabe40376c0eb47772b52780b1" ON "creator_token" ("current_amm_sale_id") `) + await db.query(`CREATE INDEX "IDX_5eca884f8728ff8f0c6a389c24" ON "creator_token" ("current_sale_id") `) + await db.query(`CREATE INDEX "IDX_df8c309ef364e49b9d2f17dc77" ON "creator_token" ("current_revenue_share_id") `) + await db.query(`CREATE TABLE "vesting_schedule" ("id" character varying NOT NULL, "cliff_ratio_permill" integer NOT NULL, "vesting_duration_blocks" integer NOT NULL, "cliff_duration_blocks" integer NOT NULL, "ends_at" integer NOT NULL, "cliff_block" integer NOT NULL, CONSTRAINT "PK_4818b05532ed9058110ed5b5b13" PRIMARY KEY ("id"))`) + await db.query(`CREATE TABLE "vested_account" ("id" character varying NOT NULL, "vesting_id" character varying, "account_id" character varying, "total_vesting_amount" numeric NOT NULL, "vesting_source" jsonb NOT NULL, "acquired_at" integer NOT NULL, CONSTRAINT "PK_23d64323d1b1b14ccbdb6ed2a64" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_745ee4e6a2dfd5de65fb8b9f44" ON "vested_account" ("vesting_id") `) + await db.query(`CREATE INDEX "IDX_6a0600f53023dca2c43b99a097" ON "vested_account" ("account_id") `) + await db.query(`CREATE TABLE "token_account" ("id" character varying NOT NULL, "member_id" character varying, "token_id" character varying, "staked_amount" numeric NOT NULL, "total_amount" numeric NOT NULL, "deleted" boolean NOT NULL, CONSTRAINT "PK_6121d7a5eafbe71fba146a98fd3" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_dc32b6b2efa86183e6329909e7" ON "token_account" ("member_id") `) + await db.query(`CREATE INDEX "IDX_b44e36e5b6093947ec28580a84" ON "token_account" ("token_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, "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 "storage_bucket" ("id" character varying NOT NULL, "operator_status" jsonb NOT NULL, "accepting_new_bags" boolean NOT NULL, "data_objects_size_limit" numeric NOT NULL, "data_object_count_limit" numeric NOT NULL, "data_objects_count" numeric NOT NULL, "data_objects_size" numeric NOT NULL, CONSTRAINT "PK_97cd0c3fe7f51e34216822e5f91" PRIMARY KEY ("id"))`) + await db.query(`CREATE TABLE "storage_bucket_bag" ("id" character varying NOT NULL, "storage_bucket_id" character varying, "bag_id" character varying, CONSTRAINT "StorageBucketBag_storageBucket_bag" UNIQUE ("storage_bucket_id", "bag_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "PK_9d54c04557134225652d566cc82" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_aaf00b2c7d0cba49f97da14fbb" ON "storage_bucket_bag" ("bag_id") `) + await db.query(`CREATE INDEX "IDX_4c475f6c9300284b095859eec3" ON "storage_bucket_bag" ("storage_bucket_id", "bag_id") `) + await db.query(`CREATE TABLE "distribution_bucket_family" ("id" character varying NOT NULL, CONSTRAINT "PK_8cb7454d1ec34b0d3bb7ecdee4e" PRIMARY KEY ("id"))`) + await db.query(`CREATE TABLE "distribution_bucket_operator" ("id" character varying NOT NULL, "distribution_bucket_id" character varying, "worker_id" integer NOT NULL, "status" character varying(7) NOT NULL, CONSTRAINT "PK_03b87e6e972f414bab94c142285" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_678dc5427cdde0cd4fef2c07a4" ON "distribution_bucket_operator" ("distribution_bucket_id") `) + await db.query(`CREATE TABLE "distribution_bucket" ("id" character varying NOT NULL, "family_id" character varying, "bucket_index" integer NOT NULL, "accepting_new_bags" boolean NOT NULL, "distributing" boolean NOT NULL, CONSTRAINT "PK_c90d25fff461f2f5fa9082e2fb7" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_8cb7454d1ec34b0d3bb7ecdee4" ON "distribution_bucket" ("family_id") `) + await db.query(`CREATE TABLE "distribution_bucket_bag" ("id" character varying NOT NULL, "distribution_bucket_id" character varying, "bag_id" character varying, CONSTRAINT "DistributionBucketBag_distributionBucket_bag" UNIQUE ("distribution_bucket_id", "bag_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "PK_02cb97c17ccabf42e8f5154d002" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_a9810100aee7584680f197c8ff" ON "distribution_bucket_bag" ("bag_id") `) + await db.query(`CREATE INDEX "IDX_32e552d352848d64ab82d38e9a" ON "distribution_bucket_bag" ("distribution_bucket_id", "bag_id") `) + await db.query(`CREATE TABLE "storage_bag" ("id" character varying NOT NULL, "owner" jsonb NOT NULL, CONSTRAINT "PK_242aecdc788d9b22bcbb9ade19a" PRIMARY KEY ("id"))`) + await db.query(`CREATE TABLE "curator"."storage_data_object" ("id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, "is_accepted" boolean NOT NULL, "size" numeric NOT NULL, "storage_bag_id" character varying, "ipfs_hash" text NOT NULL, "type" jsonb, "state_bloat_bond" numeric NOT NULL, "unset_at" TIMESTAMP WITH TIME ZONE, "resolved_urls" text array NOT NULL, CONSTRAINT "PK_61f224a4aef08f580a5ab4aadf0" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_ff8014300b8039dbaed764f51b" ON "curator"."storage_data_object" ("storage_bag_id") `) + await db.query(`CREATE TABLE "app" ("id" character varying NOT NULL, "name" text NOT NULL, "owner_member_id" character varying, "website_url" text, "use_uri" text, "small_icon" text, "medium_icon" text, "big_icon" text, "one_liner" text, "description" text, "terms_of_service" text, "platforms" text array, "category" text, "auth_key" text, CONSTRAINT "App_name" UNIQUE ("name") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "PK_9478629fc093d229df09e560aea" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_f36adbb7b096ceeb6f3e80ad14" ON "app" ("name") `) + await db.query(`CREATE INDEX "IDX_c9cc395bbc485f70a15be64553" ON "app" ("owner_member_id") `) + await db.query(`CREATE TABLE "curator"."channel" ("id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, "owner_member_id" character varying, "title" text, "description" text, "cover_photo_id" character varying, "avatar_photo_id" character varying, "is_public" boolean, "is_censored" boolean NOT NULL, "is_excluded" boolean NOT NULL, "language" text, "created_in_block" integer NOT NULL, "reward_account" text NOT NULL, "channel_state_bloat_bond" numeric NOT NULL, "follows_num" integer NOT NULL, "video_views_num" integer NOT NULL, "entry_app_id" character varying, "total_videos_created" integer NOT NULL, "revenue_share_ratio_percent" integer, "cumulative_reward_claimed" numeric NOT NULL, "cumulative_revenue" numeric NOT NULL, "cumulative_reward" numeric NOT NULL, "channel_weight" numeric, "ypp_status" jsonb, "is_yt_sync_enabled" boolean NOT NULL, CONSTRAINT "PK_590f33ee6ee7d76437acf362e39" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_a4752a0a0899dedc4d18077dd0" ON "curator"."channel" ("created_at") `) + await db.query(`CREATE INDEX "IDX_25c85bc448b5e236a4c1a5f789" ON "curator"."channel" ("owner_member_id") `) + await db.query(`CREATE INDEX "IDX_a77e12f3d8c6ced020e179a5e9" ON "curator"."channel" ("cover_photo_id") `) + await db.query(`CREATE INDEX "IDX_6997e94413b3f2f25a84e4a96f" ON "curator"."channel" ("avatar_photo_id") `) + await db.query(`CREATE INDEX "IDX_e58a2e1d78b8eccf40531a7fdb" ON "curator"."channel" ("language") `) + await db.query(`CREATE INDEX "IDX_118ecfa0199aeb5a014906933e" ON "curator"."channel" ("entry_app_id") `) + await db.query(`CREATE TABLE "curator"."video_featured_in_category" ("id" character varying NOT NULL, "video_id" character varying, "category_id" character varying, "video_cut_url" text, CONSTRAINT "VideoFeaturedInCategory_category_video" UNIQUE ("category_id", "video_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "PK_f84d38b5cdb7567ac04d6e9d209" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_7b16ddad43901921a8d3c8eab7" ON "curator"."video_featured_in_category" ("video_id") `) + await db.query(`CREATE INDEX "IDX_6d0917e1ac0cc06c8075bcf256" ON "curator"."video_featured_in_category" ("category_id", "video_id") `) + await db.query(`CREATE TABLE "curator"."video_category" ("id" character varying NOT NULL, "name" text, "description" text, "parent_category_id" character varying, "is_supported" boolean NOT NULL, "created_in_block" integer NOT NULL, CONSTRAINT "PK_2a5c61f32e9636ee10821e9a58d" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_cbe7e5d162a819e4ee2e2f6105" ON "curator"."video_category" ("name") `) + await db.query(`CREATE INDEX "IDX_da26b34f037c0d59d3c0d0646e" ON "curator"."video_category" ("parent_category_id") `) + await db.query(`CREATE TABLE "curator"."license" ("id" character varying NOT NULL, "code" integer, "attribution" text, "custom_text" text, CONSTRAINT "PK_f168ac1ca5ba87286d03b2ef905" PRIMARY KEY ("id"))`) + await db.query(`CREATE TABLE "curator"."video_subtitle" ("id" character varying NOT NULL, "video_id" character varying, "type" text NOT NULL, "language" text, "mime_type" text NOT NULL, "asset_id" character varying, CONSTRAINT "PK_2ac3e585fc608e673e7fbf94d8e" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_2203674f18d8052ed6bac39625" ON "curator"."video_subtitle" ("video_id") `) + await db.query(`CREATE INDEX "IDX_ffa63c28188eecc32af921bfc3" ON "curator"."video_subtitle" ("language") `) + await db.query(`CREATE INDEX "IDX_b6eabfb8de4128b28d73681020" ON "curator"."video_subtitle" ("asset_id") `) + await db.query(`CREATE TABLE "curator"."comment_reaction" ("id" character varying NOT NULL, "reaction_id" integer NOT NULL, "member_id" character varying, "comment_id" character varying, "video_id" character varying, CONSTRAINT "PK_87f27d282c06eb61b1e0cde2d24" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_15080d9fb7cf8b563103dd9d90" ON "curator"."comment_reaction" ("member_id") `) + await db.query(`CREATE INDEX "IDX_962582f04d3f639e33f43c54bb" ON "curator"."comment_reaction" ("comment_id") `) + await db.query(`CREATE INDEX "IDX_d7995b1d57614a6fbd0c103874" ON "curator"."comment_reaction" ("video_id") `) + await db.query(`CREATE TABLE "curator"."comment" ("id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, "author_id" character varying, "text" text NOT NULL, "video_id" character varying, "status" character varying(9) NOT NULL, "reactions_count_by_reaction_id" jsonb, "parent_comment_id" character varying, "replies_count" integer NOT NULL, "reactions_count" integer NOT NULL, "reactions_and_replies_count" integer NOT NULL, "is_edited" boolean NOT NULL, "is_excluded" boolean NOT NULL, "tip_tier" character varying(7), "tip_amount" numeric NOT NULL, "sort_priority" integer NOT NULL, CONSTRAINT "PK_0b0e4bbc8415ec426f87f3a88e2" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_3ce66469b26697baa097f8da92" ON "curator"."comment" ("author_id") `) + await db.query(`CREATE INDEX "IDX_1ff03403fd31dfeaba0623a89c" ON "curator"."comment" ("video_id") `) + await db.query(`CREATE INDEX "IDX_c3c2abe750c76c7c8e305f71f2" ON "curator"."comment" ("status") `) + await db.query(`CREATE INDEX "IDX_ac69bddf8202b7c0752d9dc8f3" ON "curator"."comment" ("parent_comment_id") `) + await db.query(`CREATE TABLE "curator"."video_reaction" ("id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, "reaction" character varying(6) NOT NULL, "member_id" character varying, "video_id" character varying, CONSTRAINT "PK_504876585c394f4ab33665dd44b" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_73dda64f53bbc7ec7035d5e7f0" ON "curator"."video_reaction" ("member_id") `) + await db.query(`CREATE INDEX "IDX_436a3836eb47acb5e1e3c88dde" ON "curator"."video_reaction" ("video_id") `) + await db.query(`CREATE TABLE "trailer_video" ("id" character varying NOT NULL, "video_id" character varying, "token_id" character varying NOT NULL, CONSTRAINT "TrailerVideo_token" UNIQUE ("token_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "TrailerVideo_token_video" UNIQUE ("token_id", "video_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "REL_0151a0342b10afcd1933f10656" UNIQUE ("token_id"), CONSTRAINT "PK_06ed751f0ca8164994ff327cacc" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_c73677538ef22a243568edac74" ON "trailer_video" ("video_id") `) + await db.query(`CREATE INDEX "IDX_0151a0342b10afcd1933f10656" ON "trailer_video" ("token_id") `) + await db.query(`CREATE INDEX "IDX_7eb550061f81d70d7c14b9368a" ON "trailer_video" ("token_id", "video_id") `) + await db.query(`CREATE TABLE "curator"."video" ("id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, "channel_id" character varying, "category_id" character varying, "title" text, "description" text, "duration" integer, "thumbnail_photo_id" character varying, "language" text, "orion_language" text, "has_marketing" boolean, "published_before_joystream" TIMESTAMP WITH TIME ZONE, "is_public" boolean, "is_censored" boolean NOT NULL, "is_excluded" boolean NOT NULL, "is_explicit" boolean, "license_id" character varying, "media_id" character varying, "video_state_bloat_bond" numeric NOT NULL, "created_in_block" integer NOT NULL, "is_comment_section_enabled" boolean NOT NULL, "pinned_comment_id" character varying, "comments_count" integer NOT NULL, "is_reaction_feature_enabled" boolean NOT NULL, "reactions_count_by_reaction_id" jsonb, "reactions_count" integer NOT NULL, "views_num" integer NOT NULL, "entry_app_id" character varying, "yt_video_id" text, "video_relevance" numeric NOT NULL, "is_short" boolean, "is_short_derived" boolean, "include_in_home_feed" boolean, CONSTRAINT "PK_1a2f3856250765d72e7e1636c8e" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_fe2b4b6aace15f1b6610830846" ON "curator"."video" ("created_at") `) + await db.query(`CREATE INDEX "IDX_81b11ef99a9db9ef1aed040d75" ON "curator"."video" ("channel_id") `) + await db.query(`CREATE INDEX "IDX_2a5c61f32e9636ee10821e9a58" ON "curator"."video" ("category_id") `) + await db.query(`CREATE INDEX "IDX_8530d052cc79b420f7ce2b4e09" ON "curator"."video" ("thumbnail_photo_id") `) + await db.query(`CREATE INDEX "IDX_57b335fa0a960877caf6d2fc29" ON "curator"."video" ("orion_language") `) + await db.query(`CREATE INDEX "IDX_3ec633ae5d0477f512b4ed957d" ON "curator"."video" ("license_id") `) + await db.query(`CREATE INDEX "IDX_2db879ed42e3308fe65e679672" ON "curator"."video" ("media_id") `) + await db.query(`CREATE INDEX "IDX_54f88a7decf7d22fd9bd9fa439" ON "curator"."video" ("pinned_comment_id") `) + await db.query(`CREATE INDEX "IDX_6c49ad08c44d36d11f77c426e4" ON "curator"."video" ("entry_app_id") `) + await db.query(`CREATE INDEX "IDX_f33816960d690ac836f5d5c28a" ON "curator"."video" ("video_relevance") `) + await db.query(`CREATE TABLE "curator"."owned_nft" ("id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, "video_id" character varying NOT NULL, "owner" jsonb NOT NULL, "transactional_status" jsonb, "creator_royalty" numeric, "last_sale_price" numeric, "last_sale_date" TIMESTAMP WITH TIME ZONE, "is_featured" boolean NOT NULL, CONSTRAINT "OwnedNft_video" UNIQUE ("video_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "REL_466896e39b9ec953f4f2545622" UNIQUE ("video_id"), CONSTRAINT "PK_5e0c289b350e863668fff44bb56" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_8c7201ed7d4765dcbcc3609356" ON "curator"."owned_nft" ("created_at") `) + await db.query(`CREATE INDEX "IDX_466896e39b9ec953f4f2545622" ON "curator"."owned_nft" ("video_id") `) + await db.query(`CREATE TABLE "storage_bucket_operator_metadata" ("id" character varying NOT NULL, "storage_bucket_id" character varying NOT NULL, "node_endpoint" text, "node_location" jsonb, "extra" text, CONSTRAINT "StorageBucketOperatorMetadata_storageBucket" UNIQUE ("storage_bucket_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "REL_7beffc9530b3f307bc1169cb52" UNIQUE ("storage_bucket_id"), CONSTRAINT "PK_9846a397400ae1a39b21fbd02d4" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_7beffc9530b3f307bc1169cb52" ON "storage_bucket_operator_metadata" ("storage_bucket_id") `) + await db.query(`CREATE TABLE "distribution_bucket_family_metadata" ("id" character varying NOT NULL, "family_id" character varying NOT NULL, "region" text, "description" text, "areas" jsonb, "latency_test_targets" text array, CONSTRAINT "DistributionBucketFamilyMetadata_family" UNIQUE ("family_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "REL_dd93ca0ea24f3e7a02f11c4c14" UNIQUE ("family_id"), CONSTRAINT "PK_df7a270835bb313d3ef17bdee2f" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_dd93ca0ea24f3e7a02f11c4c14" ON "distribution_bucket_family_metadata" ("family_id") `) + await db.query(`CREATE INDEX "IDX_5510d3b244a63d6ec702faa426" ON "distribution_bucket_family_metadata" ("region") `) + await db.query(`CREATE TABLE "distribution_bucket_operator_metadata" ("id" character varying NOT NULL, "distirbution_bucket_operator_id" character varying NOT NULL, "node_endpoint" text, "node_location" jsonb, "extra" text, CONSTRAINT "DistributionBucketOperatorMetadata_distirbutionBucketOperator" UNIQUE ("distirbution_bucket_operator_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "REL_69ec9bdc975b95f7dff94a7106" UNIQUE ("distirbution_bucket_operator_id"), CONSTRAINT "PK_9bbecaa12f30e3826922688274f" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_69ec9bdc975b95f7dff94a7106" ON "distribution_bucket_operator_metadata" ("distirbution_bucket_operator_id") `) + await db.query(`CREATE TABLE "curator"."marketplace_token" ("liquidity" integer, "market_cap" numeric, "cumulative_revenue" numeric, "amm_volume" numeric, "price_change" numeric, "liquidity_change" numeric, "id" character varying NOT NULL, "status" character varying(6) NOT NULL, "avatar" jsonb, "total_supply" numeric NOT NULL, "is_featured" boolean NOT NULL, "symbol" text, "is_invite_only" boolean NOT NULL, "annual_creator_reward_permill" integer NOT NULL, "revenue_share_ratio_permill" integer NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, "channel_id" text, "description" text, "whitelist_applicant_note" text, "whitelist_applicant_link" text, "accounts_num" integer NOT NULL, "number_of_revenue_share_activations" integer NOT NULL, "deissued" boolean NOT NULL, "current_amm_sale_id" text, "current_sale_id" text, "current_revenue_share_id" text, "number_of_vested_transfer_issued" integer NOT NULL, "last_price" numeric, CONSTRAINT "PK_d836a8c3d907b67099c140c4d84" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_1268fd020cf195b2e8d5d85093" ON "curator"."marketplace_token" ("symbol") `) + await db.query(`CREATE INDEX "IDX_b99bb1ecee77f23016f6ef687c" ON "curator"."marketplace_token" ("created_at") `) + await db.query(`CREATE TABLE "token_channel" ("id" character varying NOT NULL, "token_id" character varying NOT NULL, "channel_id" character varying NOT NULL, CONSTRAINT "TokenChannel_channel" UNIQUE ("channel_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "TokenChannel_token" UNIQUE ("token_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "TokenChannel_token_channel" UNIQUE ("token_id", "channel_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "REL_7105aa65a2d333bb2f66db129e" UNIQUE ("token_id"), CONSTRAINT "REL_b065bc433d65b0a6874073ea54" UNIQUE ("channel_id"), CONSTRAINT "PK_e5cd0127f70ee171db28af0293c" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_7105aa65a2d333bb2f66db129e" ON "token_channel" ("token_id") `) + await db.query(`CREATE INDEX "IDX_b065bc433d65b0a6874073ea54" ON "token_channel" ("channel_id") `) + await db.query(`CREATE INDEX "IDX_f13351e59524e009f99612af11" ON "token_channel" ("token_id", "channel_id") `) + await db.query(`CREATE TABLE "vested_sale" ("id" character varying NOT NULL, "sale_id" character varying NOT NULL, "vesting_id" character varying NOT NULL, CONSTRAINT "VestedSale_vesting" UNIQUE ("vesting_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "VestedSale_sale" UNIQUE ("sale_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "VestedSale_sale_vesting" UNIQUE ("sale_id", "vesting_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "REL_4b0d0d4f6a5ce72247ffe22324" UNIQUE ("sale_id"), CONSTRAINT "REL_ffa4428b95fc1c0e4df5b5f495" UNIQUE ("vesting_id"), CONSTRAINT "PK_223c9942cef9ded13304deb2488" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_4b0d0d4f6a5ce72247ffe22324" ON "vested_sale" ("sale_id") `) + await db.query(`CREATE INDEX "IDX_ffa4428b95fc1c0e4df5b5f495" ON "vested_sale" ("vesting_id") `) + await db.query(`CREATE INDEX "IDX_b2135c373c44a37e4e6842ead5" ON "vested_sale" ("sale_id", "vesting_id") `) + await db.query(`CREATE TABLE "curator"."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 "curator"."event" ("in_extrinsic") `) + await db.query(`CREATE INDEX "IDX_2c15918ff289396205521c5f3c" ON "curator"."event" ("timestamp") `) + await db.query(`CREATE TABLE "curator"."nft_history_entry" ("id" character varying NOT NULL, "nft_id" character varying, "event_id" character varying, CONSTRAINT "PK_9018e80b335a965a54959c4c6e2" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_57f51d35ecab042478fe2e31c1" ON "curator"."nft_history_entry" ("nft_id") `) + await db.query(`CREATE INDEX "IDX_d1a28b178f5d028d048d40ce20" ON "curator"."nft_history_entry" ("event_id") `) + await db.query(`CREATE TABLE "curator"."nft_activity" ("id" character varying NOT NULL, "member_id" character varying, "event_id" character varying, CONSTRAINT "PK_1553b1bbf8000039875a6e31536" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_18a65713a9fd0715c7a980f5d5" ON "curator"."nft_activity" ("member_id") `) + await db.query(`CREATE INDEX "IDX_94d325a753f2c08fdd416eb095" ON "curator"."nft_activity" ("event_id") `) + await db.query(`CREATE TABLE "curator"."user_interaction_count" ("id" character varying NOT NULL, "type" text, "entity_id" text, "day_timestamp" TIMESTAMP WITH TIME ZONE NOT NULL, "count" integer NOT NULL, CONSTRAINT "PK_8e334a51febcf02c54dff48147d" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_b5261af5f3fe48d77086ebc602" ON "curator"."user_interaction_count" ("day_timestamp") `) + await db.query(`CREATE TABLE "member_metadata" ("id" character varying NOT NULL, "name" text, "avatar" jsonb, "about" text, "member_id" character varying NOT NULL, CONSTRAINT "MemberMetadata_member" UNIQUE ("member_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "REL_e7e4d350f82ae2383894f465ed" UNIQUE ("member_id"), CONSTRAINT "PK_d3fcc374696465f3c0ac3ba8708" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_e7e4d350f82ae2383894f465ed" ON "member_metadata" ("member_id") `) + await db.query(`CREATE TABLE "curator_group" ("id" character varying NOT NULL, "is_active" boolean NOT NULL, CONSTRAINT "PK_0b4c0ab279d72bcbf4e16b65ff1" PRIMARY KEY ("id"))`) + await db.query(`CREATE TABLE "curator" ("id" character varying NOT NULL, CONSTRAINT "PK_5791051a62d2c2dfc593d38ab57" PRIMARY KEY ("id"))`) + await db.query(`CREATE TABLE "curator"."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"))`) + await db.query(`CREATE INDEX "IDX_822778b4b1ea8e3b60b127cb8b" ON "curator"."channel_follow" ("user_id") `) + await db.query(`CREATE INDEX "IDX_9bc0651dda94437ec18764a260" ON "curator"."channel_follow" ("channel_id") `) + await db.query(`CREATE TABLE "curator"."report" ("id" character varying NOT NULL, "user_id" character varying, "channel_id" text, "video_id" text, "timestamp" TIMESTAMP WITH TIME ZONE NOT NULL, "rationale" text NOT NULL, CONSTRAINT "PK_99e4d0bea58cba73c57f935a546" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_c6686efa4cd49fa9a429f01bac" ON "curator"."report" ("user_id") `) + await db.query(`CREATE INDEX "IDX_893057921f4b5cc37a0ef36684" ON "curator"."report" ("channel_id") `) + await db.query(`CREATE INDEX "IDX_f732b6f82095a935db68c9491f" ON "curator"."report" ("video_id") `) + await db.query(`CREATE TABLE "curator"."nft_featuring_request" ("id" character varying NOT NULL, "user_id" character varying, "nft_id" text NOT NULL, "timestamp" TIMESTAMP WITH TIME ZONE NOT NULL, "rationale" text NOT NULL, CONSTRAINT "PK_d0b1ccb74336b30b9575387d328" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_519be2a41216c278c35f254dcb" ON "curator"."nft_featuring_request" ("user_id") `) + await db.query(`CREATE INDEX "IDX_76d87e26cce72ac2e7ffa28dfb" ON "curator"."nft_featuring_request" ("nft_id") `) + await db.query(`CREATE TABLE "admin"."user" ("id" character varying NOT NULL, "is_root" boolean NOT NULL, "permissions" character varying(34) array, CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))`) + await db.query(`CREATE TABLE "curator"."video_view_event" ("id" character varying NOT NULL, "video_id" text NOT NULL, "user_id" character varying, "timestamp" TIMESTAMP WITH TIME ZONE NOT NULL, CONSTRAINT "PK_2efd85597a6a7a704fc4d0f7701" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_2e29fba63e12a2b1818e0782d7" ON "curator"."video_view_event" ("video_id") `) + await db.query(`CREATE INDEX "IDX_31e1e798ec387ad905cf98d33b" ON "curator"."video_view_event" ("user_id") `) + await db.query(`CREATE TABLE "admin"."gateway_config" ("id" character varying NOT NULL, "value" text NOT NULL, "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL, CONSTRAINT "PK_db1fa5a857fb6292eee4c493e6f" PRIMARY KEY ("id"))`) + await db.query(`CREATE TABLE "admin"."account" ("id" character varying NOT NULL, "user_id" character varying NOT NULL, "email" text NOT NULL, "is_email_confirmed" boolean NOT NULL, "is_blocked" boolean NOT NULL, "registered_at" TIMESTAMP WITH TIME ZONE NOT NULL, "membership_id" character varying NOT NULL, "joystream_account" text NOT NULL, "notification_preferences" jsonb NOT NULL, "referrer_channel_id" text, CONSTRAINT "Account_joystreamAccount" UNIQUE ("joystream_account") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "Account_membership" UNIQUE ("membership_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "Account_email" UNIQUE ("email") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "Account_user" UNIQUE ("user_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "REL_efef1e5fdbe318a379c06678c5" UNIQUE ("user_id"), CONSTRAINT "REL_601b93655bcbe73cb58d8c80cd" UNIQUE ("membership_id"), CONSTRAINT "PK_54115ee388cdb6d86bb4bf5b2ea" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_efef1e5fdbe318a379c06678c5" ON "admin"."account" ("user_id") `) + await db.query(`CREATE INDEX "IDX_4c8f96ccf523e9a3faefd5bdd4" ON "admin"."account" ("email") `) + await db.query(`CREATE INDEX "IDX_601b93655bcbe73cb58d8c80cd" ON "admin"."account" ("membership_id") `) + await db.query(`CREATE INDEX "IDX_df4da05a7a80c1afd18b8f0990" ON "admin"."account" ("joystream_account") `) + 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, "dispatch_block" integer, "recipient" jsonb NOT NULL, CONSTRAINT "PK_705b6c7cdf9b2c2ff7ac7872cb7" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_6bfa96ab97f1a09d73091294ef" ON "notification" ("account_id") `) + await db.query(`CREATE INDEX "IDX_122be1f0696e0255acf95f9e33" ON "notification" ("event_id") `) + await db.query(`CREATE TABLE "admin"."email_delivery_attempt" ("id" character varying NOT NULL, "notification_delivery_id" character varying, "status" jsonb NOT NULL, "timestamp" TIMESTAMP WITH TIME ZONE NOT NULL, CONSTRAINT "PK_876948339083a2f1092245f7a32" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_f985b9b362249af72cac0f52a3" ON "admin"."email_delivery_attempt" ("notification_delivery_id") `) + await db.query(`CREATE TABLE "admin"."notification_email_delivery" ("id" character varying NOT NULL, "notification_id" character varying, "discard" boolean NOT NULL, CONSTRAINT "PK_60dc7ff42a7abf7b0d44bf60516" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_3b756627c3146db150d66d1292" ON "admin"."notification_email_delivery" ("notification_id") `) + await db.query(`CREATE TABLE "curator"."video_hero" ("id" character varying NOT NULL, "video_id" character varying, "hero_title" text NOT NULL, "hero_video_cut_url" text NOT NULL, "hero_poster_url" text NOT NULL, "activated_at" TIMESTAMP WITH TIME ZONE, CONSTRAINT "PK_f3b63979879773378afac0b9495" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_9feac5d9713a9f07e32eb8ba7a" ON "curator"."video_hero" ("video_id") `) + await db.query(`CREATE TABLE "curator"."video_media_encoding" ("id" character varying NOT NULL, "codec_name" text, "container" text, "mime_media_type" text, CONSTRAINT "PK_52e25874f8d8a381e154d1125e0" PRIMARY KEY ("id"))`) + await db.query(`CREATE TABLE "curator"."video_media_metadata" ("id" character varying NOT NULL, "encoding_id" character varying, "pixel_width" integer, "pixel_height" integer, "size" numeric, "video_id" character varying NOT NULL, "created_in_block" integer NOT NULL, CONSTRAINT "VideoMediaMetadata_video" UNIQUE ("video_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "REL_4dc101240e8e1536b770aee202" UNIQUE ("video_id"), CONSTRAINT "PK_86a13815734e589cd86d0465e2d" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_5944dc5896cb16bd395414a0ce" ON "curator"."video_media_metadata" ("encoding_id") `) + await db.query(`CREATE INDEX "IDX_4dc101240e8e1536b770aee202" ON "curator"."video_media_metadata" ("video_id") `) + await db.query(`CREATE TABLE "encryption_artifacts" ("id" character varying NOT NULL, "account_id" character varying NOT NULL, "cipher_iv" text NOT NULL, "encrypted_seed" text NOT NULL, CONSTRAINT "EncryptionArtifacts_account" UNIQUE ("account_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "REL_ec8f68a544aadc4fbdadefe4a0" UNIQUE ("account_id"), CONSTRAINT "PK_6441471581ba6d149ad75655bd0" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_ec8f68a544aadc4fbdadefe4a0" ON "encryption_artifacts" ("account_id") `) + await db.query(`CREATE TABLE "admin"."session" ("id" character varying NOT NULL, "browser" text NOT NULL, "os" text NOT NULL, "device" text NOT NULL, "device_type" text, "user_id" character varying, "account_id" character varying, "ip" text NOT NULL, "started_at" TIMESTAMP WITH TIME ZONE NOT NULL, "expiry" TIMESTAMP WITH TIME ZONE NOT NULL, CONSTRAINT "PK_f55da76ac1c3ac420f444d2ff11" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_30e98e8746699fb9af235410af" ON "admin"."session" ("user_id") `) + await db.query(`CREATE INDEX "IDX_fae5a6b4a57f098e9af8520d49" ON "admin"."session" ("account_id") `) + await db.query(`CREATE INDEX "IDX_213b5a19bfdbe0ab6e06b1dede" ON "admin"."session" ("ip") `) + await db.query(`CREATE TABLE "session_encryption_artifacts" ("id" character varying NOT NULL, "session_id" character varying NOT NULL, "cipher_iv" text NOT NULL, "cipher_key" text NOT NULL, CONSTRAINT "SessionEncryptionArtifacts_session" UNIQUE ("session_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "REL_3612880efd8926a17eba5ab0e1" UNIQUE ("session_id"), CONSTRAINT "PK_e328da2643599e265a848219885" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_3612880efd8926a17eba5ab0e1" ON "session_encryption_artifacts" ("session_id") `) + await db.query(`CREATE TABLE "admin"."token" ("id" character varying NOT NULL, "type" character varying(18) NOT NULL, "issued_at" TIMESTAMP WITH TIME ZONE NOT NULL, "expiry" TIMESTAMP WITH TIME ZONE NOT NULL, "issued_for_id" character varying, CONSTRAINT "PK_82fae97f905930df5d62a702fc9" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_a6fe18c105f85a63d761ccb078" ON "admin"."token" ("issued_for_id") `) + await db.query(`CREATE TABLE "next_entity_id" ("entity_name" character varying NOT NULL, "next_id" bigint NOT NULL, CONSTRAINT "PK_09a3b40db622a65096e7344d7ae" PRIMARY KEY ("entity_name"))`) + await db.query(`CREATE TABLE "admin"."orion_offchain_cursor" ("cursor_name" character varying NOT NULL, "value" bigint NOT NULL, CONSTRAINT "PK_7083797352af5a21224b6c8ccbc" PRIMARY KEY ("cursor_name"))`) + await db.query(`ALTER TABLE "curator"."bid" ADD CONSTRAINT "FK_9e594e5a61c0f3cb25679f6ba8d" FOREIGN KEY ("auction_id") REFERENCES "curator"."auction"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."bid" ADD CONSTRAINT "FK_3caf2d6b31d2fe45a2b85b81912" FOREIGN KEY ("nft_id") REFERENCES "curator"."owned_nft"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."bid" ADD CONSTRAINT "FK_e7618559409a903a897164156b7" FOREIGN KEY ("bidder_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."bid" ADD CONSTRAINT "FK_32cb73025ec49c87f4c594a265f" FOREIGN KEY ("previous_top_bid_id") REFERENCES "curator"."bid"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."auction" ADD CONSTRAINT "FK_cfb47e97e60c9d1462576f85a88" FOREIGN KEY ("nft_id") REFERENCES "curator"."owned_nft"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."auction" ADD CONSTRAINT "FK_a3127ec87cccc5696b92cac4e09" FOREIGN KEY ("winning_member_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."auction" ADD CONSTRAINT "FK_1673ad4b059742fbabfc40b275c" FOREIGN KEY ("top_bid_id") REFERENCES "curator"."bid"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "auction_whitelisted_member" ADD CONSTRAINT "FK_aad797677bc7c7c7dc1f1d397f5" FOREIGN KEY ("auction_id") REFERENCES "curator"."auction"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "auction_whitelisted_member" ADD CONSTRAINT "FK_d5ae4854487c7658b64225be305" FOREIGN KEY ("member_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."banned_member" ADD CONSTRAINT "FK_b94ea874da235d9b6fbc35cf58e" FOREIGN KEY ("member_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."banned_member" ADD CONSTRAINT "FK_ed36c6c26bf5410796c2fc21f74" FOREIGN KEY ("channel_id") REFERENCES "curator"."channel"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "sale_transaction" ADD CONSTRAINT "FK_7c477ad14796b65a8e47214adc9" FOREIGN KEY ("sale_id") REFERENCES "sale"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "sale_transaction" ADD CONSTRAINT "FK_cfd7aa41d364144e6bbf677a488" FOREIGN KEY ("account_id") REFERENCES "token_account"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "sale" ADD CONSTRAINT "FK_53aae73a92bcdefd80d4bb94e7f" FOREIGN KEY ("token_id") REFERENCES "creator_token"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "sale" ADD CONSTRAINT "FK_00468ff9c85265853384de0e1dd" FOREIGN KEY ("funds_source_account_id") REFERENCES "token_account"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "revenue_share_participation" ADD CONSTRAINT "FK_7549dc863632b065f111a532551" FOREIGN KEY ("account_id") REFERENCES "token_account"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "revenue_share_participation" ADD CONSTRAINT "FK_018b86600c0c1228ada1c928be7" FOREIGN KEY ("revenue_share_id") REFERENCES "revenue_share"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "revenue_share" ADD CONSTRAINT "FK_4e8bfc2037cececc86ba5192ea9" FOREIGN KEY ("token_id") REFERENCES "creator_token"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "amm_transaction" ADD CONSTRAINT "FK_135d9555d7ea3e45dd78d8aedec" FOREIGN KEY ("amm_id") REFERENCES "amm_curve"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "amm_transaction" ADD CONSTRAINT "FK_51f006dbc040d62dc479adbee78" FOREIGN KEY ("account_id") REFERENCES "token_account"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "amm_curve" ADD CONSTRAINT "FK_97bee00638822978784362d19fc" FOREIGN KEY ("token_id") REFERENCES "creator_token"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "benefit" ADD CONSTRAINT "FK_b484e2182fc7a1910e84a5ae7ad" FOREIGN KEY ("token_id") REFERENCES "creator_token"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "creator_token" ADD CONSTRAINT "FK_aabe40376c0eb47772b52780b19" FOREIGN KEY ("current_amm_sale_id") REFERENCES "amm_curve"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "creator_token" ADD CONSTRAINT "FK_5eca884f8728ff8f0c6a389c24b" FOREIGN KEY ("current_sale_id") REFERENCES "sale"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "creator_token" ADD CONSTRAINT "FK_df8c309ef364e49b9d2f17dc778" FOREIGN KEY ("current_revenue_share_id") REFERENCES "revenue_share"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "vested_account" ADD CONSTRAINT "FK_745ee4e6a2dfd5de65fb8b9f44a" FOREIGN KEY ("vesting_id") REFERENCES "vesting_schedule"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "vested_account" ADD CONSTRAINT "FK_6a0600f53023dca2c43b99a0974" FOREIGN KEY ("account_id") REFERENCES "token_account"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "token_account" ADD CONSTRAINT "FK_dc32b6b2efa86183e6329909e73" FOREIGN KEY ("member_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "token_account" ADD CONSTRAINT "FK_02862fa18dececb99dd81a6a6a9" FOREIGN KEY ("token_id") REFERENCES "creator_token"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "storage_bucket_bag" ADD CONSTRAINT "FK_791e2f82e3919ffcef8712aa1b9" FOREIGN KEY ("storage_bucket_id") REFERENCES "storage_bucket"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "storage_bucket_bag" ADD CONSTRAINT "FK_aaf00b2c7d0cba49f97da14fbba" FOREIGN KEY ("bag_id") REFERENCES "storage_bag"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "distribution_bucket_operator" ADD CONSTRAINT "FK_678dc5427cdde0cd4fef2c07a43" FOREIGN KEY ("distribution_bucket_id") REFERENCES "distribution_bucket"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "distribution_bucket" ADD CONSTRAINT "FK_8cb7454d1ec34b0d3bb7ecdee4e" FOREIGN KEY ("family_id") REFERENCES "distribution_bucket_family"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "distribution_bucket_bag" ADD CONSTRAINT "FK_8a807921f1aae60d4ba94895826" FOREIGN KEY ("distribution_bucket_id") REFERENCES "distribution_bucket"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "distribution_bucket_bag" ADD CONSTRAINT "FK_a9810100aee7584680f197c8ff0" FOREIGN KEY ("bag_id") REFERENCES "storage_bag"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."storage_data_object" ADD CONSTRAINT "FK_ff8014300b8039dbaed764f51bc" FOREIGN KEY ("storage_bag_id") REFERENCES "storage_bag"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "app" ADD CONSTRAINT "FK_c9cc395bbc485f70a15be64553e" FOREIGN KEY ("owner_member_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."channel" ADD CONSTRAINT "FK_25c85bc448b5e236a4c1a5f7895" FOREIGN KEY ("owner_member_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."channel" ADD CONSTRAINT "FK_a77e12f3d8c6ced020e179a5e94" FOREIGN KEY ("cover_photo_id") REFERENCES "curator"."storage_data_object"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."channel" ADD CONSTRAINT "FK_6997e94413b3f2f25a84e4a96f8" FOREIGN KEY ("avatar_photo_id") REFERENCES "curator"."storage_data_object"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."channel" ADD CONSTRAINT "FK_118ecfa0199aeb5a014906933e8" FOREIGN KEY ("entry_app_id") REFERENCES "app"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."video_featured_in_category" ADD CONSTRAINT "FK_7b16ddad43901921a8d3c8eab71" FOREIGN KEY ("video_id") REFERENCES "curator"."video"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."video_featured_in_category" ADD CONSTRAINT "FK_0e6bb49ce9d022cd872f3ab4288" FOREIGN KEY ("category_id") REFERENCES "curator"."video_category"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."video_category" ADD CONSTRAINT "FK_da26b34f037c0d59d3c0d0646e9" FOREIGN KEY ("parent_category_id") REFERENCES "curator"."video_category"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."video_subtitle" ADD CONSTRAINT "FK_2203674f18d8052ed6bac396252" FOREIGN KEY ("video_id") REFERENCES "curator"."video"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."video_subtitle" ADD CONSTRAINT "FK_b6eabfb8de4128b28d73681020f" FOREIGN KEY ("asset_id") REFERENCES "curator"."storage_data_object"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."comment_reaction" ADD CONSTRAINT "FK_15080d9fb7cf8b563103dd9d900" FOREIGN KEY ("member_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."comment_reaction" ADD CONSTRAINT "FK_962582f04d3f639e33f43c54bbc" FOREIGN KEY ("comment_id") REFERENCES "curator"."comment"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."comment_reaction" ADD CONSTRAINT "FK_d7995b1d57614a6fbd0c103874d" FOREIGN KEY ("video_id") REFERENCES "curator"."video"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."comment" ADD CONSTRAINT "FK_3ce66469b26697baa097f8da923" FOREIGN KEY ("author_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."comment" ADD CONSTRAINT "FK_1ff03403fd31dfeaba0623a89cf" FOREIGN KEY ("video_id") REFERENCES "curator"."video"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."comment" ADD CONSTRAINT "FK_ac69bddf8202b7c0752d9dc8f32" FOREIGN KEY ("parent_comment_id") REFERENCES "curator"."comment"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."video_reaction" ADD CONSTRAINT "FK_73dda64f53bbc7ec7035d5e7f09" FOREIGN KEY ("member_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."video_reaction" ADD CONSTRAINT "FK_436a3836eb47acb5e1e3c88ddea" FOREIGN KEY ("video_id") REFERENCES "curator"."video"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "trailer_video" ADD CONSTRAINT "FK_c73677538ef22a243568edac74b" FOREIGN KEY ("video_id") REFERENCES "curator"."video"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "trailer_video" ADD CONSTRAINT "FK_0151a0342b10afcd1933f106564" FOREIGN KEY ("token_id") REFERENCES "creator_token"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."video" ADD CONSTRAINT "FK_81b11ef99a9db9ef1aed040d750" FOREIGN KEY ("channel_id") REFERENCES "curator"."channel"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."video" ADD CONSTRAINT "FK_2a5c61f32e9636ee10821e9a58d" FOREIGN KEY ("category_id") REFERENCES "curator"."video_category"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."video" ADD CONSTRAINT "FK_8530d052cc79b420f7ce2b4e09d" FOREIGN KEY ("thumbnail_photo_id") REFERENCES "curator"."storage_data_object"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."video" ADD CONSTRAINT "FK_3ec633ae5d0477f512b4ed957d6" FOREIGN KEY ("license_id") REFERENCES "curator"."license"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."video" ADD CONSTRAINT "FK_2db879ed42e3308fe65e6796729" FOREIGN KEY ("media_id") REFERENCES "curator"."storage_data_object"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."video" ADD CONSTRAINT "FK_54f88a7decf7d22fd9bd9fa439a" FOREIGN KEY ("pinned_comment_id") REFERENCES "curator"."comment"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."video" ADD CONSTRAINT "FK_6c49ad08c44d36d11f77c426e43" FOREIGN KEY ("entry_app_id") REFERENCES "app"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."owned_nft" ADD CONSTRAINT "FK_466896e39b9ec953f4f2545622d" FOREIGN KEY ("video_id") REFERENCES "curator"."video"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "storage_bucket_operator_metadata" ADD CONSTRAINT "FK_7beffc9530b3f307bc1169cb524" FOREIGN KEY ("storage_bucket_id") REFERENCES "storage_bucket"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "distribution_bucket_family_metadata" ADD CONSTRAINT "FK_dd93ca0ea24f3e7a02f11c4c149" FOREIGN KEY ("family_id") REFERENCES "distribution_bucket_family"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "distribution_bucket_operator_metadata" ADD CONSTRAINT "FK_69ec9bdc975b95f7dff94a71069" FOREIGN KEY ("distirbution_bucket_operator_id") REFERENCES "distribution_bucket_operator"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "token_channel" ADD CONSTRAINT "FK_7105aa65a2d333bb2f66db129e9" FOREIGN KEY ("token_id") REFERENCES "creator_token"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "token_channel" ADD CONSTRAINT "FK_b065bc433d65b0a6874073ea540" FOREIGN KEY ("channel_id") REFERENCES "curator"."channel"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "vested_sale" ADD CONSTRAINT "FK_4b0d0d4f6a5ce72247ffe223240" FOREIGN KEY ("sale_id") REFERENCES "sale"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "vested_sale" ADD CONSTRAINT "FK_ffa4428b95fc1c0e4df5b5f4952" FOREIGN KEY ("vesting_id") REFERENCES "vesting_schedule"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."nft_history_entry" ADD CONSTRAINT "FK_57f51d35ecab042478fe2e31c19" FOREIGN KEY ("nft_id") REFERENCES "curator"."owned_nft"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."nft_history_entry" ADD CONSTRAINT "FK_d1a28b178f5d028d048d40ce208" FOREIGN KEY ("event_id") REFERENCES "curator"."event"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."nft_activity" ADD CONSTRAINT "FK_18a65713a9fd0715c7a980f5d54" FOREIGN KEY ("member_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."nft_activity" ADD CONSTRAINT "FK_94d325a753f2c08fdd416eb095f" FOREIGN KEY ("event_id") REFERENCES "curator"."event"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "member_metadata" ADD CONSTRAINT "FK_e7e4d350f82ae2383894f465ede" FOREIGN KEY ("member_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."channel_follow" ADD CONSTRAINT "FK_822778b4b1ea8e3b60b127cb8b1" FOREIGN KEY ("user_id") REFERENCES "admin"."user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."report" ADD CONSTRAINT "FK_c6686efa4cd49fa9a429f01bac8" FOREIGN KEY ("user_id") REFERENCES "admin"."user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."nft_featuring_request" ADD CONSTRAINT "FK_519be2a41216c278c35f254dcba" FOREIGN KEY ("user_id") REFERENCES "admin"."user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."video_view_event" ADD CONSTRAINT "FK_31e1e798ec387ad905cf98d33b0" FOREIGN KEY ("user_id") REFERENCES "admin"."user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "admin"."account" ADD CONSTRAINT "FK_efef1e5fdbe318a379c06678c51" FOREIGN KEY ("user_id") REFERENCES "admin"."user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "admin"."account" ADD CONSTRAINT "FK_601b93655bcbe73cb58d8c80cd3" FOREIGN KEY ("membership_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_6bfa96ab97f1a09d73091294efc" FOREIGN KEY ("account_id") REFERENCES "admin"."account"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_122be1f0696e0255acf95f9e336" FOREIGN KEY ("event_id") REFERENCES "curator"."event"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "admin"."email_delivery_attempt" ADD CONSTRAINT "FK_f985b9b362249af72cac0f52a3b" FOREIGN KEY ("notification_delivery_id") REFERENCES "admin"."notification_email_delivery"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "admin"."notification_email_delivery" ADD CONSTRAINT "FK_3b756627c3146db150d66d12929" FOREIGN KEY ("notification_id") REFERENCES "notification"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."video_hero" ADD CONSTRAINT "FK_9feac5d9713a9f07e32eb8ba7a1" FOREIGN KEY ("video_id") REFERENCES "curator"."video"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."video_media_metadata" ADD CONSTRAINT "FK_5944dc5896cb16bd395414a0ce0" FOREIGN KEY ("encoding_id") REFERENCES "curator"."video_media_encoding"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "curator"."video_media_metadata" ADD CONSTRAINT "FK_4dc101240e8e1536b770aee202a" FOREIGN KEY ("video_id") REFERENCES "curator"."video"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "encryption_artifacts" ADD CONSTRAINT "FK_ec8f68a544aadc4fbdadefe4a0a" FOREIGN KEY ("account_id") REFERENCES "admin"."account"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "admin"."session" ADD CONSTRAINT "FK_30e98e8746699fb9af235410aff" FOREIGN KEY ("user_id") REFERENCES "admin"."user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "admin"."session" ADD CONSTRAINT "FK_fae5a6b4a57f098e9af8520d499" FOREIGN KEY ("account_id") REFERENCES "admin"."account"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "session_encryption_artifacts" ADD CONSTRAINT "FK_3612880efd8926a17eba5ab0e1a" FOREIGN KEY ("session_id") REFERENCES "admin"."session"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "admin"."token" ADD CONSTRAINT "FK_a6fe18c105f85a63d761ccb0780" FOREIGN KEY ("issued_for_id") REFERENCES "admin"."account"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + } + + async down(db) { + await db.query(`DROP TABLE "curator"."bid"`) + await db.query(`DROP INDEX "curator"."IDX_9e594e5a61c0f3cb25679f6ba8"`) + await db.query(`DROP INDEX "curator"."IDX_3caf2d6b31d2fe45a2b85b8191"`) + await db.query(`DROP INDEX "curator"."IDX_e7618559409a903a897164156b"`) + await db.query(`DROP INDEX "curator"."IDX_32cb73025ec49c87f4c594a265"`) + await db.query(`DROP TABLE "curator"."auction"`) + await db.query(`DROP INDEX "curator"."IDX_cfb47e97e60c9d1462576f85a8"`) + await db.query(`DROP INDEX "curator"."IDX_a3127ec87cccc5696b92cac4e0"`) + await db.query(`DROP INDEX "curator"."IDX_1673ad4b059742fbabfc40b275"`) + await db.query(`DROP TABLE "auction_whitelisted_member"`) + await db.query(`DROP INDEX "public"."IDX_d5ae4854487c7658b64225be30"`) + await db.query(`DROP INDEX "public"."IDX_5468573a96fa51c03743de5912"`) + await db.query(`DROP TABLE "curator"."banned_member"`) + await db.query(`DROP INDEX "curator"."IDX_ed36c6c26bf5410796c2fc21f7"`) + await db.query(`DROP INDEX "curator"."IDX_f29ff095bdb945975deca021ad"`) + await db.query(`DROP TABLE "sale_transaction"`) + await db.query(`DROP INDEX "public"."IDX_cfd7aa41d364144e6bbf677a48"`) + await db.query(`DROP INDEX "public"."IDX_b538792bd801ca0a1c77c03eff"`) + await db.query(`DROP TABLE "sale"`) + await db.query(`DROP INDEX "public"."IDX_00468ff9c85265853384de0e1d"`) + await db.query(`DROP INDEX "public"."IDX_5c5d611ec29439dc91eeea287b"`) + await db.query(`DROP TABLE "revenue_share_participation"`) + await db.query(`DROP INDEX "public"."IDX_018b86600c0c1228ada1c928be"`) + await db.query(`DROP INDEX "public"."IDX_9ca49e1effab0c3543ae0839cf"`) + await db.query(`DROP TABLE "revenue_share"`) + await db.query(`DROP INDEX "public"."IDX_4e8bfc2037cececc86ba5192ea"`) + await db.query(`DROP TABLE "amm_transaction"`) + await db.query(`DROP INDEX "public"."IDX_135d9555d7ea3e45dd78d8aede"`) + await db.query(`DROP INDEX "public"."IDX_9109e9ce696736e0dd51d90fa7"`) + await db.query(`DROP TABLE "amm_curve"`) + await db.query(`DROP INDEX "public"."IDX_97bee00638822978784362d19f"`) + await db.query(`DROP TABLE "benefit"`) + await db.query(`DROP INDEX "public"."IDX_77ac3c1669ee14648626b078f9"`) + await db.query(`DROP TABLE "creator_token"`) + await db.query(`DROP INDEX "public"."IDX_790a6fc1f7aad3711c0672bb6b"`) + await db.query(`DROP INDEX "public"."IDX_64480ef90bda6c11650c3f4279"`) + await db.query(`DROP INDEX "public"."IDX_aabe40376c0eb47772b52780b1"`) + await db.query(`DROP INDEX "public"."IDX_5eca884f8728ff8f0c6a389c24"`) + await db.query(`DROP INDEX "public"."IDX_df8c309ef364e49b9d2f17dc77"`) + await db.query(`DROP TABLE "vesting_schedule"`) + await db.query(`DROP TABLE "vested_account"`) + await db.query(`DROP INDEX "public"."IDX_745ee4e6a2dfd5de65fb8b9f44"`) + await db.query(`DROP INDEX "public"."IDX_6a0600f53023dca2c43b99a097"`) + await db.query(`DROP TABLE "token_account"`) + await db.query(`DROP INDEX "public"."IDX_dc32b6b2efa86183e6329909e7"`) + await db.query(`DROP INDEX "public"."IDX_b44e36e5b6093947ec28580a84"`) + await db.query(`DROP TABLE "membership"`) + await db.query(`DROP INDEX "public"."IDX_0c5b879f9f2ca57a774f74f7f0"`) + await db.query(`DROP TABLE "storage_bucket"`) + await db.query(`DROP TABLE "storage_bucket_bag"`) + await db.query(`DROP INDEX "public"."IDX_aaf00b2c7d0cba49f97da14fbb"`) + await db.query(`DROP INDEX "public"."IDX_4c475f6c9300284b095859eec3"`) + await db.query(`DROP TABLE "distribution_bucket_family"`) + await db.query(`DROP TABLE "distribution_bucket_operator"`) + await db.query(`DROP INDEX "public"."IDX_678dc5427cdde0cd4fef2c07a4"`) + await db.query(`DROP TABLE "distribution_bucket"`) + await db.query(`DROP INDEX "public"."IDX_8cb7454d1ec34b0d3bb7ecdee4"`) + await db.query(`DROP TABLE "distribution_bucket_bag"`) + await db.query(`DROP INDEX "public"."IDX_a9810100aee7584680f197c8ff"`) + await db.query(`DROP INDEX "public"."IDX_32e552d352848d64ab82d38e9a"`) + await db.query(`DROP TABLE "storage_bag"`) + await db.query(`DROP TABLE "curator"."storage_data_object"`) + await db.query(`DROP INDEX "curator"."IDX_ff8014300b8039dbaed764f51b"`) + await db.query(`DROP TABLE "app"`) + await db.query(`DROP INDEX "public"."IDX_f36adbb7b096ceeb6f3e80ad14"`) + await db.query(`DROP INDEX "public"."IDX_c9cc395bbc485f70a15be64553"`) + await db.query(`DROP TABLE "curator"."channel"`) + await db.query(`DROP INDEX "curator"."IDX_a4752a0a0899dedc4d18077dd0"`) + await db.query(`DROP INDEX "curator"."IDX_25c85bc448b5e236a4c1a5f789"`) + await db.query(`DROP INDEX "curator"."IDX_a77e12f3d8c6ced020e179a5e9"`) + await db.query(`DROP INDEX "curator"."IDX_6997e94413b3f2f25a84e4a96f"`) + await db.query(`DROP INDEX "curator"."IDX_e58a2e1d78b8eccf40531a7fdb"`) + await db.query(`DROP INDEX "curator"."IDX_118ecfa0199aeb5a014906933e"`) + await db.query(`DROP TABLE "curator"."video_featured_in_category"`) + await db.query(`DROP INDEX "curator"."IDX_7b16ddad43901921a8d3c8eab7"`) + await db.query(`DROP INDEX "curator"."IDX_6d0917e1ac0cc06c8075bcf256"`) + await db.query(`DROP TABLE "curator"."video_category"`) + await db.query(`DROP INDEX "curator"."IDX_cbe7e5d162a819e4ee2e2f6105"`) + await db.query(`DROP INDEX "curator"."IDX_da26b34f037c0d59d3c0d0646e"`) + await db.query(`DROP TABLE "curator"."license"`) + await db.query(`DROP TABLE "curator"."video_subtitle"`) + await db.query(`DROP INDEX "curator"."IDX_2203674f18d8052ed6bac39625"`) + await db.query(`DROP INDEX "curator"."IDX_ffa63c28188eecc32af921bfc3"`) + await db.query(`DROP INDEX "curator"."IDX_b6eabfb8de4128b28d73681020"`) + await db.query(`DROP TABLE "curator"."comment_reaction"`) + await db.query(`DROP INDEX "curator"."IDX_15080d9fb7cf8b563103dd9d90"`) + await db.query(`DROP INDEX "curator"."IDX_962582f04d3f639e33f43c54bb"`) + await db.query(`DROP INDEX "curator"."IDX_d7995b1d57614a6fbd0c103874"`) + await db.query(`DROP TABLE "curator"."comment"`) + await db.query(`DROP INDEX "curator"."IDX_3ce66469b26697baa097f8da92"`) + await db.query(`DROP INDEX "curator"."IDX_1ff03403fd31dfeaba0623a89c"`) + await db.query(`DROP INDEX "curator"."IDX_c3c2abe750c76c7c8e305f71f2"`) + await db.query(`DROP INDEX "curator"."IDX_ac69bddf8202b7c0752d9dc8f3"`) + await db.query(`DROP TABLE "curator"."video_reaction"`) + await db.query(`DROP INDEX "curator"."IDX_73dda64f53bbc7ec7035d5e7f0"`) + await db.query(`DROP INDEX "curator"."IDX_436a3836eb47acb5e1e3c88dde"`) + await db.query(`DROP TABLE "trailer_video"`) + await db.query(`DROP INDEX "public"."IDX_c73677538ef22a243568edac74"`) + await db.query(`DROP INDEX "public"."IDX_0151a0342b10afcd1933f10656"`) + await db.query(`DROP INDEX "public"."IDX_7eb550061f81d70d7c14b9368a"`) + await db.query(`DROP TABLE "curator"."video"`) + await db.query(`DROP INDEX "curator"."IDX_fe2b4b6aace15f1b6610830846"`) + await db.query(`DROP INDEX "curator"."IDX_81b11ef99a9db9ef1aed040d75"`) + await db.query(`DROP INDEX "curator"."IDX_2a5c61f32e9636ee10821e9a58"`) + await db.query(`DROP INDEX "curator"."IDX_8530d052cc79b420f7ce2b4e09"`) + await db.query(`DROP INDEX "curator"."IDX_57b335fa0a960877caf6d2fc29"`) + await db.query(`DROP INDEX "curator"."IDX_3ec633ae5d0477f512b4ed957d"`) + await db.query(`DROP INDEX "curator"."IDX_2db879ed42e3308fe65e679672"`) + await db.query(`DROP INDEX "curator"."IDX_54f88a7decf7d22fd9bd9fa439"`) + await db.query(`DROP INDEX "curator"."IDX_6c49ad08c44d36d11f77c426e4"`) + await db.query(`DROP INDEX "curator"."IDX_f33816960d690ac836f5d5c28a"`) + await db.query(`DROP TABLE "curator"."owned_nft"`) + await db.query(`DROP INDEX "curator"."IDX_8c7201ed7d4765dcbcc3609356"`) + await db.query(`DROP INDEX "curator"."IDX_466896e39b9ec953f4f2545622"`) + await db.query(`DROP TABLE "storage_bucket_operator_metadata"`) + await db.query(`DROP INDEX "public"."IDX_7beffc9530b3f307bc1169cb52"`) + await db.query(`DROP TABLE "distribution_bucket_family_metadata"`) + await db.query(`DROP INDEX "public"."IDX_dd93ca0ea24f3e7a02f11c4c14"`) + await db.query(`DROP INDEX "public"."IDX_5510d3b244a63d6ec702faa426"`) + await db.query(`DROP TABLE "distribution_bucket_operator_metadata"`) + await db.query(`DROP INDEX "public"."IDX_69ec9bdc975b95f7dff94a7106"`) + await db.query(`DROP TABLE "curator"."marketplace_token"`) + await db.query(`DROP INDEX "curator"."IDX_1268fd020cf195b2e8d5d85093"`) + await db.query(`DROP INDEX "curator"."IDX_b99bb1ecee77f23016f6ef687c"`) + await db.query(`DROP TABLE "token_channel"`) + await db.query(`DROP INDEX "public"."IDX_7105aa65a2d333bb2f66db129e"`) + await db.query(`DROP INDEX "public"."IDX_b065bc433d65b0a6874073ea54"`) + await db.query(`DROP INDEX "public"."IDX_f13351e59524e009f99612af11"`) + await db.query(`DROP TABLE "vested_sale"`) + await db.query(`DROP INDEX "public"."IDX_4b0d0d4f6a5ce72247ffe22324"`) + await db.query(`DROP INDEX "public"."IDX_ffa4428b95fc1c0e4df5b5f495"`) + await db.query(`DROP INDEX "public"."IDX_b2135c373c44a37e4e6842ead5"`) + await db.query(`DROP TABLE "curator"."event"`) + await db.query(`DROP INDEX "curator"."IDX_8f3f220c4e717207d841d4e6d4"`) + await db.query(`DROP INDEX "curator"."IDX_2c15918ff289396205521c5f3c"`) + await db.query(`DROP TABLE "curator"."nft_history_entry"`) + await db.query(`DROP INDEX "curator"."IDX_57f51d35ecab042478fe2e31c1"`) + await db.query(`DROP INDEX "curator"."IDX_d1a28b178f5d028d048d40ce20"`) + await db.query(`DROP TABLE "curator"."nft_activity"`) + await db.query(`DROP INDEX "curator"."IDX_18a65713a9fd0715c7a980f5d5"`) + await db.query(`DROP INDEX "curator"."IDX_94d325a753f2c08fdd416eb095"`) + await db.query(`DROP TABLE "curator"."user_interaction_count"`) + await db.query(`DROP INDEX "curator"."IDX_b5261af5f3fe48d77086ebc602"`) + await db.query(`DROP TABLE "member_metadata"`) + await db.query(`DROP INDEX "public"."IDX_e7e4d350f82ae2383894f465ed"`) + await db.query(`DROP TABLE "curator_group"`) + await db.query(`DROP TABLE "curator"`) + await db.query(`DROP TABLE "curator"."channel_follow"`) + await db.query(`DROP INDEX "curator"."IDX_822778b4b1ea8e3b60b127cb8b"`) + await db.query(`DROP INDEX "curator"."IDX_9bc0651dda94437ec18764a260"`) + await db.query(`DROP TABLE "curator"."report"`) + await db.query(`DROP INDEX "curator"."IDX_c6686efa4cd49fa9a429f01bac"`) + await db.query(`DROP INDEX "curator"."IDX_893057921f4b5cc37a0ef36684"`) + await db.query(`DROP INDEX "curator"."IDX_f732b6f82095a935db68c9491f"`) + await db.query(`DROP TABLE "curator"."nft_featuring_request"`) + await db.query(`DROP INDEX "curator"."IDX_519be2a41216c278c35f254dcb"`) + await db.query(`DROP INDEX "curator"."IDX_76d87e26cce72ac2e7ffa28dfb"`) + await db.query(`DROP TABLE "admin"."user"`) + await db.query(`DROP TABLE "curator"."video_view_event"`) + await db.query(`DROP INDEX "curator"."IDX_2e29fba63e12a2b1818e0782d7"`) + await db.query(`DROP INDEX "curator"."IDX_31e1e798ec387ad905cf98d33b"`) + await db.query(`DROP TABLE "admin"."gateway_config"`) + await db.query(`DROP TABLE "admin"."account"`) + await db.query(`DROP INDEX "admin"."IDX_efef1e5fdbe318a379c06678c5"`) + await db.query(`DROP INDEX "admin"."IDX_4c8f96ccf523e9a3faefd5bdd4"`) + await db.query(`DROP INDEX "admin"."IDX_601b93655bcbe73cb58d8c80cd"`) + await db.query(`DROP INDEX "admin"."IDX_df4da05a7a80c1afd18b8f0990"`) + await db.query(`DROP TABLE "notification"`) + await db.query(`DROP INDEX "public"."IDX_6bfa96ab97f1a09d73091294ef"`) + await db.query(`DROP INDEX "public"."IDX_122be1f0696e0255acf95f9e33"`) + await db.query(`DROP TABLE "admin"."email_delivery_attempt"`) + await db.query(`DROP INDEX "admin"."IDX_f985b9b362249af72cac0f52a3"`) + await db.query(`DROP TABLE "admin"."notification_email_delivery"`) + await db.query(`DROP INDEX "admin"."IDX_3b756627c3146db150d66d1292"`) + await db.query(`DROP TABLE "curator"."video_hero"`) + await db.query(`DROP INDEX "curator"."IDX_9feac5d9713a9f07e32eb8ba7a"`) + await db.query(`DROP TABLE "curator"."video_media_encoding"`) + await db.query(`DROP TABLE "curator"."video_media_metadata"`) + await db.query(`DROP INDEX "curator"."IDX_5944dc5896cb16bd395414a0ce"`) + await db.query(`DROP INDEX "curator"."IDX_4dc101240e8e1536b770aee202"`) + await db.query(`DROP TABLE "encryption_artifacts"`) + await db.query(`DROP INDEX "public"."IDX_ec8f68a544aadc4fbdadefe4a0"`) + await db.query(`DROP TABLE "admin"."session"`) + await db.query(`DROP INDEX "admin"."IDX_30e98e8746699fb9af235410af"`) + await db.query(`DROP INDEX "admin"."IDX_fae5a6b4a57f098e9af8520d49"`) + await db.query(`DROP INDEX "admin"."IDX_213b5a19bfdbe0ab6e06b1dede"`) + await db.query(`DROP TABLE "session_encryption_artifacts"`) + await db.query(`DROP INDEX "public"."IDX_3612880efd8926a17eba5ab0e1"`) + await db.query(`DROP TABLE "admin"."token"`) + await db.query(`DROP INDEX "admin"."IDX_a6fe18c105f85a63d761ccb078"`) + await db.query(`DROP TABLE "next_entity_id"`) + await db.query(`DROP TABLE "admin"."orion_offchain_cursor"`) + await db.query(`ALTER TABLE "curator"."bid" DROP CONSTRAINT "FK_9e594e5a61c0f3cb25679f6ba8d"`) + await db.query(`ALTER TABLE "curator"."bid" DROP CONSTRAINT "FK_3caf2d6b31d2fe45a2b85b81912"`) + await db.query(`ALTER TABLE "curator"."bid" DROP CONSTRAINT "FK_e7618559409a903a897164156b7"`) + await db.query(`ALTER TABLE "curator"."bid" DROP CONSTRAINT "FK_32cb73025ec49c87f4c594a265f"`) + await db.query(`ALTER TABLE "curator"."auction" DROP CONSTRAINT "FK_cfb47e97e60c9d1462576f85a88"`) + await db.query(`ALTER TABLE "curator"."auction" DROP CONSTRAINT "FK_a3127ec87cccc5696b92cac4e09"`) + await db.query(`ALTER TABLE "curator"."auction" DROP CONSTRAINT "FK_1673ad4b059742fbabfc40b275c"`) + await db.query(`ALTER TABLE "auction_whitelisted_member" DROP CONSTRAINT "FK_aad797677bc7c7c7dc1f1d397f5"`) + await db.query(`ALTER TABLE "auction_whitelisted_member" DROP CONSTRAINT "FK_d5ae4854487c7658b64225be305"`) + await db.query(`ALTER TABLE "curator"."banned_member" DROP CONSTRAINT "FK_b94ea874da235d9b6fbc35cf58e"`) + await db.query(`ALTER TABLE "curator"."banned_member" DROP CONSTRAINT "FK_ed36c6c26bf5410796c2fc21f74"`) + await db.query(`ALTER TABLE "sale_transaction" DROP CONSTRAINT "FK_7c477ad14796b65a8e47214adc9"`) + await db.query(`ALTER TABLE "sale_transaction" DROP CONSTRAINT "FK_cfd7aa41d364144e6bbf677a488"`) + await db.query(`ALTER TABLE "sale" DROP CONSTRAINT "FK_53aae73a92bcdefd80d4bb94e7f"`) + await db.query(`ALTER TABLE "sale" DROP CONSTRAINT "FK_00468ff9c85265853384de0e1dd"`) + await db.query(`ALTER TABLE "revenue_share_participation" DROP CONSTRAINT "FK_7549dc863632b065f111a532551"`) + await db.query(`ALTER TABLE "revenue_share_participation" DROP CONSTRAINT "FK_018b86600c0c1228ada1c928be7"`) + await db.query(`ALTER TABLE "revenue_share" DROP CONSTRAINT "FK_4e8bfc2037cececc86ba5192ea9"`) + await db.query(`ALTER TABLE "amm_transaction" DROP CONSTRAINT "FK_135d9555d7ea3e45dd78d8aedec"`) + await db.query(`ALTER TABLE "amm_transaction" DROP CONSTRAINT "FK_51f006dbc040d62dc479adbee78"`) + await db.query(`ALTER TABLE "amm_curve" DROP CONSTRAINT "FK_97bee00638822978784362d19fc"`) + await db.query(`ALTER TABLE "benefit" DROP CONSTRAINT "FK_b484e2182fc7a1910e84a5ae7ad"`) + await db.query(`ALTER TABLE "creator_token" DROP CONSTRAINT "FK_aabe40376c0eb47772b52780b19"`) + await db.query(`ALTER TABLE "creator_token" DROP CONSTRAINT "FK_5eca884f8728ff8f0c6a389c24b"`) + await db.query(`ALTER TABLE "creator_token" DROP CONSTRAINT "FK_df8c309ef364e49b9d2f17dc778"`) + await db.query(`ALTER TABLE "vested_account" DROP CONSTRAINT "FK_745ee4e6a2dfd5de65fb8b9f44a"`) + await db.query(`ALTER TABLE "vested_account" DROP CONSTRAINT "FK_6a0600f53023dca2c43b99a0974"`) + await db.query(`ALTER TABLE "token_account" DROP CONSTRAINT "FK_dc32b6b2efa86183e6329909e73"`) + await db.query(`ALTER TABLE "token_account" DROP CONSTRAINT "FK_02862fa18dececb99dd81a6a6a9"`) + await db.query(`ALTER TABLE "storage_bucket_bag" DROP CONSTRAINT "FK_791e2f82e3919ffcef8712aa1b9"`) + await db.query(`ALTER TABLE "storage_bucket_bag" DROP CONSTRAINT "FK_aaf00b2c7d0cba49f97da14fbba"`) + await db.query(`ALTER TABLE "distribution_bucket_operator" DROP CONSTRAINT "FK_678dc5427cdde0cd4fef2c07a43"`) + await db.query(`ALTER TABLE "distribution_bucket" DROP CONSTRAINT "FK_8cb7454d1ec34b0d3bb7ecdee4e"`) + await db.query(`ALTER TABLE "distribution_bucket_bag" DROP CONSTRAINT "FK_8a807921f1aae60d4ba94895826"`) + await db.query(`ALTER TABLE "distribution_bucket_bag" DROP CONSTRAINT "FK_a9810100aee7584680f197c8ff0"`) + await db.query(`ALTER TABLE "curator"."storage_data_object" DROP CONSTRAINT "FK_ff8014300b8039dbaed764f51bc"`) + await db.query(`ALTER TABLE "app" DROP CONSTRAINT "FK_c9cc395bbc485f70a15be64553e"`) + await db.query(`ALTER TABLE "curator"."channel" DROP CONSTRAINT "FK_25c85bc448b5e236a4c1a5f7895"`) + await db.query(`ALTER TABLE "curator"."channel" DROP CONSTRAINT "FK_a77e12f3d8c6ced020e179a5e94"`) + await db.query(`ALTER TABLE "curator"."channel" DROP CONSTRAINT "FK_6997e94413b3f2f25a84e4a96f8"`) + await db.query(`ALTER TABLE "curator"."channel" DROP CONSTRAINT "FK_118ecfa0199aeb5a014906933e8"`) + await db.query(`ALTER TABLE "curator"."video_featured_in_category" DROP CONSTRAINT "FK_7b16ddad43901921a8d3c8eab71"`) + await db.query(`ALTER TABLE "curator"."video_featured_in_category" DROP CONSTRAINT "FK_0e6bb49ce9d022cd872f3ab4288"`) + await db.query(`ALTER TABLE "curator"."video_category" DROP CONSTRAINT "FK_da26b34f037c0d59d3c0d0646e9"`) + await db.query(`ALTER TABLE "curator"."video_subtitle" DROP CONSTRAINT "FK_2203674f18d8052ed6bac396252"`) + await db.query(`ALTER TABLE "curator"."video_subtitle" DROP CONSTRAINT "FK_b6eabfb8de4128b28d73681020f"`) + await db.query(`ALTER TABLE "curator"."comment_reaction" DROP CONSTRAINT "FK_15080d9fb7cf8b563103dd9d900"`) + await db.query(`ALTER TABLE "curator"."comment_reaction" DROP CONSTRAINT "FK_962582f04d3f639e33f43c54bbc"`) + await db.query(`ALTER TABLE "curator"."comment_reaction" DROP CONSTRAINT "FK_d7995b1d57614a6fbd0c103874d"`) + await db.query(`ALTER TABLE "curator"."comment" DROP CONSTRAINT "FK_3ce66469b26697baa097f8da923"`) + await db.query(`ALTER TABLE "curator"."comment" DROP CONSTRAINT "FK_1ff03403fd31dfeaba0623a89cf"`) + await db.query(`ALTER TABLE "curator"."comment" DROP CONSTRAINT "FK_ac69bddf8202b7c0752d9dc8f32"`) + await db.query(`ALTER TABLE "curator"."video_reaction" DROP CONSTRAINT "FK_73dda64f53bbc7ec7035d5e7f09"`) + await db.query(`ALTER TABLE "curator"."video_reaction" DROP CONSTRAINT "FK_436a3836eb47acb5e1e3c88ddea"`) + await db.query(`ALTER TABLE "trailer_video" DROP CONSTRAINT "FK_c73677538ef22a243568edac74b"`) + await db.query(`ALTER TABLE "trailer_video" DROP CONSTRAINT "FK_0151a0342b10afcd1933f106564"`) + await db.query(`ALTER TABLE "curator"."video" DROP CONSTRAINT "FK_81b11ef99a9db9ef1aed040d750"`) + await db.query(`ALTER TABLE "curator"."video" DROP CONSTRAINT "FK_2a5c61f32e9636ee10821e9a58d"`) + await db.query(`ALTER TABLE "curator"."video" DROP CONSTRAINT "FK_8530d052cc79b420f7ce2b4e09d"`) + await db.query(`ALTER TABLE "curator"."video" DROP CONSTRAINT "FK_3ec633ae5d0477f512b4ed957d6"`) + await db.query(`ALTER TABLE "curator"."video" DROP CONSTRAINT "FK_2db879ed42e3308fe65e6796729"`) + await db.query(`ALTER TABLE "curator"."video" DROP CONSTRAINT "FK_54f88a7decf7d22fd9bd9fa439a"`) + await db.query(`ALTER TABLE "curator"."video" DROP CONSTRAINT "FK_6c49ad08c44d36d11f77c426e43"`) + await db.query(`ALTER TABLE "curator"."owned_nft" DROP CONSTRAINT "FK_466896e39b9ec953f4f2545622d"`) + await db.query(`ALTER TABLE "storage_bucket_operator_metadata" DROP CONSTRAINT "FK_7beffc9530b3f307bc1169cb524"`) + await db.query(`ALTER TABLE "distribution_bucket_family_metadata" DROP CONSTRAINT "FK_dd93ca0ea24f3e7a02f11c4c149"`) + await db.query(`ALTER TABLE "distribution_bucket_operator_metadata" DROP CONSTRAINT "FK_69ec9bdc975b95f7dff94a71069"`) + await db.query(`ALTER TABLE "token_channel" DROP CONSTRAINT "FK_7105aa65a2d333bb2f66db129e9"`) + await db.query(`ALTER TABLE "token_channel" DROP CONSTRAINT "FK_b065bc433d65b0a6874073ea540"`) + await db.query(`ALTER TABLE "vested_sale" DROP CONSTRAINT "FK_4b0d0d4f6a5ce72247ffe223240"`) + await db.query(`ALTER TABLE "vested_sale" DROP CONSTRAINT "FK_ffa4428b95fc1c0e4df5b5f4952"`) + await db.query(`ALTER TABLE "curator"."nft_history_entry" DROP CONSTRAINT "FK_57f51d35ecab042478fe2e31c19"`) + await db.query(`ALTER TABLE "curator"."nft_history_entry" DROP CONSTRAINT "FK_d1a28b178f5d028d048d40ce208"`) + await db.query(`ALTER TABLE "curator"."nft_activity" DROP CONSTRAINT "FK_18a65713a9fd0715c7a980f5d54"`) + await db.query(`ALTER TABLE "curator"."nft_activity" DROP CONSTRAINT "FK_94d325a753f2c08fdd416eb095f"`) + await db.query(`ALTER TABLE "member_metadata" DROP CONSTRAINT "FK_e7e4d350f82ae2383894f465ede"`) + await db.query(`ALTER TABLE "curator"."channel_follow" DROP CONSTRAINT "FK_822778b4b1ea8e3b60b127cb8b1"`) + await db.query(`ALTER TABLE "curator"."report" DROP CONSTRAINT "FK_c6686efa4cd49fa9a429f01bac8"`) + await db.query(`ALTER TABLE "curator"."nft_featuring_request" DROP CONSTRAINT "FK_519be2a41216c278c35f254dcba"`) + await db.query(`ALTER TABLE "curator"."video_view_event" DROP CONSTRAINT "FK_31e1e798ec387ad905cf98d33b0"`) + await db.query(`ALTER TABLE "admin"."account" DROP CONSTRAINT "FK_efef1e5fdbe318a379c06678c51"`) + await db.query(`ALTER TABLE "admin"."account" DROP CONSTRAINT "FK_601b93655bcbe73cb58d8c80cd3"`) + await db.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_6bfa96ab97f1a09d73091294efc"`) + await db.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_122be1f0696e0255acf95f9e336"`) + await db.query(`ALTER TABLE "admin"."email_delivery_attempt" DROP CONSTRAINT "FK_f985b9b362249af72cac0f52a3b"`) + await db.query(`ALTER TABLE "admin"."notification_email_delivery" DROP CONSTRAINT "FK_3b756627c3146db150d66d12929"`) + await db.query(`ALTER TABLE "curator"."video_hero" DROP CONSTRAINT "FK_9feac5d9713a9f07e32eb8ba7a1"`) + await db.query(`ALTER TABLE "curator"."video_media_metadata" DROP CONSTRAINT "FK_5944dc5896cb16bd395414a0ce0"`) + await db.query(`ALTER TABLE "curator"."video_media_metadata" DROP CONSTRAINT "FK_4dc101240e8e1536b770aee202a"`) + await db.query(`ALTER TABLE "encryption_artifacts" DROP CONSTRAINT "FK_ec8f68a544aadc4fbdadefe4a0a"`) + await db.query(`ALTER TABLE "admin"."session" DROP CONSTRAINT "FK_30e98e8746699fb9af235410aff"`) + await db.query(`ALTER TABLE "admin"."session" DROP CONSTRAINT "FK_fae5a6b4a57f098e9af8520d499"`) + await db.query(`ALTER TABLE "session_encryption_artifacts" DROP CONSTRAINT "FK_3612880efd8926a17eba5ab0e1a"`) + await db.query(`ALTER TABLE "admin"."token" DROP CONSTRAINT "FK_a6fe18c105f85a63d761ccb0780"`) + } +} diff --git a/db/migrations/1749123353715-Views.js b/db/migrations/1749123353715-Views.js new file mode 100644 index 000000000..37256f531 --- /dev/null +++ b/db/migrations/1749123353715-Views.js @@ -0,0 +1,21 @@ + +const { createViews } = require('../../lib/model/views') + +module.exports = class Views1749123353715 { + name = 'Views1749123353715' + + async up(db) { + // these two queries will be invoked and the cleaned up by the squid itself + // we only do this to be able to reference processor height in mappings + await db.query(`CREATE SCHEMA IF NOT EXISTS squid_processor;`) + await db.query(`CREATE TABLE IF NOT EXISTS squid_processor.status ( + id SERIAL PRIMARY KEY, + height INT + );`) + await createViews(db); + } + + async down(db) { + await dropViews(db); + } +} diff --git a/db/migrations/2300000000000-Operator.js b/db/migrations/2000000000000-Operator.js similarity index 100% rename from db/migrations/2300000000000-Operator.js rename to db/migrations/2000000000000-Operator.js diff --git a/db/migrations/2200000000000-Indexes.js b/db/migrations/2200000000000-Indexes.js deleted file mode 100644 index 652d1a957..000000000 --- a/db/migrations/2200000000000-Indexes.js +++ /dev/null @@ -1,76 +0,0 @@ -module.exports = class Indexes2200000000000 { - name = 'Indexes2200000000000' - - async up(db) { - await db.query( - `CREATE INDEX "events_video" ON "admin"."event" USING BTREE (("data"->>'video'));` - ) - await db.query( - `CREATE INDEX "events_comment" ON "admin"."event" USING BTREE (("data"->>'comment'));` - ) - await db.query( - `CREATE INDEX "events_nft_owner_member" ON "admin"."event" USING BTREE (("data"->'nftOwner'->>'member'));` - ) - await db.query( - `CREATE INDEX "events_nft_owner_channel" ON "admin"."event" USING BTREE (("data"->'nftOwner'->>'channel'));` - ) - await db.query( - `CREATE INDEX "events_auction" ON "admin"."event" USING BTREE (("data"->>'auction'));` - ) - await db.query( - `CREATE INDEX "events_type" ON "admin"."event" USING BTREE (("data"->>'isTypeOf'));` - ) - await db.query(`CREATE INDEX "events_nft" ON "admin"."event" USING BTREE (("data"->>'nft'));`) - await db.query(`CREATE INDEX "events_bid" ON "admin"."event" USING BTREE (("data"->>'bid'));`) - await db.query( - `CREATE INDEX "events_member" ON "admin"."event" USING BTREE (("data"->>'member'));` - ) - await db.query( - `CREATE INDEX "events_winning_bid" ON "admin"."event" USING BTREE (("data"->>'winningBid'));` - ) - await db.query( - `CREATE INDEX "events_previous_nft_owner_member" ON "admin"."event" USING BTREE (("data"->'previousNftOwner'->>'member'));` - ) - await db.query( - `CREATE INDEX "events_previous_nft_owner_channel" ON "admin"."event" USING BTREE (("data"->'previousNftOwner'->>'channel'));` - ) - await db.query( - `CREATE INDEX "events_buyer" ON "admin"."event" USING BTREE (("data"->>'buyer'));` - ) - await db.query( - `CREATE INDEX "auction_type" ON "admin"."auction" USING BTREE (("auction_type"->>'isTypeOf'));` - ) - await db.query( - `CREATE INDEX "member_metadata_avatar" ON "member_metadata" USING BTREE (("avatar"->>'avatarObject'));` - ) - await db.query( - `CREATE INDEX "owned_nft_auction" ON "admin"."owned_nft" USING BTREE (("transactional_status"->>'auction'));` - ) - await db.query( - `CREATE INDEX video_include_in_home_feed_idx ON admin.video (include_in_home_feed) WHERE include_in_home_feed = true;` - ) - await db.query( - `CREATE INDEX idx_video_is_short_false ON admin.video (is_short) WHERE is_short = FALSE;` - ) - await db.query( - `CREATE INDEX idx_video_is_short_derived_false ON admin.video (is_short_derived) WHERE is_short_derived = FALSE;` - ) - } - - async down(db) { - await db.query(`DROP INDEX "events_video"`) - await db.query(`DROP INDEX "events_comment"`) - await db.query(`DROP INDEX "events_nft_owner_member"`) - await db.query(`DROP INDEX "events_nft_owner_channel"`) - await db.query(`DROP INDEX "events_auction"`) - await db.query(`DROP INDEX "events_type"`) - await db.query(`DROP INDEX "events_nft"`) - await db.query(`DROP INDEX "events_bid"`) - await db.query(`DROP INDEX "events_member"`) - await db.query(`DROP INDEX "events_winning_bid"`) - await db.query(`DROP INDEX "events_previous_nft_owner_member"`) - await db.query(`DROP INDEX "events_previous_nft_owner_channel"`) - await db.query(`DROP INDEX "events_buyer"`) - await db.query(`DROP INDEX "video_include_in_home_feed_idx"`) - } -} diff --git a/db/viewDefinitions.js b/db/viewDefinitions.js deleted file mode 100644 index af05d5f12..000000000 --- a/db/viewDefinitions.js +++ /dev/null @@ -1,156 +0,0 @@ -const { withPriceChange } = require('../lib/server-extension/resolvers/CreatorToken/utils') - -const noCategoryVideosSupportedByDefault = - process.env.SUPPORT_NO_CATEGORY_VIDEOS === 'true' || - process.env.SUPPORT_NO_CATEGORY_VIDEOS === '1' - -const BLOCKS_PER_DAY = 10 * 60 * 24 // 10 blocs per minute, 60 mins * 24 hours - -// Add public 'VIEW' definitions for hidden entities created by -// applying `@schema(name: "admin") directive to the Graphql entities -function getViewDefinitions(db) { - return { - channel: [`is_excluded='0'`, `is_censored='0'`], - banned_member: [`EXISTS(SELECT 1 FROM "channel" WHERE "id"="channel_id")`], - video: [ - `is_excluded='0'`, - `is_censored='0'`, - `EXISTS(SELECT 1 FROM "channel" WHERE "id"="channel_id")`, - `EXISTS(SELECT 1 FROM "admin"."video_category" WHERE "id"="category_id" AND "is_supported"='1') - OR ( - "category_id" IS NULL - AND COALESCE( - (SELECT "value" FROM "admin"."gateway_config" WHERE "id"='SUPPORT_NO_CATEGORY_VIDEOS'), - ${noCategoryVideosSupportedByDefault ? "'1'" : "'0'"} - )='1' - )`, - ], - video_category: [`"is_supported" = '1'`], - owned_nft: [`EXISTS(SELECT 1 FROM "video" WHERE "id"="video_id")`], - auction: [`EXISTS(SELECT 1 FROM "owned_nft" WHERE "id"="nft_id")`], - bid: [`EXISTS(SELECT 1 FROM "owned_nft" WHERE "id"="nft_id")`], - comment: ` - SELECT - ${db.connection - .getMetadata('Comment') - .columns.filter((c) => c.databaseName !== 'text') - .map((c) => `"${c.databaseName}"`) - .join(',')}, - CASE WHEN "is_excluded" = '1' THEN '' ELSE "comment"."text" END as "text" - FROM - "admin"."comment" - WHERE EXISTS(SELECT 1 FROM "video" WHERE "id"="video_id") - `, - comment_reaction: [`EXISTS(SELECT 1 FROM "comment" WHERE "id"="comment_id")`], - license: [`EXISTS(SELECT 1 FROM "video" WHERE "license_id"="this"."id")`], - video_media_metadata: [`EXISTS(SELECT 1 FROM "video" WHERE "id"="video_id")`], - video_media_encoding: [ - `EXISTS(SELECT 1 FROM "video_media_metadata" WHERE "encoding_id"="this"."id")`, - ], - video_reaction: [`EXISTS(SELECT 1 FROM "video" WHERE "id"="video_id")`], - video_subtitle: [`EXISTS(SELECT 1 FROM "video" WHERE "id"="video_id")`], - video_featured_in_category: [ - `EXISTS(SELECT 1 FROM "video" WHERE "id"="video_id")`, - `EXISTS(SELECT 1 FROM "video_category" WHERE "id"="category_id")`, - ], - video_hero: [`EXISTS(SELECT 1 FROM "video" WHERE "id"="video_id")`], - // TODO: Consider all events having ref to a video they're related to - this will make filtering much easier - event: [ - `("data"->>'channel' IS NULL OR EXISTS(SELECT 1 FROM "channel" WHERE "id"="data"->>'channel'))`, - `("data"->>'video' IS NULL OR EXISTS(SELECT 1 FROM "video" WHERE "id"="data"->>'video'))`, - `("data"->>'nft' IS NULL OR EXISTS(SELECT 1 FROM "owned_nft" WHERE "id"="data"->>'nft'))`, - `("data"->>'auction' IS NULL OR EXISTS(SELECT 1 FROM "auction" WHERE "id"="data"->>'auction'))`, - `("data"->>'bid' IS NULL OR EXISTS(SELECT 1 FROM "bid" WHERE "id"="data"->>'bid'))`, - `("data"->>'winningBid' IS NULL OR EXISTS(SELECT 1 FROM "bid" WHERE "id"="data"->>'winningBid'))`, - `("data"->>'comment' IS NULL OR EXISTS(SELECT 1 FROM "comment" WHERE "id"="data"->>'comment'))`, - ], - storage_data_object: [ - `("type"->>'channel' IS NULL OR EXISTS(SELECT 1 FROM "channel" WHERE "id"="type"->>'channel'))`, - `("type"->>'video' IS NULL OR EXISTS(SELECT 1 FROM "video" WHERE "id"="type"->>'video'))`, - ], - nft_history_entry: [`EXISTS(SELECT 1 FROM "event" WHERE "id"="event_id")`], - nft_activity: [`EXISTS(SELECT 1 FROM "event" WHERE "id"="event_id")`], - // *** HIDDEN entities *** - // Even though the following entities are hidden by default (because they are part of "admin" schema) - // we still define these in the views definitions to create their VIEW in the public schema as they are - // exposed by the GRAPHQL API, so that when querying the GRAPHQL API, the response is just empty object - // instead of `"relation does not exist"` error. - video_view_event: ['FALSE'], - channel_follow: ['FALSE'], - report: ['FALSE'], - exclusion: ['FALSE'], - session: ['FALSE'], - notification_email_delivery: ['FALSE'], - channel_verification: ['FALSE'], - channel_suspension: ['FALSE'], - user: ['FALSE'], - account: ['FALSE'], - token: ['FALSE'], - nft_featuring_request: ['FALSE'], - gateway_config: ['FALSE'], - email_delivery_attempt: ['FALSE'], - // TODO (notifications v2): make this part of the admin schema with appropriate resolver for queries - // notification: ['FALSE'], - marketplace_token: ` - WITH - last_block AS ( - SELECT height FROM squid_processor.status - ), - tokens_with_stats AS ( - SELECT - ac.token_id, - SUM(CASE - WHEN ( - transaction_type = 'BUY' - AND tr.created_in < last_block.height - ${BLOCKS_PER_DAY * 30} - ) THEN quantity - WHEN ( - transaction_type = 'SELL' - AND tr.created_in < last_block.height - ${BLOCKS_PER_DAY * 30} - ) THEN quantity * -1 - ELSE 0 - END) AS total_liquidity_30d_ago, - SUM (CASE - WHEN transaction_type = 'BUY' THEN quantity - ELSE quantity * -1 - END) AS total_liquidity, - SUM(tr.price_paid) as amm_volume - FROM amm_transaction tr - JOIN amm_curve ac ON ac.id = tr.amm_id - JOIN creator_token ct ON ct.current_amm_sale_id = ac.id - JOIN last_block ON 1=1 - GROUP BY token_id - ), - ${withPriceChange({ periodDays: 30, currentBlock: 'last_block' })} - SELECT - COALESCE(tws.total_liquidity, 0) as liquidity, - CASE - WHEN tws.amm_volume >= COALESCE( - market_cap_min_volume_cfg.value::int8, - ${parseInt(process.env.CRT_MARKET_CAP_MIN_VOLUME_JOY)} - ) THEN (ct.last_price * ct.total_supply) - ELSE 0 - END as market_cap, - c.cumulative_revenue, - c.id as channel_id, - COALESCE(tws.amm_volume, 0) as amm_volume, - COALESCE(twpc.percentage_change, 0) price_change, - CASE - WHEN (tws.total_liquidity_30d_ago IS NULL OR tws.total_liquidity_30d_ago = 0) THEN 0 - ELSE ( - (tws.total_liquidity - tws.total_liquidity_30d_ago) - * 100 - / tws.total_liquidity_30d_ago - ) - END as liquidity_change, - ct.* - FROM creator_token ct - LEFT JOIN token_channel tc ON tc.token_id = ct.id - LEFT JOIN channel c ON c.id = tc.channel_id - LEFT JOIN tokens_with_price_change twpc ON twpc.token_id = ct.id - LEFT JOIN tokens_with_stats tws ON tws.token_id = ct.id - LEFT JOIN "admin"."gateway_config" market_cap_min_volume_cfg ON market_cap_min_volume_cfg.id = 'CRT_MARKET_CAP_MIN_VOLUME_JOY'`, - } -} - -module.exports = { getViewDefinitions } diff --git a/docker-compose.yml b/docker-compose.yml index 87f638ad6..3fc1023a4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -33,6 +33,8 @@ services: env_file: - .env - docker.env + environment: + - TYPEORM_SUBSCRIBERS_DIR=lib/mappings/subscribers ports: - '127.0.0.1:${PROCESSOR_PROMETHEUS_PORT}:${PROCESSOR_PROMETHEUS_PORT}' - '[::1]:${PROCESSOR_PROMETHEUS_PORT}:${PROCESSOR_PROMETHEUS_PORT}' @@ -45,6 +47,34 @@ services: working_dir: /orion command: ['make', 'process'] + orion_relevance-service: + container_name: orion_relevance-service + image: node:18 + restart: unless-stopped + networks: + - joystream_default + depends_on: + - orion_rabbitmq + env_file: + - .env + - docker.env + volumes: + - type: bind + source: . + target: /orion + working_dir: /orion + command: ['make', 'relevance-service'] + + orion_rabbitmq: + container_name: orion_rabbitmq + hostname: orion_rabbitmq + image: rabbitmq:4.1 + networks: + - joystream_default + ports: + - '127.0.0.1:${RABBITMQ_PORT}:${RABBITMQ_PORT}' + - '[::1]:${RABBITMQ_PORT}:${RABBITMQ_PORT}' + orion_graphql-server: container_name: orion_graphql-server hostname: orion_graphql-server @@ -58,6 +88,7 @@ services: environment: - SQD_TRACE=authentication - OTEL_EXPORTER_OTLP_ENDPOINT=${TELEMETRY_ENDPOINT} + - TYPEORM_SUBSCRIBERS_DIR=lib/server-extension/subscribers depends_on: - orion_db volumes: diff --git a/package-lock.json b/package-lock.json index 23a38f00d..55bbddd4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "orion", - "version": "4.5.0", + "version": "5.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "orion", - "version": "4.5.0", + "version": "5.0.0", "hasInstallScript": true, "workspaces": [ "network-tests" @@ -35,6 +35,7 @@ "@types/node-schedule": "^2.1.0", "@typescript/analyze-trace": "^0.9.1", "ajv": "^6.11.0", + "amqplib": "^0.10.8", "async-lock": "^1.3.1", "axios": "^1.2.1", "big-json": "^3.2.0", @@ -74,6 +75,7 @@ "@subsquid/substrate-metadata-explorer": "^1.0.9", "@subsquid/substrate-typegen": "^2.1.0", "@subsquid/typeorm-codegen": "0.3.1", + "@types/amqplib": "^0.10.7", "@types/async-lock": "^1.1.3", "@types/big-json": "^3.2.4", "@types/chai": "^4.3.11", @@ -9657,6 +9659,15 @@ "@types/node": "*" } }, + "node_modules/@types/amqplib": { + "version": "0.10.7", + "resolved": "https://registry.npmjs.org/@types/amqplib/-/amqplib-0.10.7.tgz", + "integrity": "sha512-IVj3avf9AQd2nXCx0PGk/OYq7VmHiyNxWFSb5HhU9ATh+i+gHWvVcljFTcTWQ/dyHJCTrzCixde+r/asL2ErDA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/async-lock": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/@types/async-lock/-/async-lock-1.4.2.tgz", @@ -10903,6 +10914,18 @@ "amp": "0.3.1" } }, + "node_modules/amqplib": { + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.10.8.tgz", + "integrity": "sha512-Tfn1O9sFgAP8DqeMEpt2IacsVTENBpblB3SqLdn0jK2AeX8iyCvbptBc8lyATT9bQ31MsjVwUSQ1g8f4jHOUfw==", + "dependencies": { + "buffer-more-ints": "~1.0.0", + "url-parse": "~1.5.10" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/ansi-color": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/ansi-color/-/ansi-color-0.2.1.tgz", @@ -12120,6 +12143,11 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, + "node_modules/buffer-more-ints": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz", + "integrity": "sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg==" + }, "node_modules/buffer-reverse": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-reverse/-/buffer-reverse-1.0.1.tgz", @@ -24231,6 +24259,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -24589,6 +24622,11 @@ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "dev": true }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, "node_modules/reset": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/reset/-/reset-0.1.0.tgz", @@ -26804,6 +26842,15 @@ "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==" }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/urlpattern-polyfill": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-8.0.2.tgz", diff --git a/package.json b/package.json index 8f1249ed8..8beed4269 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "orion", - "version": "4.5.0", + "version": "5.0.0", "engines": { "node": ">=16" }, @@ -22,6 +22,7 @@ "processor-start": "node -r dotenv-expand/config lib/processor.js", "graphql-server-start": "./entrypoints/graphql-server.sh", "auth-server-start": "./entrypoints/auth-server.sh", + "relevance-service-start": "node -r dotenv-expand/config lib/relevance-service/main.js", "postinstall": "patch-package --patch-dir assets/patches", "mail-scheduler": "npx ts-node ./src/mail-scheduler/index.ts", "tests:codegen": "npx graphql-codegen -c ./src/tests/v1/codegen.yml && npx graphql-codegen -c ./src/tests/v2/codegen.yml", @@ -69,6 +70,7 @@ "@types/node-schedule": "^2.1.0", "@typescript/analyze-trace": "^0.9.1", "ajv": "^6.11.0", + "amqplib": "^0.10.8", "async-lock": "^1.3.1", "axios": "^1.2.1", "big-json": "^3.2.0", @@ -108,6 +110,7 @@ "@subsquid/substrate-metadata-explorer": "^1.0.9", "@subsquid/substrate-typegen": "^2.1.0", "@subsquid/typeorm-codegen": "0.3.1", + "@types/amqplib": "^0.10.7", "@types/async-lock": "^1.1.3", "@types/big-json": "^3.2.4", "@types/chai": "^4.3.11", diff --git a/schema/NFTs.graphql b/schema/NFTs.graphql index 3d398b041..d44d99f39 100644 --- a/schema/NFTs.graphql +++ b/schema/NFTs.graphql @@ -30,7 +30,7 @@ type TransactionalStatusAuction { } "Represents NFT details" -type OwnedNft @entity @schema(name: "admin") { +type OwnedNft @entity @schema(name: "curator") { "Timestamp of the block the NFT was created at" createdAt: DateTime! @index @@ -87,7 +87,7 @@ type AuctionTypeOpen @variant { } "Represents NFT auction" -type Auction @entity @schema(name: "admin") { +type Auction @entity @schema(name: "curator") { "Unique identifier" id: ID! @@ -137,7 +137,7 @@ type AuctionWhitelistedMember @entity @index(fields: ["auction", "member"], uniq } "Represents bid in NFT auction" -type Bid @entity @schema(name: "admin") { +type Bid @entity @schema(name: "curator") { "Unique identifier" id: ID! diff --git a/schema/auth.graphql b/schema/auth.graphql index 055c3cf6d..b55da14f6 100644 --- a/schema/auth.graphql +++ b/schema/auth.graphql @@ -1,8 +1,8 @@ enum OperatorPermission { GRANT_OPERATOR_PERMISSIONS REVOKE_OPERATOR_PERMISSIONS - SET_VIDEO_WEIGHTS - SET_CHANNEL_WEIGHTS + SET_RELEVANCE_WEIGHTS + SET_RELEVANCE_CONFIG SET_KILL_SWITCH SET_VIDEO_VIEW_PER_USER_TIME_LIMIT SET_VIDEO_HERO @@ -15,6 +15,10 @@ enum OperatorPermission { SET_FEATURED_CRTS SET_CRT_MARKETCAP_MIN_VOLUME SET_TIP_TIERS + SET_CHANNEL_YPP_STATUS + SET_APP_CONFIGS + VIEW_CURATOR_SCHEMA + VIEW_ADMIN_SCHEMA } type User @entity @schema(name: "admin") { diff --git a/schema/channels.graphql b/schema/channels.graphql index 4c5584ea2..41871a436 100644 --- a/schema/channels.graphql +++ b/schema/channels.graphql @@ -1,9 +1,9 @@ -type Channel @entity @schema(name: "admin") { +type Channel @entity @schema(name: "curator") { "Runtime entity identifier (EntityId)" id: ID! "Timestamp of the block the channel was created at" - createdAt: DateTime! @index + createdAt: DateTime! "Current member-owner of the channel (if owned by a member)" ownerMember: Membership @@ -30,7 +30,7 @@ type Channel @entity @schema(name: "admin") { isExcluded: Boolean! "The primary langauge of the channel's content" - language: String @index + language: String "List of videos that belong to the channel" videos: [Video!]! @derivedFrom(field: "channel") @@ -77,52 +77,41 @@ type Channel @entity @schema(name: "admin") { "Weight/Bias of the channel affecting video relevance in the Homepage" channelWeight: Float - "Channel Ypp Status: either unverified , verified or suspended" - yppStatus: ChannelYppStatus! + "Channel Ypp Status (if exists): either unverified, verified or suspended" + yppStatus: ChannelYppStatus + + "Whether YouTube sync is enabled for this channel" + isYtSyncEnabled: Boolean! +} + +enum ChannelTier { + BRONZE + SILVER + GOLD + DIAMOND } union ChannelYppStatus = YppUnverified | YppVerified | YppSuspended type YppUnverified { - phantom: Int + timestamp: DateTime! } type YppVerified { - verification: ChannelVerification! + tier: ChannelTier! + timestamp: DateTime! } type YppSuspended { - suspension: ChannelSuspension! + timestamp: DateTime! } type BannedMember @entity - @schema(name: "admin") + @schema(name: "curator") @index(fields: ["member", "channel"], unique: true) { "{memberId}-{channelId}" id: ID! member: Membership! channel: Channel! } - -type ChannelVerification @entity @schema(name: "admin") { - "unique Id" - id: ID! - - "channel verified" - channel: Channel! - - "timestamp of verification" - timestamp: DateTime! -} - -type ChannelSuspension @entity @schema(name: "admin") { - "unique Id" - id: ID! - - "channel suspended" - channel: Channel! - - "timestamp of suspension" - timestamp: DateTime! -} diff --git a/schema/events.graphql b/schema/events.graphql index c4b2dfbcc..6b2a289b6 100644 --- a/schema/events.graphql +++ b/schema/events.graphql @@ -1,4 +1,4 @@ -type Event @entity @schema(name: "admin") { +type Event @entity @schema(name: "curator") { "{blockNumber}-{indexInBlock}" id: ID! @@ -18,7 +18,7 @@ type Event @entity @schema(name: "admin") { data: EventData! } -type NftHistoryEntry @entity @schema(name: "admin") { +type NftHistoryEntry @entity @schema(name: "curator") { "Autoincremented" id: ID! @@ -29,7 +29,7 @@ type NftHistoryEntry @entity @schema(name: "admin") { event: Event! } -type NftActivity @entity @schema(name: "admin") { +type NftActivity @entity @schema(name: "curator") { "Autoincremented" id: ID! @@ -506,7 +506,7 @@ type CreatorTokenRevenueSplitIssuedEventData { revenueShare: RevenueShare } -type UserInteractionCount @entity @schema(name: "admin") { +type UserInteractionCount @entity @schema(name: "curator") { "Autoincremented ID" id: ID! diff --git a/schema/hidden.graphql b/schema/hidden.graphql index b6624a1fb..3e8603c5d 100644 --- a/schema/hidden.graphql +++ b/schema/hidden.graphql @@ -1,4 +1,4 @@ -type VideoViewEvent @entity @schema(name: "admin") { +type VideoViewEvent @entity @schema(name: "curator") { "Unique identifier of the video view event" id: ID! @@ -12,7 +12,7 @@ type VideoViewEvent @entity @schema(name: "admin") { timestamp: DateTime! } -type Report @entity @schema(name: "admin") { +type Report @entity @schema(name: "curator") { "Unique identifier of the report" id: ID! @@ -32,7 +32,7 @@ type Report @entity @schema(name: "admin") { rationale: String! } -type NftFeaturingRequest @entity @schema(name: "admin") { +type NftFeaturingRequest @entity @schema(name: "curator") { "Unique identifier of the request" id: ID! @@ -49,7 +49,7 @@ type NftFeaturingRequest @entity @schema(name: "admin") { rationale: String! } -type ChannelFollow @entity @schema(name: "admin") { +type ChannelFollow @entity @schema(name: "curator") { "Unique identifier of the follow" id: ID! @@ -73,20 +73,3 @@ type GatewayConfig @entity @schema(name: "admin") { "Last time the configuration variable was updated" updatedAt: DateTime! } - -type Exclusion @entity @schema(name: "admin") { - "Unique identifier of the exclusion" - id: ID! - - "If it's a channel exclusion: ID of the channel being reported (the channel may no longer exist)" - channelId: String @index - - "If it's a video exclusion: ID of the video being reported (the video may no longer exist)" - videoId: String @index - - "Time of the exclusion" - timestamp: DateTime! - - "Rationale behind the exclusion" - rationale: String! -} diff --git a/schema/storage.graphql b/schema/storage.graphql index 3ff2914cf..3b43a4554 100644 --- a/schema/storage.graphql +++ b/schema/storage.graphql @@ -208,7 +208,7 @@ union DataObjectType = | DataObjectTypeVideoSubtitle | DataObjectTypeChannelPayoutsPayload -type StorageDataObject @entity @schema(name: "admin") { +type StorageDataObject @entity @schema(name: "curator") { "Data object runtime id" id: ID! diff --git a/schema/token.graphql b/schema/token.graphql index 6b60f39e8..59d89a980 100644 --- a/schema/token.graphql +++ b/schema/token.graphql @@ -102,7 +102,7 @@ type CreatorToken @entity { lastPrice: BigInt } -type MarketplaceToken @entity @schema(name: "admin") { +type MarketplaceToken @entity @schema(name: "curator") { liquidity: Int marketCap: BigInt cumulativeRevenue: BigInt diff --git a/schema/videoComments.graphql b/schema/videoComments.graphql index 14e3daf35..5a93fe61d 100644 --- a/schema/videoComments.graphql +++ b/schema/videoComments.graphql @@ -1,4 +1,4 @@ -type CommentReaction @entity @schema(name: "admin") { +type CommentReaction @entity @schema(name: "curator") { "{memberId}-{commentId}-{reactionId}" id: ID! @@ -37,7 +37,7 @@ enum CommentTipTier { DIAMOND } -type Comment @entity @schema(name: "admin") { +type Comment @entity @schema(name: "curator") { "METAPROTOCOL-{network}-{blockNumber}-{indexInBlock}" id: ID! diff --git a/schema/videos.graphql b/schema/videos.graphql index 7fd150b36..d668e729d 100644 --- a/schema/videos.graphql +++ b/schema/videos.graphql @@ -1,4 +1,4 @@ -type VideoCategory @entity @schema(name: "admin") { +type VideoCategory @entity @schema(name: "curator") { "Runtime identifier" id: ID! @@ -21,12 +21,12 @@ type VideoCategory @entity @schema(name: "admin") { createdInBlock: Int! } -type Video @entity @schema(name: "admin") { +type Video @entity @schema(name: "curator") { "Runtime identifier" id: ID! "Timestamp of the block the video was created at" - createdAt: DateTime! @index + createdAt: DateTime! "Reference to videos's channel" channel: Channel! @@ -50,7 +50,7 @@ type Video @entity @schema(name: "admin") { language: String "Video's orion langauge" - orionLanguage: String @index + orionLanguage: String "Whether or not Video contains marketing" hasMarketing: Boolean @@ -128,7 +128,7 @@ type Video @entity @schema(name: "admin") { trailerVideoForToken: [TrailerVideo!]! @derivedFrom(field: "video") "Video relevance score based on the views, reactions, comments and update date" - videoRelevance: Float! @index + videoRelevance: Float! "Whether the video is a short format, vertical video (e.g. Youtube Shorts, TikTok, Instagram Reels)" isShort: Boolean @@ -142,7 +142,7 @@ type Video @entity @schema(name: "admin") { type VideoFeaturedInCategory @entity - @schema(name: "admin") + @schema(name: "curator") @index(fields: ["category", "video"], unique: true) { "{categoryId-videoId}" id: ID! @@ -157,7 +157,7 @@ type VideoFeaturedInCategory videoCutUrl: String } -type VideoHero @entity @schema(name: "admin") { +type VideoHero @entity @schema(name: "curator") { "Unique ID" id: ID! @@ -177,7 +177,7 @@ type VideoHero @entity @schema(name: "admin") { activatedAt: DateTime } -type VideoMediaMetadata @entity @schema(name: "admin") { +type VideoMediaMetadata @entity @schema(name: "curator") { "Unique identifier" id: ID! @@ -198,7 +198,7 @@ type VideoMediaMetadata @entity @schema(name: "admin") { createdInBlock: Int! } -type VideoMediaEncoding @entity @schema(name: "admin") { +type VideoMediaEncoding @entity @schema(name: "curator") { "Encoding of the video media object" codecName: String @@ -209,7 +209,7 @@ type VideoMediaEncoding @entity @schema(name: "admin") { mimeMediaType: String } -type License @entity @schema(name: "admin") { +type License @entity @schema(name: "curator") { "Unique identifier" id: ID! @@ -223,7 +223,7 @@ type License @entity @schema(name: "admin") { customText: String } -type VideoSubtitle @entity @schema(name: "admin") { +type VideoSubtitle @entity @schema(name: "curator") { "{type}-{language}" id: ID! @@ -258,7 +258,7 @@ enum VideoReactionOptions { UNLIKE } -type VideoReaction @entity @schema(name: "admin") { +type VideoReaction @entity @schema(name: "curator") { "{memberId}-{videoId}" id: ID! diff --git a/src/auth-server/handlers/createAccount.ts b/src/auth-server/handlers/createAccount.ts index 096927fa3..42d21b26c 100644 --- a/src/auth-server/handlers/createAccount.ts +++ b/src/auth-server/handlers/createAccount.ts @@ -1,12 +1,12 @@ import express from 'express' -import { Account, EncryptionArtifacts, Membership, NextEntityId } from '../../model' +import { Account, EncryptionArtifacts, Membership } from '../../model' import { AuthContext } from '../../utils/auth' import { globalEm } from '../../utils/globalEm' -import { idStringFromNumber } from '../../utils/misc' import { defaultNotificationPreferences } from '../../utils/notification/helpers' import { BadRequestError, ConflictError, NotFoundError } from '../errors' import { components } from '../generated/api-types' import { verifyActionExecutionRequest } from '../utils' +import { uniqueId } from '../../utils/crypto' type ReqParams = Record type ResBody = @@ -34,16 +34,7 @@ export const createAccount: ( await verifyActionExecutionRequest(em, req.body) await em.transaction(async (em) => { - // Get and lock next account id - // FIXME: For some reason this doesn't work as expected without the parseInt! - // (returns `nextId` as a string instead of a number) - const nextAccountId = parseInt( - ( - await em - .getRepository(NextEntityId) - .findOne({ where: { entityName: 'Account' }, lock: { mode: 'pessimistic_write' } }) - )?.nextId.toString() || '1' - ) + const accountId = uniqueId() const existingByEmail = await em.getRepository(Account).findOneBy({ email }) if (existingByEmail) { @@ -79,7 +70,7 @@ export const createAccount: ( const notificationPreferences = defaultNotificationPreferences() const account = new Account({ - id: idStringFromNumber(nextAccountId), + id: accountId, email, isEmailConfirmed: false, registeredAt: new Date(), @@ -91,10 +82,7 @@ export const createAccount: ( referrerChannelId: null, }) - await em.save([ - account, - new NextEntityId({ entityName: 'Account', nextId: nextAccountId + 1 }), - ]) + await em.save(account) if (req.body.payload.encryptionArtifacts) { const { cipherIv, encryptedSeed, id: lookupKey } = req.body.payload.encryptionArtifacts diff --git a/src/mappings/content/channel.ts b/src/mappings/content/channel.ts index 3d5a98423..7fae8e0f0 100644 --- a/src/mappings/content/channel.ts +++ b/src/mappings/content/channel.ts @@ -26,7 +26,6 @@ import { MetaprotocolTransactionResultFailed, MetaprotocolTransactionStatusEventData, StorageDataObject, - YppUnverified, } from '../../model' import { EventHandlerContext } from '../../utils/events' import { addNotification } from '../../utils/notification' @@ -77,7 +76,8 @@ export async function processChannelCreatedEvent({ totalVideosCreated: 0, cumulativeRevenue: BigInt(0), cumulativeRewardClaimed: BigInt(0), - yppStatus: new YppUnverified(), + isYtSyncEnabled: false, + yppStatus: null, cumulativeReward: 0n, }) @@ -333,7 +333,7 @@ export async function processChannelRewardUpdatedEvent({ }) channel.cumulativeRewardClaimed += claimedAmount - increaseChannelCumulativeRevenue(channel, claimedAmount) + await increaseChannelCumulativeRevenue(channel, claimedAmount) } export async function processChannelRewardClaimedAndWithdrawnEvent({ @@ -359,7 +359,7 @@ export async function processChannelRewardClaimedAndWithdrawnEvent({ }) channel.cumulativeRewardClaimed += claimedAmount - increaseChannelCumulativeRevenue(channel, claimedAmount) + await increaseChannelCumulativeRevenue(channel, claimedAmount) } export async function processChannelFundsWithdrawnEvent({ diff --git a/src/mappings/content/commentsAndReactions.ts b/src/mappings/content/commentsAndReactions.ts index ac5a4f74c..d59ebf696 100644 --- a/src/mappings/content/commentsAndReactions.ts +++ b/src/mappings/content/commentsAndReactions.ts @@ -53,7 +53,7 @@ import { genericEventFields, metaprotocolTransactionFailure, commentCountersManager, - videoRelevanceManager, + relevanceQueuePublisher, } from '../utils' import { getAccountForMember, getChannelOwnerMemberByChannelId, memberHandleById } from './utils' import { addNotification } from '../../utils/notification' @@ -317,7 +317,7 @@ export async function processReactVideoMessage( existingReaction ) - videoRelevanceManager.scheduleRecalcForChannel(channelId) + await relevanceQueuePublisher.pushChannel(channelId) return new MetaprotocolTransactionResultOK() } @@ -538,7 +538,7 @@ export async function processCreateCommentMessage( // schedule comment counters update commentCountersManager.scheduleRecalcForComment(comment.parentCommentId) commentCountersManager.scheduleRecalcForVideo(comment.videoId) - videoRelevanceManager.scheduleRecalcForChannel(video.channelId) + await relevanceQueuePublisher.pushChannel(video.channelId) const event = overlay.getRepository(Event).new({ ...genericEventFields(overlay, block, indexInBlock, txHash), @@ -702,7 +702,7 @@ export async function processDeleteCommentMessage( // schedule comment counters update commentCountersManager.scheduleRecalcForComment(comment.parentCommentId) commentCountersManager.scheduleRecalcForVideo(comment.videoId) - videoRelevanceManager.scheduleRecalcForChannel(video.channelId) + await relevanceQueuePublisher.pushChannel(video.channelId) // update the comment comment.text = '' diff --git a/src/mappings/content/metadata.ts b/src/mappings/content/metadata.ts index 1e1258d45..67be7fa95 100644 --- a/src/mappings/content/metadata.ts +++ b/src/mappings/content/metadata.ts @@ -669,7 +669,7 @@ export async function processChannelPaymentFromMember( }), }) - increaseChannelCumulativeRevenue(channel, amount) + await increaseChannelCumulativeRevenue(channel, amount) const ownerAccount = await getChannelOwnerAccount(overlay, channel) await addNotification( overlay, diff --git a/src/mappings/content/utils.ts b/src/mappings/content/utils.ts index 1a2ee7b0f..122831238 100644 --- a/src/mappings/content/utils.ts +++ b/src/mappings/content/utils.ts @@ -74,7 +74,13 @@ import { import { criticalError } from '../../utils/misc' import { addNotification } from '../../utils/notification' import { EntityManagerOverlay, Flat } from '../../utils/overlay' -import { addNftActivity, addNftHistoryEntry, genericEventFields, invalidMetadata } from '../utils' +import { + addNftActivity, + addNftHistoryEntry, + genericEventFields, + invalidMetadata, + relevanceQueuePublisher, +} from '../utils' import { parseChannelTitle, parseVideoTitle } from '../../utils/notification/helpers' // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -878,17 +884,21 @@ export async function maybeIncreaseChannelCumulativeRevenueAfterNft( const nftOwnerType = previousNftOwner ? previousNftOwner.isTypeOf : nft.owner.isTypeOf if (nftOwnerType === 'NftOwnerChannel') { - increaseChannelCumulativeRevenue(channel, assertNotNull(nft.lastSalePrice)) + await increaseChannelCumulativeRevenue(channel, assertNotNull(nft.lastSalePrice)) } else { if (nft.creatorRoyalty) { const royaltyAmount = computeRoyalty(nft.creatorRoyalty, assertNotNull(nft.lastSalePrice)) - increaseChannelCumulativeRevenue(channel, royaltyAmount) + await increaseChannelCumulativeRevenue(channel, royaltyAmount) } } } -export function increaseChannelCumulativeRevenue(channel: Flat, amount: bigint): void { +export async function increaseChannelCumulativeRevenue( + channel: Flat, + amount: bigint +): Promise { channel.cumulativeRevenue = (channel.cumulativeRevenue || 0n) + amount + await relevanceQueuePublisher.pushChannel(channel.id) } export async function memberHandleById( diff --git a/src/mappings/content/video.ts b/src/mappings/content/video.ts index 964d580a2..88b488623 100644 --- a/src/mappings/content/video.ts +++ b/src/mappings/content/video.ts @@ -21,8 +21,8 @@ import { deserializeMetadata, genericEventFields, orionVideoLanguageManager, + relevanceQueuePublisher, u8aToBytes, - videoRelevanceManager, } from '../utils' import { processVideoMetadata } from './metadata' import { @@ -65,7 +65,7 @@ export async function processVideoCreatedEvent({ videoRelevance: 0, }) - videoRelevanceManager.scheduleRecalcForChannel(channelId.toString()) + await relevanceQueuePublisher.pushChannel(channelId.toString()) // fetch related channel and owner const channel = await overlay.getRepository(Channel).getByIdOrFail(channelId.toString()) diff --git a/src/mappings/subscribers/TransactionCommitSubscriber.ts b/src/mappings/subscribers/TransactionCommitSubscriber.ts new file mode 100644 index 000000000..93e4b2032 --- /dev/null +++ b/src/mappings/subscribers/TransactionCommitSubscriber.ts @@ -0,0 +1,11 @@ +import { EntitySubscriberInterface, EventSubscriber } from 'typeorm' +import { relevanceQueuePublisher } from '../utils' + +@EventSubscriber() +export class TransactionCommitSubscriber implements EntitySubscriberInterface { + async afterTransactionCommit(): Promise { + if (relevanceQueuePublisher.initialized) { + await relevanceQueuePublisher.commitDeferred() + } + } +} diff --git a/src/mappings/utils.ts b/src/mappings/utils.ts index 0e5ed6e7d..d2c6ce541 100644 --- a/src/mappings/utils.ts +++ b/src/mappings/utils.ts @@ -8,13 +8,19 @@ import { SubstrateBlock } from '@subsquid/substrate-processor' import { Logger } from '../logger' import { Event, MetaprotocolTransactionResultFailed, NftActivity, NftHistoryEntry } from '../model' import { CommentCountersManager } from '../utils/CommentsCountersManager' -import { VideoRelevanceManager } from '../utils/VideoRelevanceManager' import { EntityManagerOverlay } from '../utils/overlay' import { OrionVideoLanguageManager } from '../utils/OrionVideoLanguageManager' +import { RelevanceQueuePublisher } from '../relevance-service/RelevanceQueue' export const orionVideoLanguageManager = new OrionVideoLanguageManager() export const commentCountersManager = new CommentCountersManager() -export const videoRelevanceManager = new VideoRelevanceManager() +export const relevanceQueuePublisher = new RelevanceQueuePublisher({ + autoInitialize: false, + defaultPushOptions: { + deferred: true, + skipIfUninitialized: true, + }, +}) export const JOYSTREAM_SS58_PREFIX = 126 export function bytesToString(b: Uint8Array): string { diff --git a/src/model/indexes.ts b/src/model/indexes.ts new file mode 100644 index 000000000..41f4cd4c8 --- /dev/null +++ b/src/model/indexes.ts @@ -0,0 +1,116 @@ +import _ from 'lodash' +import { EntityManager } from 'typeorm' + +/** + * Utilities for creating PostgreSQL indexes. + * Reasons for chosing this approach over @index decorators in the schema: + * - More flexibility (support for expression indexes, different index types etc.) + * - Indexes can be created once Orion is fully synced, which makes a big difference in + * initial processing speed. + */ + +// TODO: Move other indexes from schema here + +type IndexDefinition = { + type?: 'BTREE' + name?: string + on: string + target: string + expression?: string +} + +export const INDEX_DEFS: IndexDefinition[] = [ + // Event + { on: `"curator"."event"`, target: `(("data"->>'video'))` }, + { on: `"curator"."event"`, target: `(("data"->>'comment'))` }, + { on: `"curator"."event"`, target: `(("data"->'nftOwner'->>'member'))` }, + { on: `"curator"."event"`, target: `(("data"->'nftOwner'->>'channel'))` }, + { on: `"curator"."event"`, target: `(("data"->>'auction'))` }, + { on: `"curator"."event"`, target: `(("data"->>'isTypeOf'))` }, + { on: `"curator"."event"`, target: `(("data"->>'nft'))` }, + { on: `"curator"."event"`, target: `(("data"->>'bid'))` }, + { on: `"curator"."event"`, target: `(("data"->>'member'))` }, + { on: `"curator"."event"`, target: `(("data"->>'winningBid'))` }, + { on: `"curator"."event"`, target: `(("data"->'previousNftOwner'->>'member'))` }, + { on: `"curator"."event"`, target: `(("data"->'previousNftOwner'->>'channel'))` }, + { on: `"curator"."event"`, target: `(("data"->>'buyer'))` }, + // Auction + { on: `"curator"."auction"`, target: `(("auction_type"->>'isTypeOf'))` }, + // OwnedNFT + { on: `"curator"."owned_nft"`, target: `(("transactional_status"->>'auction'))` }, + // MemberMetadata + { on: `"member_metadata"`, target: `(("avatar"->>'avatarObject'))` }, + // Video + { on: `"curator"."video"`, target: `("created_at")` }, + { on: `"curator"."video"`, target: `("language")` }, + { on: `"curator"."video"`, target: `("orion_language")` }, + { on: `"curator"."video"`, target: `("video_relevance")` }, + { on: `"curator"."video"`, target: `("yt_video_id")` }, + { on: `"curator"."video"`, target: `("views_num")` }, + { on: `"curator"."video"`, target: `("reactions_count")` }, + { on: `"curator"."video"`, target: `("comments_count")` }, + { on: `"curator"."video"`, target: `("is_censored")` }, + { on: `"curator"."video"`, target: `("is_public")` }, + { on: `"curator"."video"`, target: `("is_excluded")` }, + { on: `"curator"."video"`, target: `("include_in_home_feed")` }, + { on: `"curator"."video"`, target: `("is_short")` }, + { on: `"curator"."video"`, target: `("is_short_derived")` }, + // Channel + { on: `"curator"."channel"`, target: `("created_at")` }, + { on: `"curator"."channel"`, target: `("language")` }, + { on: `"curator"."channel"`, target: `("channel_weight")` }, + { on: `"curator"."channel"`, target: `("video_views_num")` }, + { on: `"curator"."channel"`, target: `("total_videos_created")` }, + { on: `"curator"."channel"`, target: `("cumulative_revenue")` }, + { on: `"curator"."channel"`, target: `(("ypp_status"->>'isTypeOf'))` }, + { on: `"curator"."channel"`, target: `(("ypp_status"->>'tier'))` }, + { on: `"curator"."channel"`, target: `("is_censored")` }, + { on: `"curator"."channel"`, target: `("is_public")` }, + { on: `"curator"."channel"`, target: `("is_excluded")` }, +] + +function normalizePsqlName(name: string): string { + return name + .replace(/->/g, '_') + .replace(/\./g, '_') + .replace(/[^A-Za-z0-9_]/g, '') +} + +export function indexName(index: IndexDefinition) { + return index.name || generateIndexName(index) +} + +function generateIndexName({ on, target }: Pick): string { + return `"idx__${normalizePsqlName(on)}__${normalizePsqlName(target)}"` +} + +function createIndexQuery({ type = 'BTREE', name, on, target }: IndexDefinition): string { + name = name || generateIndexName({ on, target }) + return `CREATE INDEX IF NOT EXISTS ${name} ON ${on} USING ${type} ${target};` +} + +export async function createIndexes( + db: EntityManager, + defs: IndexDefinition[] = INDEX_DEFS +): Promise { + for (const index of defs) { + const query = createIndexQuery(index) + await db.query(query) + } +} + +export async function dropIndexes(db: EntityManager): Promise { + for (const index of INDEX_DEFS) { + const query = `DROP INDEX IF EXISTS ${indexName(index)};` + await db.query(query) + } +} + +export async function getMissingIndexes(db: EntityManager): Promise { + const existingIndexNames: string[] = (await db.query(`SELECT indexname FROM pg_indexes`)).map( + (r: { indexname: string }) => r.indexname + ) + return _.differenceBy(INDEX_DEFS, existingIndexNames, (v) => + (typeof v === 'string' ? v : indexName(v)).replace(/(\\|")/g, '') + ) +} diff --git a/src/model/views.ts b/src/model/views.ts new file mode 100644 index 000000000..5eccb17fc --- /dev/null +++ b/src/model/views.ts @@ -0,0 +1,185 @@ +import { EntityManager } from 'typeorm' +import { withPriceChange } from '../server-extension/resolvers/CreatorToken/utils' +import { config, ConfigVariable } from '../utils/config' + +type ViewDefinitions = { + [schemaName: string]: { + [tableName: string]: string[] | string + } +} + +// Add public 'VIEW' definitions for hidden entities created by +// applying `@schema(name: "admin")` or `@schema(name: "curator") directive to the Graphql entities +export function getPublicViewDefinitions(db: EntityManager): ViewDefinitions { + const blocksPerDay = 10 * 60 * 24 // 10 blocs per minute, 60 mins * 24 hours + return { + curator: { + channel: [`is_excluded='0'`, `is_censored='0'`], + banned_member: [`EXISTS(SELECT 1 FROM "channel" WHERE "id"="channel_id")`], + video_category: [`"is_supported" = '1'`], + video: [ + `is_excluded='0'`, + `is_censored='0'`, + `EXISTS(SELECT 1 FROM "channel" WHERE "id"="channel_id")`, + `EXISTS(SELECT 1 FROM "video_category" WHERE "id"="category_id") + OR ( + "category_id" IS NULL + AND COALESCE( + (SELECT "value" FROM "admin"."gateway_config" WHERE "id"='SUPPORT_NO_CATEGORY_VIDEOS'), + ${config.getDefault(ConfigVariable.SupportNoCategoryVideo) ? "'1'" : "'0'"} + )='1' + )`, + ], + owned_nft: [`EXISTS(SELECT 1 FROM "video" WHERE "id"="video_id")`], + auction: [`EXISTS(SELECT 1 FROM "owned_nft" WHERE "id"="nft_id")`], + bid: [`EXISTS(SELECT 1 FROM "owned_nft" WHERE "id"="nft_id")`], + comment: ` + SELECT + ${db.connection + .getMetadata('Comment') + .columns.filter((c) => c.databaseName !== 'text') + .map((c) => `"${c.databaseName}"`) + .join(',')}, + CASE WHEN "is_excluded" = '1' THEN '' ELSE "comment"."text" END as "text" + FROM + "curator"."comment" + WHERE EXISTS(SELECT 1 FROM "video" WHERE "id"="video_id") + `, + comment_reaction: [`EXISTS(SELECT 1 FROM "comment" WHERE "id"="comment_id")`], + license: [`EXISTS(SELECT 1 FROM "video" WHERE "license_id"="this"."id")`], + video_media_metadata: [`EXISTS(SELECT 1 FROM "video" WHERE "id"="video_id")`], + video_media_encoding: [ + `EXISTS(SELECT 1 FROM "video_media_metadata" WHERE "encoding_id"="this"."id")`, + ], + video_reaction: [`EXISTS(SELECT 1 FROM "video" WHERE "id"="video_id")`], + video_subtitle: [`EXISTS(SELECT 1 FROM "video" WHERE "id"="video_id")`], + video_featured_in_category: [ + `EXISTS(SELECT 1 FROM "video" WHERE "id"="video_id")`, + `EXISTS(SELECT 1 FROM "video_category" WHERE "id"="category_id")`, + ], + video_hero: [`EXISTS(SELECT 1 FROM "video" WHERE "id"="video_id")`], + // TODO: Consider all events having ref to a video they're related to - this will make filtering much easier + event: [ + `("data"->>'channel' IS NULL OR EXISTS(SELECT 1 FROM "channel" WHERE "id"="data"->>'channel'))`, + `("data"->>'video' IS NULL OR EXISTS(SELECT 1 FROM "video" WHERE "id"="data"->>'video'))`, + `("data"->>'nft' IS NULL OR EXISTS(SELECT 1 FROM "owned_nft" WHERE "id"="data"->>'nft'))`, + `("data"->>'auction' IS NULL OR EXISTS(SELECT 1 FROM "auction" WHERE "id"="data"->>'auction'))`, + `("data"->>'bid' IS NULL OR EXISTS(SELECT 1 FROM "bid" WHERE "id"="data"->>'bid'))`, + `("data"->>'winningBid' IS NULL OR EXISTS(SELECT 1 FROM "bid" WHERE "id"="data"->>'winningBid'))`, + `("data"->>'comment' IS NULL OR EXISTS(SELECT 1 FROM "comment" WHERE "id"="data"->>'comment'))`, + ], + storage_data_object: [ + `("type"->>'channel' IS NULL OR EXISTS(SELECT 1 FROM "channel" WHERE "id"="type"->>'channel'))`, + `("type"->>'video' IS NULL OR EXISTS(SELECT 1 FROM "video" WHERE "id"="type"->>'video'))`, + ], + nft_history_entry: [`EXISTS(SELECT 1 FROM "event" WHERE "id"="event_id")`], + nft_activity: [`EXISTS(SELECT 1 FROM "event" WHERE "id"="event_id")`], + marketplace_token: ` + WITH + last_block AS ( + SELECT height FROM squid_processor.status + ), + tokens_with_stats AS ( + SELECT + ac.token_id, + SUM(CASE + WHEN ( + transaction_type = 'BUY' + AND tr.created_in < last_block.height - ${blocksPerDay * 30} + ) THEN quantity + WHEN ( + transaction_type = 'SELL' + AND tr.created_in < last_block.height - ${blocksPerDay * 30} + ) THEN quantity * -1 + ELSE 0 + END) AS total_liquidity_30d_ago, + SUM (CASE + WHEN transaction_type = 'BUY' THEN quantity + ELSE quantity * -1 + END) AS total_liquidity, + SUM(tr.price_paid) as amm_volume + FROM amm_transaction tr + JOIN amm_curve ac ON ac.id = tr.amm_id + JOIN creator_token ct ON ct.current_amm_sale_id = ac.id + JOIN last_block ON 1=1 + GROUP BY token_id + ), + ${withPriceChange({ periodDays: 30, currentBlock: 'last_block' })} + SELECT + COALESCE(tws.total_liquidity, 0) as liquidity, + CASE + WHEN tws.amm_volume >= COALESCE( + market_cap_min_volume_cfg.value::int8, + ${config.getDefault(ConfigVariable.CrtMarketCapMinVolumeJoy)} + ) THEN (ct.last_price * ct.total_supply) + ELSE 0 + END as market_cap, + c.cumulative_revenue, + c.id as channel_id, + COALESCE(tws.amm_volume, 0) as amm_volume, + COALESCE(twpc.percentage_change, 0) price_change, + CASE + WHEN (tws.total_liquidity_30d_ago IS NULL OR tws.total_liquidity_30d_ago = 0) THEN 0 + ELSE ( + (tws.total_liquidity - tws.total_liquidity_30d_ago) + * 100 + / tws.total_liquidity_30d_ago + ) + END as liquidity_change, + ct.* + FROM creator_token ct + LEFT JOIN token_channel tc ON tc.token_id = ct.id + LEFT JOIN channel c ON c.id = tc.channel_id + LEFT JOIN tokens_with_price_change twpc ON twpc.token_id = ct.id + LEFT JOIN tokens_with_stats tws ON tws.token_id = ct.id + LEFT JOIN "admin"."gateway_config" market_cap_min_volume_cfg ON market_cap_min_volume_cfg.id = 'CRT_MARKET_CAP_MIN_VOLUME_JOY'`, + // *** HIDDEN entities *** + // Even though the following entities are hidden to the public, + // we still define these in the views definitions to create their VIEW in the public schema as they are + // exposed by the GRAPHQL API, so that when querying the GRAPHQL API, the response is just empty object + // instead of `"relation does not exist"` error. + video_view_event: ['FALSE'], + channel_follow: ['FALSE'], + report: ['FALSE'], + nft_featuring_request: ['FALSE'], + // notification: ['FALSE'], + }, + admin: { + session: ['FALSE'], + notification_email_delivery: ['FALSE'], + user: ['FALSE'], + account: ['FALSE'], + token: ['FALSE'], + gateway_config: ['FALSE'], + email_delivery_attempt: ['FALSE'], + }, + } +} + +export async function createViews(db: EntityManager) { + const defs = getPublicViewDefinitions(db) + for (const [schemaName, schemaViews] of Object.entries(defs)) { + for (const [tableName, viewConditions] of Object.entries(schemaViews)) { + if (Array.isArray(viewConditions)) { + await db.query(`DROP VIEW IF EXISTS "${tableName}" CASCADE`) + await db.query(` + CREATE OR REPLACE VIEW "${tableName}" AS + SELECT * + FROM "${schemaName}"."${tableName}" AS "this" + WHERE ${viewConditions.map((cond) => `(${cond})`).join(' AND ')} + `) + } else { + await db.query(`CREATE OR REPLACE VIEW "${tableName}" AS (${viewConditions})`) + } + } + } +} + +export async function dropViews(db: EntityManager) { + const defs = getPublicViewDefinitions(db) + for (const schemaViews of Object.values(defs)) { + for (const tableName of Object.keys(schemaViews)) { + await db.query(`DROP VIEW IF EXISTS "${tableName}" CASCADE`) + } + } +} diff --git a/src/processor.ts b/src/processor.ts index abc895605..fb51b1361 100644 --- a/src/processor.ts +++ b/src/processor.ts @@ -115,14 +115,15 @@ import { } from './mappings/token' import { commentCountersManager, - videoRelevanceManager, orionVideoLanguageManager, + relevanceQueuePublisher, } from './mappings/utils' import { Event } from './types/support' import { EventHandler, EventInstance, EventNames, eventConstructors } from './utils/events' import { assertAssignable } from './utils/misc' import { OffchainState } from './utils/offchainState' import { EntityManagerOverlay } from './utils/overlay' +import { createIndexes, getMissingIndexes, indexName } from './model/indexes' const defaultEventOptions = { data: { @@ -377,16 +378,8 @@ orionVideoLanguageManager throw new Error(`Failed to initialize Orion video language manager: ${e.toString()}`) }) -videoRelevanceManager - .init({ - fullUpdateLoopTime: 1000 * 60 * 60 * 12, // 12 hrs - scheduledUpdateLoopTime: 1000 * 60 * 10, // 10 mins - }) - .catch((e) => { - throw new Error(`Failed to initialize Orion video relevance manager: ${e.toString()}`) - }) - let exportBlockNumber: number +let indexesChecked = false processor.run(new TypeormDatabase({ isolationLevel: 'READ COMMITTED' }), async (ctx) => { Logger.set(ctx.log) @@ -397,26 +390,47 @@ processor.run(new TypeormDatabase({ isolationLevel: 'READ COMMITTED' }), async ( } const overlay = await EntityManagerOverlay.create(ctx.store, afterDbUpdate) + const em = overlay.getEm() - for (const block of ctx.blocks) { - if (block.header.height > exportBlockNumber && !videoRelevanceManager.isVideoRelevanceEnabled) { - videoRelevanceManager.turnOnVideoRelevanceManager() + if (ctx.isHead && offchainState.isImported) { + const missingIndexes = await getMissingIndexes(em) + if (missingIndexes.length) { + ctx.log.info( + `Head reached and some indexes are missing (${missingIndexes + .map((i) => indexName(i)) + .join(', ')})! Creating indexes...` + ) + await createIndexes(em, missingIndexes) + ctx.log.info(`Indexes created successfully!`) } - // Importing exported offchain state - if (block.header.height > exportBlockNumber && !offchainState.isImported) { - ctx.log.info(`Export block ${exportBlockNumber} reached, importing offchain state...`) - // there is no need to recalc video relevance before orion is synced - await overlay.updateDatabase() - const em = overlay.getEm() - await offchainState.import(overlay) - await commentCountersManager.updateVideoCommentsCounters(em, true) - await commentCountersManager.updateParentRepliesCounters(em, true) - await videoRelevanceManager.updateVideoRelevanceValue(em, true) - ctx.log.info(`Offchain state successfully imported!`) + indexesChecked = true + } + + for (const block of ctx.blocks) { + if (block.header.height > exportBlockNumber) { + if (!offchainState.isImported) { + // Importing exported offchain state + ctx.log.info(`Export block ${exportBlockNumber} reached, importing offchain state...`) + // there is no need to recalc video relevance before orion is synced + await overlay.updateDatabase() + await offchainState.import(overlay) + ctx.log.info('Updating video comments counters...') + await commentCountersManager.updateVideoCommentsCounters(em, true) + ctx.log.info('Video comments counters updated!') + ctx.log.info(`Updating video comments reply counters...`) + await commentCountersManager.updateParentRepliesCounters(em, true) + ctx.log.info(`Video comments reply counters updated!`) + ctx.log.info(`Offchain state successfully imported!`) + } + if (!relevanceQueuePublisher.initialized) { + // Initializing relevance queue publisher + await relevanceQueuePublisher.init() + ctx.log.info(`Relevance queue publisher initialized!`) + } } for (const item of block.items) { if (item.name !== '*') { - ctx.log.info(`Processing ${item.name} event in block ${block.header.height}...`) + ctx.log.debug(`Processing ${item.name} event in block ${block.header.height}...`) await processEvent( ctx, item.name, diff --git a/src/relevance-service/RelevanceQueue.ts b/src/relevance-service/RelevanceQueue.ts new file mode 100644 index 000000000..71aeef90f --- /dev/null +++ b/src/relevance-service/RelevanceQueue.ts @@ -0,0 +1,216 @@ +import amqp from 'amqplib' +import { createLogger, Logger } from '@subsquid/logger' + +export const CHANNELS_QUEUE_NAME = 'orion.relevance.channels' +export const RESTART_QUEUE_NAME = 'orion.relevance.restart' + +export async function initChannel() { + const url = process.env.RABBITMQ_URL || 'amqp://localhost' + const conn = await amqp.connect(url) + const channel = await conn.createConfirmChannel() + await channel.assertQueue(CHANNELS_QUEUE_NAME, { + durable: true, + }) + await channel.assertQueue(RESTART_QUEUE_NAME, { + durable: false, + maxLength: 1, + autoDelete: true, + }) + return channel +} + +type PushOptions = { + deferred?: boolean + skipIfUninitialized?: boolean +} + +type RelevancePublisherConfig = { + autoInitialize: boolean + defaultPushOptions: PushOptions +} + +export class RelevanceQueuePublisher { + protected logger: Logger = createLogger('relevance-queue:publisher') + protected deferredChannelIds: Set = new Set() + protected autoInitialize: boolean + protected deferredRestart = false + protected defaultPushOptions: Required + protected _channel: amqp.ConfirmChannel + + constructor(config?: RelevancePublisherConfig) { + this.autoInitialize = config?.autoInitialize || false + this.defaultPushOptions = { + deferred: false, + skipIfUninitialized: false, + ...(config?.defaultPushOptions || {}), + } + } + + protected get channel() { + if (!this._channel) { + throw new Error('RelevanceQueuePublisher is not initialized. Call init() first.') + } + return this._channel + } + + public get initialized() { + return !!this._channel + } + + public async init() { + if (!this._channel) { + this._channel = await initChannel() + } else { + this.logger.warn('RelevanceQueuePublisher is already initialized.') + } + } + + protected parseOptions(options?: PushOptions) { + return { + ...this.defaultPushOptions, + ...(options || {}), + } + } + + public async pushRestartRequest(options?: PushOptions) { + options = this.parseOptions(options) + if (!this.initialized && options.skipIfUninitialized) { + return + } + if (options.deferred) { + this.deferredRestart = true + } else { + await this.queueRestart() + } + } + + public async pushChannel(channelId?: string | null, options?: PushOptions) { + options = this.parseOptions(options) + if (!channelId || (!this.initialized && options.skipIfUninitialized)) { + return + } + if (options.deferred) { + this.deferredChannelIds.add(channelId) + } else { + await this.queueChannel(channelId) + } + } + + public async commitDeferred(): Promise { + if (this.deferredChannelIds.size) { + const deferredChannelIds = [...this.deferredChannelIds] + this.deferredChannelIds.clear() + const results = await Promise.all( + deferredChannelIds.map((channelId) => this.queueChannel(channelId)) + ) + const [successCount, totalCount] = [ + results.filter((result) => result === true).length, + results.length, + ] + this.logger.info( + `Committed ${successCount} / ${totalCount} deferred messages to relevance recalc queue.` + ) + } + if (this.deferredRestart) { + this.deferredRestart = false + const result = await this.queueRestart() + if (result === true) { + this.logger.info(`Comitted a deferred restart`) + } + } + } + + protected async sendMessage( + queue: string, + message: Buffer, + options?: amqp.Options.Publish + ): Promise { + if (!this.initialized && this.autoInitialize) { + await this.init() + } + return new Promise((resolve) => { + this.channel.sendToQueue( + queue, + message, + { + ...options, + }, + (err) => { + if (err) { + this.logger.error(err, `Failed to send message to queue "${queue}"!`) + resolve(err instanceof Error ? err.message : String(err)) + } else { + this.logger.debug(`Message sent to queue "${queue}". Content: ${message.toString()}`) + resolve(true) + } + } + ) + }) + } + + protected queueChannel(channelId: string): Promise { + return this.sendMessage(CHANNELS_QUEUE_NAME, Buffer.from(channelId), { + persistent: true, + contentType: 'text/plain', + }) + } + + protected queueRestart(): Promise { + return this.sendMessage(RESTART_QUEUE_NAME, Buffer.from([1])) + } +} + +export type ChannelIdsBatch = { + channelIds: string[] + ack?: () => void +} + +type RelevanceConsumerConfig = { + onRestartSignal: () => void +} + +export class RelevanceQueueConsumer { + protected logger: Logger = createLogger('relevance-queue:consumer') + + protected constructor(protected channel: amqp.ConfirmChannel) {} + + public static async init({ onRestartSignal }: RelevanceConsumerConfig) { + const channel = await initChannel() + await channel.consume( + RESTART_QUEUE_NAME, + (msg) => { + if (msg?.content) { + channel.ack(msg) + onRestartSignal() + } + }, + { noAck: false } + ) + return new RelevanceQueueConsumer(channel) + } + + public async channelsQueueSize(): Promise { + const { messageCount } = await this.channel.checkQueue(CHANNELS_QUEUE_NAME) + return messageCount + } + + public async getChannelsBatch(size: number): Promise { + const batch: Set = new Set() + let lastMessage: amqp.GetMessage | null = null + let msg: amqp.GetMessage | false = false + while ( + batch.size < size && + (msg = await this.channel.get(CHANNELS_QUEUE_NAME, { noAck: false })) + ) { + lastMessage = msg + const channelId = msg.content.toString() + if (!batch.has(channelId)) { + batch.add(channelId) + } + } + return { + channelIds: Array.from(batch), + ack: () => lastMessage && this.channel.ack(lastMessage, true), + } + } +} diff --git a/src/relevance-service/RelevanceService.ts b/src/relevance-service/RelevanceService.ts new file mode 100644 index 000000000..5b0116c03 --- /dev/null +++ b/src/relevance-service/RelevanceService.ts @@ -0,0 +1,396 @@ +import { EntityManager } from 'typeorm' +import { createLogger, Logger } from '@subsquid/logger' +import _ from 'lodash' +import { ChannelIdsBatch, RelevanceQueueConsumer } from './RelevanceQueue' +import { RelevanceServiceConfig, RelevanceWeights } from '../utils/config' + +export const SECONDS_PER_DAY = 60 * 60 * 24 + +// TODO: Make it configurable +const MIN_PERCENTILE_TO_RATE = new Map([ + [0.25, 0.15], // top 75% + [0.5, 0.3], // top 50% + [0.75, 0.45], // top 25% + [0.9, 0.6], // top 10% + [0.95, 0.75], // top 5% + [0.975, 0.9], // top 2.5% + [0.99, 1], // top 1% +]) + +type PercentileStats = { + crtLiquidity: number[] + crtVolume: number[] + channelFollowers: number[] + channelRevenue: number[] + videoViews: number[] + videoComments: number[] + videoReactions: number[] +} + +export class RelevanceService { + // The background queue is populated periodically with all existing channels + // and has a lower priority. It's used to update relevances due to passage of time and/or + // system-wide changes. + private backgroundQueue: Set = new Set() + // Percentile stats used for calculating channel and video relevances + private percentileStats?: PercentileStats + + private running = false + private logger: Logger + + constructor( + private em: EntityManager, + private priorityQueue: RelevanceQueueConsumer, + private config: RelevanceServiceConfig, + private weights: RelevanceWeights + ) { + this.logger = createLogger('relevance-manager') + } + + async run(): Promise { + if (this.running) { + throw new Error('VideoRelevanceManager is already running') + } + + await this.updatePercentileStats() + + this.runPopulateBackgroundQueueLoop().catch((err) => { + this.logger.error(err, 'Background loop terminated') + process.exit(-1) + }) + + this.runUpdateLoop().catch((err) => { + this.logger.error(err, 'Update loop terminated') + process.exit(-1) + }) + } + + private async getChannelsToUpdate(): Promise { + const { channelsPerIteration } = this.config + const { channelIds: priorityChannels, ack } = await this.priorityQueue.getChannelsBatch( + channelsPerIteration + ) + const backgroundQueue = Array.from(this.backgroundQueue) + const backgroundChunkSize = Math.max(0, channelsPerIteration - priorityChannels.length) + const backgroundChannels = backgroundQueue.slice(0, backgroundChunkSize) + this.backgroundQueue = new Set(backgroundQueue.slice(backgroundChunkSize)) + return { channelIds: priorityChannels.concat(backgroundChannels), ack } + } + + private rateQuery(column: string, cutoffs: number[]) { + const rates = Array.from(MIN_PERCENTILE_TO_RATE.values()) + const cutoffsWithRates = cutoffs.map((cutoff, i) => ({ + cutoff, + rate: rates[i], + })) + const conditions = _.sortBy(cutoffsWithRates, (r) => -r.rate).map( + ({ cutoff, rate }) => `WHEN ${column} >= ${cutoff} THEN ${rate}` + ) + return `( + CASE + ${conditions.join('\n')} + ELSE 0 + END + )` + } + + private async getVideosToRate(channelId: string): Promise { + const result = await this.em.query( + `SELECT id FROM video WHERE channel_id = $1 ORDER BY created_at DESC LIMIT $2`, + [channelId, this.config.videosPerChannelLimit] + ) + return result.map((row: { id: string }) => row.id) + } + + private channelYppTierRateQuery() { + // TODO: Make the rates configurable + return `( + CASE + WHEN channel.ypp_status->>'tier' = 'BRONZE' THEN 0.125 + WHEN channel.ypp_status->>'tier' = 'SILVER' THEN 0.25 + WHEN channel.ypp_status->>'tier' = 'GOLD' THEN 0.5 + WHEN channel.ypp_status->>'tier' = 'DIAMOND' THEN 1 + ELSE 0 + END + )` + } + + private async updateChannelWeights(channelIds: string[]): Promise { + if (!this.percentileStats) { + this.logger.warn('Percentile stats missing, skipping channel weights update') + return + } + this.logger.info(`Updating weights of ${channelIds.length} channels`) + const { percentileStats } = this + const { crtLiquidityWeight, crtVolumeWeight, followersWeight, revenueWeight, yppTierWeight } = + this.weights.channel + await this.em.transaction(async (em) => { + // Acquire the row locks first in a predictable order + await em.query( + `SELECT id FROM curator.channel WHERE id = ANY($1) ORDER BY id ASC FOR UPDATE`, + [channelIds] + ) + // Execute the update + await em.query( + ` + WITH channels_with_rates AS ( + SELECT + channel.id AS channel_id, + ${this.rateQuery( + 'channel.follows_num', + percentileStats.channelFollowers + )} AS follows_num_rate, + ${this.rateQuery( + 'channel.cumulative_revenue', + percentileStats.channelRevenue + )} AS cumulative_revenue_rate, + ${this.rateQuery('mt.liquidity', percentileStats.crtLiquidity)} AS liquidity_rate, + ${this.rateQuery('mt.amm_volume', percentileStats.crtVolume)} AS volume_rate, + ${this.channelYppTierRateQuery()} AS ypp_rate + FROM curator.channel + LEFT JOIN public.marketplace_token mt ON mt.channel_id = channel.id + WHERE channel.id = ANY($1) + ) + UPDATE + curator.channel + SET + channel_weight = 1 + ( + cwr.follows_num_rate * ${followersWeight} + + cwr.cumulative_revenue_rate * ${revenueWeight} + + cwr.liquidity_rate * ${crtLiquidityWeight} + + cwr.volume_rate * ${crtVolumeWeight} + + cwr.ypp_rate * ${yppTierWeight} + ) + FROM + channels_with_rates cwr + WHERE + channel.id = cwr.channel_id + `, + [channelIds] + ) + }) + } + + private videoAgeRateQuery() { + const { ageScoreHalvingDays } = this.config + const { joystreamAgeWeight, youtubeAgeWeight } = this.weights.video.ageSubWeights + const weightedTimestamp = ` + ( + EXTRACT(EPOCH FROM video.created_at) * ${joystreamAgeWeight} + + CASE + WHEN ( + video.yt_video_id IS NOT NULL + AND video.published_before_joystream IS NOT NULL + AND video.published_before_joystream < now() + ) THEN EXTRACT(EPOCH FROM video.published_before_joystream) + ELSE EXTRACT(EPOCH FROM video.created_at) + END * ${youtubeAgeWeight} + )` + const weightedAgeDays = `((EXTRACT(EPOCH FROM now()) - ${weightedTimestamp}) / ${SECONDS_PER_DAY})` + return `(1 / POWER(2, ${weightedAgeDays} / ${ageScoreHalvingDays}))` + } + + private videoRelevanceFormula(alias: string) { + const { ageWeight, viewsWeight, commentsWeight, reactionsWeight } = this.weights.video + return `( + ${alias}.channel_weight * ( + ${alias}.age_rate * ${ageWeight} + + ${alias}.views_num_rate * ${viewsWeight} + + ${alias}.comments_count_rate * ${commentsWeight} + + ${alias}.reactions_count_rate * ${reactionsWeight} + ) + )` + } + + private async updateVideoRelevances(channelIds: string[]): Promise { + if (!this.percentileStats) { + this.logger.warn('Percentile stats missing, skipping video relevances update') + return + } + const { + percentileStats, + config: { videosPerChannelSelectTop }, + } = this + + const videosToRate = ( + await Promise.all(channelIds.map((channelId) => this.getVideosToRate(channelId))) + ).flat() + + this.logger.info(`Calculating relevances of ${videosToRate.length} videos...`) + await this.em.transaction(async (em) => { + // Acquire the row locks first in a predictable order + await this.em.query( + `SELECT id FROM curator.video WHERE channel_id = ANY($1) ORDER BY id ASC FOR UPDATE`, + [channelIds] + ) + // Reset all video relevances for the channels that are being updated + await em.query( + `UPDATE curator.video SET video_relevance = 0 WHERE video_relevance != 0 AND video.channel_id = ANY($1)`, + [channelIds] + ) + // Calculate the relevances of the latest `videosPerChannelLimit` videos per channel + // and update it for top `videosPerChannelSelectTop` videos per channel + if (videosToRate.length > 0) { + await em.query( + ` + WITH + videos_with_ratings AS ( + SELECT + channel_id, + channel.channel_weight AS channel_weight, + video.id AS video_id, + ${this.videoAgeRateQuery()} as age_rate, + ${this.rateQuery('video.views_num', percentileStats.videoViews)} AS views_num_rate, + ${this.rateQuery( + 'video.comments_count', + percentileStats.videoComments + )} AS comments_count_rate, + ${this.rateQuery( + `video.reactions_count`, + percentileStats.videoReactions + )} AS reactions_count_rate + FROM + video + INNER JOIN channel ON video.channel_id = channel.id + WHERE + video.id = ANY($1) + ), + rated_videos AS ( + SELECT + vwr.*, + ${this.videoRelevanceFormula('vwr')} as video_relevance + FROM videos_with_ratings vwr + ), + ranked_videos AS ( + SELECT + rv.*, + rank() OVER (PARTITION BY channel_id ORDER BY video_relevance DESC, age_rate DESC, video_id DESC) AS relevance_rank + FROM rated_videos rv + ) + UPDATE + video + SET + video_relevance = rnv.video_relevance + FROM + ranked_videos rnv + WHERE + video.id = rnv.video_id + AND rnv.relevance_rank <= ${videosPerChannelSelectTop} + `, + [videosToRate] + ) + } + }) + } + + private async queueSize() { + return (await this.priorityQueue.channelsQueueSize()) + this.backgroundQueue.size + } + + async runSingleUpdate() { + const queueSize = await this.queueSize() + if (queueSize === 0) { + return + } + this.logger.info(`Running single update iteration (queue size: ${queueSize})`) + const { channelIds, ack } = await this.getChannelsToUpdate() + if (channelIds.length === 0) { + this.logger.info('No channels to update found. Skipping iteration.') + return + } + // Update channel weights + await this.updateChannelWeights(channelIds) + // Update video relevances + await this.updateVideoRelevances(channelIds) + ack?.() + this.logger.info(`Single update done (queue size: ${await this.queueSize()})`) + } + + private async loadPercentileStats( + inputs: { + fractions: number[] + from: string + by: string + nonZero?: boolean + }[] + ) { + const queries: string[] = [] + for (const input of inputs) { + const { from, by, fractions, nonZero = true } = input + const fractionsStr = fractions.join(',') + queries.push(` + SELECT + COALESCE( + percentile_cont(ARRAY[${fractionsStr}]) WITHIN GROUP (ORDER BY ${by}), + ARRAY[${fractions.map(() => 0).join(',')}] + ) AS percentiles + FROM + ${from} + ${nonZero ? `WHERE ${by} > 0` : ''} + `) + } + const query = queries.map((q) => `(${q})`).join(' UNION ALL ') + const results = await this.em.query(query) + return results.map((r: { percentiles: number[] }) => r.percentiles) + } + + private async updatePercentileStats(): Promise { + const fractions = Array.from(MIN_PERCENTILE_TO_RATE.keys()) + const [ + crtLiquidityPercentiles, + crtVolumePercentiles, + channelFollowersPercentiles, + channelRevenuePercentiles, + videoViewsPercentiles, + videoCommentsPercentiles, + videoReactionsPercentiles, + ] = await this.loadPercentileStats([ + { from: 'public.marketplace_token', by: 'liquidity', fractions }, + { from: 'public.marketplace_token', by: 'amm_volume', fractions }, + { from: 'channel', by: 'follows_num', fractions }, + { from: 'channel', by: 'cumulative_revenue', fractions }, + { from: 'video', by: 'views_num', fractions }, + { from: 'video', by: 'comments_count', fractions }, + { from: 'video', by: 'reactions_count', fractions }, + ]) + this.percentileStats = { + channelFollowers: channelFollowersPercentiles, + crtLiquidity: crtLiquidityPercentiles, + crtVolume: crtVolumePercentiles, + videoViews: videoViewsPercentiles, + videoComments: videoCommentsPercentiles, + videoReactions: videoReactionsPercentiles, + channelRevenue: channelRevenuePercentiles, + } + this.logger.info({ ...this.percentileStats }, 'Updated percentile stats') + } + + private async populateBackgroundQueue(): Promise { + // Only select channels with at least 1 uploaded video + const result = await this.em.query( + `SELECT id FROM curator.channel WHERE total_videos_created > 0` + ) + const ids = result.map((row: { id: string }) => row.id) + this.logger.info(`Populating background queue. Found ${ids.length} channels`) + for (const id of ids) { + this.backgroundQueue.add(id) + } + } + + private async runPopulateBackgroundQueueLoop(): Promise { + const { populateBackgroundQueueInterval } = this.config + while (true) { + await this.populateBackgroundQueue() + await new Promise((resolve) => setTimeout(resolve, populateBackgroundQueueInterval)) + await this.updatePercentileStats() + } + } + + private async runUpdateLoop(): Promise { + const { updateLoopInterval } = this.config + while (true) { + await this.runSingleUpdate() + await new Promise((resolve) => setTimeout(resolve, updateLoopInterval)) + } + } +} diff --git a/src/relevance-service/main.ts b/src/relevance-service/main.ts new file mode 100644 index 000000000..e11c718fd --- /dev/null +++ b/src/relevance-service/main.ts @@ -0,0 +1,20 @@ +import { config, ConfigVariable } from '../utils/config' +import { globalEm } from '../utils/globalEm' +import { RelevanceQueueConsumer } from './RelevanceQueue' +import { RelevanceService } from './RelevanceService' + +async function main() { + const em = await globalEm + const relevanceQueue = await RelevanceQueueConsumer.init({ + onRestartSignal: () => { + // Let the docker handle the restart + process.exit(0) + }, + }) + const serviceConfig = await config.get(ConfigVariable.RelevanceServiceConfig, em) + const weights = await config.get(ConfigVariable.RelevanceWeights, em) + const service = new RelevanceService(em, relevanceQueue, serviceConfig, weights) + await service.run() +} + +main().catch(console.error) diff --git a/src/server-extension/check.ts b/src/server-extension/check.ts index f0c650999..c6e0d13e8 100644 --- a/src/server-extension/check.ts +++ b/src/server-extension/check.ts @@ -3,6 +3,7 @@ import { TypeormOpenreaderContext } from '@subsquid/graphql-server/lib/typeorm' import { Context as OpenreaderContext } from '@subsquid/openreader/lib/context' import { UnauthorizedError } from 'type-graphql' import { AuthContext, authenticate } from '../utils/auth' +import { OperatorPermission } from '../model' export type Context = OpenreaderContext & AuthContext @@ -44,10 +45,16 @@ export const requestCheck: RequestCheckFunction = async (ctx) => { throw new UnauthorizedError() } - // Set search_path accordingly if it's an operator request - if (authContext?.user.isRoot) { + // Set search_path accordingly to user's permissions + if (authContext?.user) { const em = await (context.openreader as unknown as TypeormOpenreaderContext).getEntityManager() - await em.query('SET LOCAL search_path TO admin,public') + const { user } = authContext + const permissions = user.permissions || [] + if (user.isRoot || permissions.includes(OperatorPermission.VIEW_ADMIN_SCHEMA)) { + await em.query('SET LOCAL search_path TO admin,curator,public') + } else if (permissions.includes(OperatorPermission.VIEW_CURATOR_SCHEMA)) { + await em.query('SET LOCAL search_path TO curator,public') + } } return true diff --git a/src/server-extension/resolvers/AdminResolver/index.ts b/src/server-extension/resolvers/AdminResolver/index.ts index b496d9a0a..064c6b3df 100644 --- a/src/server-extension/resolvers/AdminResolver/index.ts +++ b/src/server-extension/resolvers/AdminResolver/index.ts @@ -10,10 +10,9 @@ import { getResolveTree } from '@subsquid/openreader/lib/util/resolve-tree' import { GraphQLResolveInfo } from 'graphql' import 'reflect-metadata' import { Args, Ctx, Info, Int, Mutation, Query, Resolver, UseMiddleware } from 'type-graphql' -import { EntityManager, In, Not, UpdateResult } from 'typeorm' +import { EntityManager, In, Not } from 'typeorm' import { Account, - Channel, ChannelRecipient, CreatorToken, NftFeaturedOnMarketPlace, @@ -35,7 +34,6 @@ import { AppActionSignatureInput, AppRootDomain, CommentTipTiers, - ChannelWeight, CrtMarketCapMinVolume, ExcludableContentType, ExcludeContentArgs, @@ -50,7 +48,6 @@ import { RevokeOperatorPermissionsInput, SetCategoryFeaturedVideosArgs, SetCategoryFeaturedVideosResult, - SetChannelsWeightsArgs, SetCrtMarketCapMinVolume, SetFeaturedCrtsInput, SetFeaturedCrtsResult, @@ -71,20 +68,22 @@ import { SetVideoHeroInput, SetVideoHeroResult, SetVideoViewPerUserTimeLimitInput, - SetVideoWeightsInput, VideoViewPerUserTimeLimit, - VideoWeights, + SetRelevanceWeightsArgs, + SetRelevanceWeightsResult, + SetRelevanceServiceConfigResult, + SetRelevanceServiceConfigArgs, } from './types' import { processCommentsCensorshipStatusUpdate } from './utils' -import { recalculateAllVideosRelevance } from '../../utils' import { parseVideoTitle } from '../../../utils/notification/helpers' +import { relevanceQueuePublisher } from '../../utils' @Resolver() export class AdminResolver { // Set by dependency injection constructor(private em: () => Promise) {} - @UseMiddleware(OperatorOnly()) + @UseMiddleware(OperatorOnly(OperatorPermission.SET_APP_CONFIGS)) @Mutation(() => SetNewAppAssetStorageResult) async setAppAssetStorage( @Args() args: SetNewAppAssetStorageInput @@ -94,7 +93,7 @@ export class AdminResolver { return { newAppAssetStorage: args.newAppAssetStorage } } - @UseMiddleware(OperatorOnly()) + @UseMiddleware(OperatorOnly(OperatorPermission.SET_APP_CONFIGS)) @Mutation(() => SetNewAppNameAltResult) async setAppNameAlt(@Args() args: SetNewAppNameAltInput): Promise { const em = await this.em() @@ -102,7 +101,7 @@ export class AdminResolver { return { newAppNameAlt: args.newAppNameAlt } } - @UseMiddleware(OperatorOnly()) + @UseMiddleware(OperatorOnly(OperatorPermission.SET_APP_CONFIGS)) @Mutation(() => SetNewNotificationAssetRootResult) async setNewNotificationAssetRoot( @Args() args: SetNewNotificationAssetRootInput @@ -147,24 +146,44 @@ export class AdminResolver { return { newPermissions: user.permissions } } - @UseMiddleware(OperatorOnly(OperatorPermission.SET_VIDEO_WEIGHTS)) - @Mutation(() => VideoWeights) - async setVideoWeights(@Args() args: SetVideoWeightsInput): Promise { + @UseMiddleware(OperatorOnly(OperatorPermission.SET_RELEVANCE_WEIGHTS)) + @Mutation(() => SetRelevanceWeightsResult) + async setRelevanceWeights( + @Args() args: SetRelevanceWeightsArgs + ): Promise { const em = await this.em() + const currentWeights = await config.get(ConfigVariable.RelevanceWeights, em) await config.set( ConfigVariable.RelevanceWeights, - [ - args.newnessWeight, - args.viewsWeight, - args.commentsWeight, - args.reactionsWeight, - [args.joysteamTimestampSubWeight, args.ytTimestampSubWeight], - args.defaultChannelWeight, - ], + { + channel: args.channel ?? currentWeights.channel, + video: args.video ?? currentWeights.video, + }, em ) - await recalculateAllVideosRelevance(em) - return { isApplied: true } + const updatedWeights = await config.get(ConfigVariable.RelevanceWeights, em) + await relevanceQueuePublisher.pushRestartRequest() + return { updatedWeights } + } + + @UseMiddleware(OperatorOnly(OperatorPermission.SET_RELEVANCE_CONFIG)) + @Mutation(() => SetRelevanceServiceConfigResult) + async setRelevanceServiceConfig( + @Args() args: SetRelevanceServiceConfigArgs + ): Promise { + const em = await this.em() + const currentConfig = await config.get(ConfigVariable.RelevanceServiceConfig, em) + await config.set( + ConfigVariable.RelevanceServiceConfig, + { + ...currentConfig, + ...args, + }, + em + ) + const updatedConfig = await config.get(ConfigVariable.RelevanceServiceConfig, em) + await relevanceQueuePublisher.pushRestartRequest() + return { updatedConfig } } @UseMiddleware(OperatorOnly(OperatorPermission.SET_TIP_TIERS)) @@ -183,7 +202,7 @@ export class AdminResolver { return config.get(ConfigVariable.CommentTipTiers, em) } - @UseMiddleware(OperatorOnly()) + @UseMiddleware(OperatorOnly(OperatorPermission.SET_APP_CONFIGS)) @Mutation(() => Int) async setMaxAttemptsOnMailDelivery( @Args() args: SetMaxAttemptsOnMailDeliveryInput @@ -196,20 +215,7 @@ export class AdminResolver { return { maxAttempts: args.newMaxAttempts } } - @UseMiddleware(OperatorOnly()) - @Mutation(() => Int) - async setNewNotificationCenterPath( - @Args() args: SetMaxAttemptsOnMailDeliveryInput - ): Promise { - const em = await this.em() - if (args.newMaxAttempts < 1) { - throw new Error('Max attempts cannot be less than 1') - } - await config.set(ConfigVariable.EmailNotificationDeliveryMaxAttempts, args.newMaxAttempts, em) - return { maxAttempts: args.newMaxAttempts } - } - - @UseMiddleware(OperatorOnly()) + @UseMiddleware(OperatorOnly(OperatorPermission.SET_APP_CONFIGS)) @Mutation(() => AppRootDomain) async setNewAppRootDomain(@Args() args: SetRootDomainInput): Promise { const em = await this.em() @@ -217,41 +223,6 @@ export class AdminResolver { return { isApplied: true } } - @UseMiddleware(OperatorOnly(OperatorPermission.SET_CHANNEL_WEIGHTS)) - @Mutation(() => [ChannelWeight]) - async setChannelsWeights(@Args() { inputs }: SetChannelsWeightsArgs): Promise { - const em = await this.em() - - const results: ChannelWeight[] = [] - - // Process each SetChannelWeightInput - for (const weightInput of inputs) { - const { channelId, weight } = weightInput - - // Update the channel weight in the database - const updateResult: UpdateResult = await em.transaction( - async (transactionalEntityManager) => { - return transactionalEntityManager - .createQueryBuilder() - .update(Channel) - .set({ channelWeight: weight }) - .where('id = :id', { id: channelId }) - .execute() - } - ) - - await recalculateAllVideosRelevance(em) - - // Push the result into the results array - results.push({ - channelId, - isApplied: !!updateResult.affected, - }) - } - - return results - } - @UseMiddleware(OperatorOnly(OperatorPermission.SET_CRT_MARKETCAP_MIN_VOLUME)) @Mutation(() => CrtMarketCapMinVolume) async setCrtMarketCapMinVolume(@Args() args: SetCrtMarketCapMinVolume) { @@ -467,6 +438,7 @@ export class AdminResolver { @Args() { ids, type }: ExcludeContentArgs ): Promise { + // TODO: Consider adding rationale and notification const em = await this.em() return withHiddenEntities(em, async () => { @@ -493,6 +465,7 @@ export class AdminResolver { @Args() { ids, type }: RestoreContentArgs ): Promise { + // TODO: Consider adding rationale and notification const em = await this.em() return withHiddenEntities(em, async () => { diff --git a/src/server-extension/resolvers/AdminResolver/types.ts b/src/server-extension/resolvers/AdminResolver/types.ts index 41541ea09..6c0e41f51 100644 --- a/src/server-extension/resolvers/AdminResolver/types.ts +++ b/src/server-extension/resolvers/AdminResolver/types.ts @@ -1,29 +1,129 @@ import { AppAction } from '@joystream/metadata-protobuf' import { ArgsType, Field, Float, InputType, Int, ObjectType, registerEnumType } from 'type-graphql' import { OperatorPermission } from '../../../model' +import { SumTo } from '../validators' -@ArgsType() -export class SetVideoWeightsInput { +@InputType('ChannelRelevanceWeightsInput') +@ObjectType('ChannelRelevanceWeights') +export class ChannelRelevanceWeights { @Field(() => Float, { nullable: false }) - newnessWeight!: number + crtVolumeWeight!: number @Field(() => Float, { nullable: false }) - viewsWeight!: number + crtLiquidityWeight!: number @Field(() => Float, { nullable: false }) - commentsWeight!: number + followersWeight!: number @Field(() => Float, { nullable: false }) - reactionsWeight!: number + revenueWeight!: number @Field(() => Float, { nullable: false }) - joysteamTimestampSubWeight!: number + yppTierWeight!: number +} +@InputType('AgeSubWeightsInput') +@ObjectType('AgeSubWeights') +export class AgeSubWeights { @Field(() => Float, { nullable: false }) - ytTimestampSubWeight!: number + joystreamAgeWeight!: number @Field(() => Float, { nullable: false }) - defaultChannelWeight!: number + youtubeAgeWeight!: number +} + +@InputType('VideoRelevanceWeightsInput') +@ObjectType('VideoRelevanceWeights') +export class VideoRelevanceWeights { + @Field(() => Float, { nullable: false }) + ageWeight!: number + + @Field(() => AgeSubWeights, { nullable: false }) + @SumTo(1) + ageSubWeights!: AgeSubWeights + + @Field(() => Float, { nullable: false }) + viewsWeight!: number + + @Field(() => Float, { nullable: false }) + commentsWeight!: number + + @Field(() => Float, { nullable: false }) + reactionsWeight!: number +} + +@ArgsType() +export class SetRelevanceWeightsArgs { + @Field(() => ChannelRelevanceWeights, { nullable: true }) + @SumTo(1, { type: '<=' }) + channel?: ChannelRelevanceWeights + + @Field(() => VideoRelevanceWeights, { nullable: true }) + @SumTo(1, { omit: ['ageSubWeights'], type: '==' }) + video?: VideoRelevanceWeights +} + +@ObjectType() +export class RelevanceWeights { + @Field(() => ChannelRelevanceWeights, { nullable: false }) + channel!: ChannelRelevanceWeights + + @Field(() => VideoRelevanceWeights, { nullable: false }) + video!: VideoRelevanceWeights +} + +@ObjectType() +export class SetRelevanceWeightsResult { + @Field(() => RelevanceWeights, { nullable: false }) + updatedWeights!: RelevanceWeights +} + +@ArgsType() +export class SetRelevanceServiceConfigArgs { + @Field(() => Int, { nullable: true }) + populateBackgroundQueueInterval?: number + + @Field(() => Int, { nullable: true }) + updateLoopInterval?: number + + @Field(() => Int, { nullable: true }) + channelsPerIteration?: number + + @Field(() => Int, { nullable: true }) + videosPerChannelLimit?: number + + @Field(() => Int, { nullable: true }) + videosPerChannelSelectTop?: number + + @Field(() => Int, { nullable: true }) + ageScoreHalvingDays?: number +} + +@ObjectType() +export class RelevanceServiceConfig { + @Field(() => Int, { nullable: false }) + populateBackgroundQueueInterval!: number + + @Field(() => Int, { nullable: false }) + updateLoopInterval!: number + + @Field(() => Int, { nullable: false }) + channelsPerIteration!: number + + @Field(() => Int, { nullable: false }) + videosPerChannelLimit!: number + + @Field(() => Int, { nullable: false }) + videosPerChannelSelectTop!: number + + @Field(() => Int, { nullable: false }) + ageScoreHalvingDays!: number +} + +@ObjectType() +export class SetRelevanceServiceConfigResult { + @Field(() => RelevanceServiceConfig, { nullable: false }) + updatedConfig!: RelevanceServiceConfig } @ArgsType() @@ -74,36 +174,6 @@ export class AppRootDomain { isApplied!: boolean } -@ObjectType() -export class VideoWeights { - @Field(() => Boolean, { nullable: false }) - isApplied!: boolean -} - -@InputType() -export class ChannelWeightInput { - @Field(() => String, { nullable: false }) - channelId!: string - - @Field(() => Float, { nullable: false }) - weight!: number -} - -@ArgsType() -export class SetChannelsWeightsArgs { - @Field(() => [ChannelWeightInput], { nullable: false }) - inputs!: ChannelWeightInput[] -} - -@ObjectType() -export class ChannelWeight { - @Field(() => String, { nullable: false }) - channelId!: string - - @Field(() => Boolean, { nullable: false }) - isApplied!: boolean -} - @ArgsType() export class SetCrtMarketCapMinVolume { @Field(() => Int, { nullable: false }) diff --git a/src/server-extension/resolvers/AdminResolver/utils.ts b/src/server-extension/resolvers/AdminResolver/utils.ts index 23a7fd780..d6a4c4387 100644 --- a/src/server-extension/resolvers/AdminResolver/utils.ts +++ b/src/server-extension/resolvers/AdminResolver/utils.ts @@ -1,13 +1,20 @@ import { EntityManager, In } from 'typeorm' import { Comment } from '../../../model' -import { commentCountersManager } from '../../utils' +import { commentCountersManager, relevanceQueuePublisher } from '../../utils' +import _ from 'lodash' export async function processCommentsCensorshipStatusUpdate(em: EntityManager, ids: string[]) { - const comments = await em.getRepository(Comment).find({ where: { id: In(ids) } }) - comments.forEach((c) => { - commentCountersManager.scheduleRecalcForComment(c.parentCommentId) - commentCountersManager.scheduleRecalcForVideo(c.videoId) - }) + const comments = await em + .getRepository(Comment) + .find({ where: { id: In(ids) }, relations: ['video'] }) + + await Promise.all( + comments.map(async (c) => { + commentCountersManager.scheduleRecalcForComment(c.parentCommentId) + commentCountersManager.scheduleRecalcForVideo(c.videoId) + await relevanceQueuePublisher.pushChannel(c.video.channelId) + }) + ) await commentCountersManager.updateVideoCommentsCounters(em) await commentCountersManager.updateParentRepliesCounters(em) } diff --git a/src/server-extension/resolvers/ChannelsResolver/index.ts b/src/server-extension/resolvers/ChannelsResolver/index.ts index 82395a5d5..21ce1e61e 100644 --- a/src/server-extension/resolvers/ChannelsResolver/index.ts +++ b/src/server-extension/resolvers/ChannelsResolver/index.ts @@ -16,12 +16,12 @@ import { ChannelNftCollectorsOrderByInput, TopSellingChannelsArgs, TopSellingChannelsResult, - ExcludeChannelArgs, - ExcludeChannelResult, - VerifyChannelArgs, - VerifyChannelResult, - SuspendChannelResult, - SuspendChannelArgs, + SetChannelYppStatusResult, + SetChannelYppStatusArgs, + SetChannelYppStatusInput, + ChannelYppInputStatus, + SetChannelYtSyncEnabledArgs, + SetChannelYtSyncEnabledResult, } from './types' import { GraphQLResolveInfo } from 'graphql' import { @@ -30,17 +30,16 @@ import { ChannelFollow, Report, Membership, - Exclusion, NewChannelFollower, - ChannelExcluded, ChannelVerified, - ChannelVerification, - ChannelSuspension, - ChannelSuspended, YppVerified, YppSuspended, ChannelRecipient, - MemberRecipient, + ChannelTier, + OperatorPermission, + ChannelYppStatus, + YppUnverified, + ChannelSuspended, } from '../../../model' import { extendClause, withHiddenEntities } from '../../../utils/sql' import { buildExtendedChannelsQuery, buildTopSellingChannelsQuery } from './utils' @@ -54,7 +53,8 @@ import { AccountOnly, OperatorOnly, UserOnly } from '../middleware' import { addNotification } from '../../../utils/notification' import { assertNotNull } from '@subsquid/substrate-processor' import pLimit from 'p-limit' -import { parseChannelTitle } from '../../../utils/notification/helpers' +import { config, ConfigVariable } from '../../../utils/config' +import { relevanceQueuePublisher } from '../../utils' @Resolver() export class ChannelsResolver { @@ -223,6 +223,11 @@ export class ChannelsResolver { timestamp: new Date(), }) + const tick = await config.get(ConfigVariable.ChannelWeightFollowsTick, em) + if (channel.followsNum % tick === 0) { + await relevanceQueuePublisher.pushChannel(channel.id) + } + const ownerAccount = channel.ownerMemberId ? await em.getRepository(Account).findOneBy({ membershipId: channel.ownerMemberId }) : null @@ -340,196 +345,150 @@ export class ChannelsResolver { }) } - @Mutation(() => [SuspendChannelResult]) - @UseMiddleware(OperatorOnly()) - async suspendChannels( - @Args() { channelIds }: SuspendChannelArgs - ): Promise { + @Mutation(() => [SetChannelYppStatusResult]) + @UseMiddleware(OperatorOnly(OperatorPermission.SET_CHANNEL_YPP_STATUS)) + async setChannelYppStatus( + @Args() { channels, skipNotification }: SetChannelYppStatusArgs + ): Promise { const em = await this.em() + return await setChannelsYppStatus(em, channels, skipNotification) + } + @Mutation(() => SetChannelYtSyncEnabledResult) + @UseMiddleware(OperatorOnly(OperatorPermission.SET_CHANNEL_YPP_STATUS)) + async setChannelYoutubeSyncEnabled( + @Args() { ids, isYtSyncEnabled }: SetChannelYtSyncEnabledArgs + ): Promise { + const em = await this.em() return withHiddenEntities(em, async () => { - const channels = await em.find(Channel, { - where: { id: In(channelIds) }, - }) - - const suspendChannel = async (channel: Channel) => { - // If channel already suspended - return its data - if (channel.yppStatus.isTypeOf === 'YppSuspended') { - const existingSuspension = await em.getRepository(ChannelSuspension).findOneOrFail({ - where: { id: channel.yppStatus.suspension }, - }) - return { - id: existingSuspension.id, - channelId: channel.id, - createdAt: existingSuspension.timestamp, - } - } - // otherwise create a new suspension - const newSuspension = new ChannelSuspension({ - id: uniqueId(), - channelId: channel.id, - timestamp: new Date(), - }) - channel.yppStatus = new YppSuspended({ suspension: newSuspension.id }) - await em.save([newSuspension, channel]) - - // in case account exist deposit notification - const channelOwnerMemberId = channel.ownerMemberId - if (channelOwnerMemberId) { - const account = await em.findOne(Account, { - where: { membershipId: channelOwnerMemberId }, - }) - await addNotification( - em, - account, - new ChannelRecipient({ channel: channel.id }), - new ChannelSuspended({}) - ) - } - - return { - id: newSuspension.id, - channelId: channel.id, - createdAt: newSuspension.timestamp, - } - } - - const limit = pLimit(5) // Limit to 5 concurrent promises - const existingChannels = channels.filter((channel) => channel) - return await Promise.all( - existingChannels.map((channel) => limit(async () => await suspendChannel(channel))) - ) + const result = await em.getRepository(Channel).update({ id: In(ids) }, { isYtSyncEnabled }) + return { updatedChannels: result.affected || 0 } }) } +} - @Mutation(() => VerifyChannelResult) - @UseMiddleware(OperatorOnly()) - async verifyChannel(@Args() { channelIds }: VerifyChannelArgs): Promise { - const em = await this.em() - return await verifyChannelService(em, channelIds) +export const channelYppStatusToInputStatus = ( + status?: ChannelYppStatus | null +): ChannelYppInputStatus => { + if (status?.isTypeOf === 'YppVerified') { + switch (status.tier) { + case ChannelTier.BRONZE: + return ChannelYppInputStatus.VerifiedBronze + case ChannelTier.SILVER: + return ChannelYppInputStatus.VerifiedSilver + case ChannelTier.GOLD: + return ChannelYppInputStatus.VerifiedGold + case ChannelTier.DIAMOND: + return ChannelYppInputStatus.VerifiedDiamond + default: + throw new Error(`Unknown ChannelTier: ${status.tier}`) + } + } + if (status?.isTypeOf === 'YppSuspended') { + return ChannelYppInputStatus.Suspended + } + if (status?.isTypeOf === 'YppUnverified') { + return ChannelYppInputStatus.Unverified } + return ChannelYppInputStatus.Empty +} - @Mutation(() => ExcludeChannelResult) - @UseMiddleware(OperatorOnly()) - async excludeChannel( - @Args() { channelId, rationale }: ExcludeChannelArgs - ): Promise { - const em = await this.em() - return await excludeChannelService(em, channelId, rationale) +export const channelYppInputStatusToStatus = ( + status: ChannelYppInputStatus, + timestamp: Date +): ChannelYppStatus | null => { + switch (status) { + case ChannelYppInputStatus.VerifiedBronze: + return new YppVerified({ tier: ChannelTier.BRONZE, timestamp }) + case ChannelYppInputStatus.VerifiedSilver: + return new YppVerified({ tier: ChannelTier.SILVER, timestamp }) + case ChannelYppInputStatus.VerifiedGold: + return new YppVerified({ tier: ChannelTier.GOLD, timestamp }) + case ChannelYppInputStatus.VerifiedDiamond: + return new YppVerified({ tier: ChannelTier.DIAMOND, timestamp }) + case ChannelYppInputStatus.Suspended: + return new YppSuspended({ timestamp }) + case ChannelYppInputStatus.Unverified: + return new YppUnverified({ timestamp }) + case ChannelYppInputStatus.Empty: + return null + default: + throw new Error(`Unknown ChannelYppInputStatus: ${status}`) } } -export const excludeChannelService = async ( +export const setChannelYppStatus = async ( em: EntityManager, - channelId: string, - rationale: string -): Promise => { - return withHiddenEntities(em, async () => { - const channel = await em.findOne(Channel, { - where: { id: channelId }, - }) - - if (!channel) { - throw new Error(`Channel by id ${channelId} not found!`) - } - const existingExclusion = await em.findOne(Exclusion, { - where: { channelId: channel.id, videoId: IsNull() }, - }) - // If exclusion already exists - return its data with { created: false } - if (existingExclusion) { - return { - id: existingExclusion.id, - channelId: channel.id, - created: false, - createdAt: existingExclusion.timestamp, - rationale: existingExclusion.rationale, - } - } - // If exclusion doesn't exist, create a new one - const newExclusion = new Exclusion({ - id: uniqueId(8), - channelId: channel.id, - rationale, - timestamp: new Date(), + channel: Channel, + status: ChannelYppInputStatus, + skipNotification = false +): Promise => { + const currentTimestamp = new Date() + const previousStatus = channelYppStatusToInputStatus(channel.yppStatus) + const newStatus = status + const changed = previousStatus !== newStatus + const result: SetChannelYppStatusResult = { + id: channel.id, + newStatus, + previousStatus, + timestamp: changed ? currentTimestamp : channel.yppStatus?.timestamp, + updated: false, + notificationAdded: false, + } + // If channel status didn't change - just return the current status + if (!changed) { + return result + } + // othewise update the status + channel.yppStatus = channelYppInputStatusToStatus(status, currentTimestamp) + await em.save(channel) + result.updated = true + + const statusType = channel.yppStatus?.isTypeOf + // Deposit notification if: + // 1. skipNotification is false + // 2. channel has an owner member ID and an associated account + // 3. status changed to either YppSuspended or YppVerified + if ( + !skipNotification && + channel.ownerMemberId && + (statusType === 'YppSuspended' || statusType === 'YppVerified') + ) { + const account = await em.findOne(Account, { + where: { membershipId: channel.ownerMemberId }, }) - channel.isExcluded = true - await em.save([newExclusion, channel]) - - // in case account exist deposit notification - const channelOwnerMemberId = channel.ownerMemberId - if (channelOwnerMemberId) { - const account = await em.findOne(Account, { where: { membershipId: channelOwnerMemberId } }) + if (account) { await addNotification( em, account, - new MemberRecipient({ membership: channelOwnerMemberId }), - new ChannelExcluded({ channelTitle: parseChannelTitle(channel) }) + new ChannelRecipient({ channel: channel.id }), + statusType === 'YppVerified' ? new ChannelVerified({}) : new ChannelSuspended({}) ) + result.notificationAdded = true } + } - return { - id: newExclusion.id, - channelId: channel.id, - videoId: null, - created: true, - createdAt: newExclusion.timestamp, - rationale, - } - }) + return result } -export const verifyChannelService = async (em: EntityManager, channelIds: string[]) => { +export const setChannelsYppStatus = async ( + em: EntityManager, + inputs: SetChannelYppStatusInput[], + skipNotification = false +): Promise => { return withHiddenEntities(em, async () => { const channels = await em.getRepository(Channel).find({ - where: { id: In(channelIds) }, + where: { id: In(inputs.map(({ id }) => id)) }, }) - - const verifyChannel = async (channel: Channel) => { - // If channel already verified - return its data - if (channel.yppStatus.isTypeOf === 'YppVerified') { - const existingVerification = await em.getRepository(ChannelVerification).findOneOrFail({ - where: { channelId: channel.id }, - }) - return { - id: existingVerification.id, - channelId: channel.id, - createdAt: existingVerification.timestamp, - } - } - // othewise create new verification regardless whether the channel was previously verified - const newVerification = new ChannelVerification({ - id: uniqueId(), - channelId: channel.id, - timestamp: new Date(), - }) - channel.yppStatus = new YppVerified({ verification: newVerification.id }) - await em.save([newVerification, channel]) - - // in case account exist deposit notification - const channelOwnerMemberId = channel.ownerMemberId - if (channelOwnerMemberId) { - const account = await em.findOne(Account, { - where: { membershipId: channelOwnerMemberId }, - }) - await addNotification( - em, - account, - new ChannelRecipient({ channel: channel.id }), - new ChannelVerified({}) - ) - } - - return { - id: newVerification.id, - channelId: channel.id, - createdAt: newVerification.timestamp, - } - } - const limit = pLimit(5) // Limit to 5 concurrent promises - const existingChannels = channels.filter((channel) => channel) + const channelUpdates = channels.flatMap((channel) => { + const input = inputs.find((i) => i.id === channel.id) + return input ? [[channel, input.status] as const] : [] + }) return await Promise.all( - existingChannels.map((channel) => limit(async () => await verifyChannel(channel))) + channelUpdates.map(([channel, status]) => + limit(() => setChannelYppStatus(em, channel, status, skipNotification)) + ) ) }) } diff --git a/src/server-extension/resolvers/ChannelsResolver/types.ts b/src/server-extension/resolvers/ChannelsResolver/types.ts index d7ddc3283..1ec50034b 100644 --- a/src/server-extension/resolvers/ChannelsResolver/types.ts +++ b/src/server-extension/resolvers/ChannelsResolver/types.ts @@ -178,54 +178,69 @@ export class ChannelsSearchArgs { limit?: number } -@ArgsType() -export class SuspendChannelArgs { - @Field(() => [String], { nullable: false }) - channelIds!: string[] +export enum ChannelYppInputStatus { + Suspended, + Unverified, + Empty, + VerifiedBronze, + VerifiedSilver, + VerifiedGold, + VerifiedDiamond, +} +registerEnumType(ChannelYppInputStatus, { + name: 'ChannelYppInputStatus', +}) + +@InputType() +export class SetChannelYppStatusInput { + @Field(() => String, { nullable: false }) + id!: string + + @Field(() => ChannelYppInputStatus, { nullable: false }) + status!: ChannelYppInputStatus } @ArgsType() -export class VerifyChannelArgs { - @Field(() => [String], { nullable: false }) - channelIds!: string[] +export class SetChannelYppStatusArgs { + @Field(() => [SetChannelYppStatusInput], { nullable: false }) + channels!: SetChannelYppStatusInput[] + + @Field(() => Boolean, { nullable: true }) + skipNotification?: boolean } @ObjectType() -export class SuspendChannelResult { +export class SetChannelYppStatusResult { @Field(() => String, { nullable: false }) id!: string - @Field(() => String, { nullable: false }) - channelId!: string + @Field(() => ChannelYppInputStatus, { nullable: false }) + previousStatus!: ChannelYppInputStatus - @Field(() => DateTime, { nullable: false }) - createdAt!: Date -} + @Field(() => ChannelYppInputStatus, { nullable: false }) + newStatus!: ChannelYppInputStatus -@ObjectType() -export class VerifyChannelResult { - @Field(() => String, { nullable: false }) - id!: string + @Field(() => DateTime, { nullable: true }) + timestamp?: Date - @Field(() => String, { nullable: false }) - channelId!: string + @Field(() => Boolean, { nullable: false }) + updated!: boolean - @Field(() => DateTime, { nullable: false }) - createdAt!: Date + @Field(() => Boolean, { nullable: false }) + notificationAdded!: boolean } @ArgsType() -export class ExcludeChannelArgs { - @Field(() => String, { nullable: false }) - channelId!: string +export class SetChannelYtSyncEnabledArgs { + @Field(() => [String], { nullable: false }) + ids!: string[] - @Field(() => String, { nullable: false }) - @MaxLength(400, { message: 'Rationale cannot be longer than 400 characters' }) - rationale!: string + @Field(() => Boolean, { nullable: false }) + isYtSyncEnabled!: boolean } @ObjectType() -export class ExcludeChannelResult extends EntityReportInfo { - @Field(() => String, { nullable: false }) - channelId!: string +export class SetChannelYtSyncEnabledResult { + @Field(() => Int, { nullable: false }) + updatedChannels!: number } diff --git a/src/server-extension/resolvers/StateResolver/index.ts b/src/server-extension/resolvers/StateResolver/index.ts index 1969854a5..becc0c2f2 100644 --- a/src/server-extension/resolvers/StateResolver/index.ts +++ b/src/server-extension/resolvers/StateResolver/index.ts @@ -91,7 +91,7 @@ export class StateResolver { entity_id, SUM(count) as entryCount FROM - admin.user_interaction_count + curator.user_interaction_count WHERE type = $1 AND day_timestamp >= NOW() - INTERVAL '${args.period} DAYS' GROUP BY diff --git a/src/server-extension/resolvers/VideosResolver/index.ts b/src/server-extension/resolvers/VideosResolver/index.ts index bbd36f3b1..a8ca21fff 100644 --- a/src/server-extension/resolvers/VideosResolver/index.ts +++ b/src/server-extension/resolvers/VideosResolver/index.ts @@ -21,20 +21,10 @@ import { isObject } from 'lodash' import 'reflect-metadata' import { Arg, Args, Ctx, Info, Mutation, Query, Resolver, UseMiddleware } from 'type-graphql' import { EntityManager, In, MoreThan } from 'typeorm' -import { - Account, - ChannelRecipient, - Exclusion, - OperatorPermission, - Report, - Video, - VideoExcluded, - VideoViewEvent, -} from '../../../model' +import { OperatorPermission, Report, Video, VideoViewEvent } from '../../../model' import { ConfigVariable, config } from '../../../utils/config' import { uniqueId } from '../../../utils/crypto' import { has } from '../../../utils/misc' -import { addNotification } from '../../../utils/notification' import { extendClause, overrideClause, withHiddenEntities } from '../../../utils/sql' import { Context } from '../../check' import { Video as VideoReturnType, VideosConnection } from '../baseTypes' @@ -43,7 +33,6 @@ import { model } from '../model' import { AddVideoViewResult, DumbPublicFeedArgs, - ExcludeVideoInfo, MostViewedVideosConnectionArgs, PublicFeedOperationType, ReportVideoArgs, @@ -51,8 +40,7 @@ import { SetOrUnsetPublicFeedResult, VideoReportInfo, } from './types' -import { videoRelevanceManager } from '../../utils' -import { parseVideoTitle } from '../../../utils/notification/helpers' +import { relevanceQueuePublisher } from '../../utils' @Resolver() export class VideosResolver { @@ -309,8 +297,8 @@ export class VideosResolver { }) const tick = await config.get(ConfigVariable.VideoRelevanceViewsTick, em) - if (video.viewsNum % tick === 0) { - videoRelevanceManager.scheduleRecalcForChannel(video.channelId) + if (video.viewsNum % tick === 0 && video.channelId) { + await relevanceQueuePublisher.pushChannel(video.channelId) } await em.save([video, video.channel, newView]) return { @@ -373,72 +361,4 @@ export class VideosResolver { } }) } - - @Mutation(() => ExcludeVideoInfo) - @UseMiddleware(OperatorOnly()) - async excludeVideo(@Args() { videoId, rationale }: ReportVideoArgs): Promise { - return excludeVideoService(await this.em(), videoId, rationale) - } -} - -export const excludeVideoService = async ( - em: EntityManager, - videoId: string, - rationale: string -) => { - return withHiddenEntities(em, async () => { - const video = await em.findOne(Video, { - where: { id: videoId }, - relations: { channel: true }, - }) - - if (!video) { - throw new Error(`Video by id ${videoId} not found!`) - } - - const existingExclusion = await em.findOne(Exclusion, { - where: { channelId: video.channel.id, videoId }, - }) - // If exclusion already exists - return its data with { created: false } - if (existingExclusion) { - return { - id: existingExclusion.id, - channelId: video.channel.id, - videoId, - created: false, - createdAt: existingExclusion.timestamp, - rationale: existingExclusion.rationale, - } - } - // If exclusion doesn't exist, create a new one - const newExclusion = new Exclusion({ - id: uniqueId(8), - channelId: video.channel.id, - videoId, - rationale, - timestamp: new Date(), - }) - video.isExcluded = true - await em.save([newExclusion, video]) - - // in case account exist deposit notification - const channelOwnerMemberId = video.channel.ownerMemberId - if (channelOwnerMemberId) { - const account = await em.findOne(Account, { where: { membershipId: channelOwnerMemberId } }) - await addNotification( - em, - account, - new ChannelRecipient({ channel: video.channel.id }), - new VideoExcluded({ videoTitle: parseVideoTitle(video) }) - ) - } - - return { - id: newExclusion.id, - videoId, - created: true, - createdAt: newExclusion.timestamp, - rationale, - } - }) } diff --git a/src/server-extension/resolvers/VideosResolver/types.ts b/src/server-extension/resolvers/VideosResolver/types.ts index 949b573e1..25232aab1 100644 --- a/src/server-extension/resolvers/VideosResolver/types.ts +++ b/src/server-extension/resolvers/VideosResolver/types.ts @@ -79,22 +79,6 @@ export class ReportVideoArgs { rationale!: string } -@ArgsType() -export class ExcludeVideoArgs { - @Field(() => String, { nullable: false }) - videoId!: string - - @Field(() => String, { nullable: false }) - @MaxLength(400, { message: 'Rationale cannot be longer than 400 characters' }) - rationale!: string -} - -@ObjectType() -export class ExcludeVideoInfo extends EntityReportInfo { - @Field(() => String, { nullable: false }) - videoId!: string -} - @ArgsType() export class DumbPublicFeedArgs { @Field(() => VideoWhereInput, { nullable: true }) diff --git a/src/server-extension/resolvers/baseTypes.ts b/src/server-extension/resolvers/baseTypes.ts index 41d24acae..be59b781d 100644 --- a/src/server-extension/resolvers/baseTypes.ts +++ b/src/server-extension/resolvers/baseTypes.ts @@ -1,5 +1,4 @@ import { Field, Int, ObjectType, registerEnumType } from 'type-graphql' - import { GraphQLScalarType } from 'graphql' // We only need to define one field for each type, which matches with the autogenerated type's field diff --git a/src/server-extension/resolvers/validators.ts b/src/server-extension/resolvers/validators.ts new file mode 100644 index 000000000..5345bf589 --- /dev/null +++ b/src/server-extension/resolvers/validators.ts @@ -0,0 +1,71 @@ +import { + registerDecorator, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, + ValidationArguments, +} from 'class-validator' +import _ from 'lodash' + +type SumToOptions = { + type?: '==' | '>' | '>=' | '<' | '<=' + precision?: number + pick?: string[] + omit?: string[] +} + +// SumTo constraint +@ValidatorConstraint({ name: 'SumTo', async: false }) +class SumToConstraint implements ValidatorConstraintInterface { + private prepareValue(obj: Record, options: SumToOptions) { + if (options.pick) { + obj = _.pick(obj, options.pick) + } + if (options.omit) { + obj = _.omit(obj, options.omit) + } + return obj + } + + validate(obj: Record, args: ValidationArguments) { + const [targetSum, options] = args.constraints as [number, SumToOptions] + obj = this.prepareValue(obj, options) + const sum = _.round(_.sum(Object.values(obj)), options.precision ?? 8) + switch (options.type) { + case '>': + return sum > targetSum + case '>=': + return sum >= targetSum + case '<': + return sum < targetSum + case '<=': + return sum <= targetSum + default: + return sum === targetSum + } + } + + defaultMessage(args: ValidationArguments) { + const [targetSum, options] = args.constraints as [number, SumToOptions] + const props = Object.keys(this.prepareValue(args.value, options)) + return `Sum of properties: ${props.join(', ')} must be ${options.type || '=='} ${targetSum}` + } +} + +// SumTo decorator +export function SumTo( + targetSum: number, + options?: SumToOptions, + validationOptions?: ValidationOptions +) { + return function (object: object, propertyName: string) { + registerDecorator({ + name: 'SumTo', + target: object.constructor, + propertyName, + options: validationOptions, + constraints: [targetSum, options || {}], + validator: SumToConstraint, + }) + } +} diff --git a/src/server-extension/subscribers/TransactionCommitSubscriber.ts b/src/server-extension/subscribers/TransactionCommitSubscriber.ts new file mode 100644 index 000000000..9f4c28d95 --- /dev/null +++ b/src/server-extension/subscribers/TransactionCommitSubscriber.ts @@ -0,0 +1,9 @@ +import { EntitySubscriberInterface, EventSubscriber } from 'typeorm' +import { relevanceQueuePublisher } from '../utils' + +@EventSubscriber() +export class TransactionCommitSubscriber implements EntitySubscriberInterface { + async afterTransactionCommit(): Promise { + await relevanceQueuePublisher.commitDeferred() + } +} diff --git a/src/server-extension/utils.ts b/src/server-extension/utils.ts index 1e7f3ecd8..54c3f9611 100644 --- a/src/server-extension/utils.ts +++ b/src/server-extension/utils.ts @@ -1,12 +1,11 @@ -import { EntityManager } from 'typeorm' +import { RelevanceQueuePublisher } from '../relevance-service/RelevanceQueue' import { CommentCountersManager } from '../utils/CommentsCountersManager' -import { VideoRelevanceManager } from '../utils/VideoRelevanceManager' export const commentCountersManager = new CommentCountersManager() -export const videoRelevanceManager = new VideoRelevanceManager() - -videoRelevanceManager.turnOnVideoRelevanceManager() - -export async function recalculateAllVideosRelevance(em: EntityManager) { - return videoRelevanceManager.updateVideoRelevanceValue(em, true) -} +export const relevanceQueuePublisher = new RelevanceQueuePublisher({ + autoInitialize: true, + defaultPushOptions: { + deferred: true, + skipIfUninitialized: false, + }, +}) diff --git a/src/tests/integration/notifications.test.ts b/src/tests/integration/notifications.test.ts index 6ff931146..94ca403a2 100644 --- a/src/tests/integration/notifications.test.ts +++ b/src/tests/integration/notifications.test.ts @@ -12,35 +12,25 @@ import { backwardCompatibleMetaID } from '../../mappings/utils' import { Account, Channel, - ChannelExcluded, ChannelRecipient, - ChannelVerification, CommentPostedToVideo, CommentReply, - Exclusion, MemberRecipient, - NextEntityId, NftFeaturedOnMarketPlace, NftOwnerChannel, Notification, NotificationEmailDelivery, + NotificationType, OwnedNft, - Video, VideoLiked, } from '../../model' import { setFeaturedNftsInner } from '../../server-extension/resolvers/AdminResolver' -import { - excludeChannelService, - verifyChannelService, -} from '../../server-extension/resolvers/ChannelsResolver' -import { excludeVideoService } from '../../server-extension/resolvers/VideosResolver' import { globalEm } from '../../utils/globalEm' -import { - OFFCHAIN_NOTIFICATION_ID_TAG, - RUNTIME_NOTIFICATION_ID_TAG, -} from '../../utils/notification/helpers' -import { AnyEntity, Constructor, EntityManagerOverlay } from '../../utils/overlay' +import { EntityManagerOverlay } from '../../utils/overlay' import { defaultTestBlock, populateDbWithSeedData } from './testUtils' +import assert from 'assert' +import { MembersMemberRemarkedEvent } from '../../types/events' +import { SubstrateBlock } from '@subsquid/substrate-processor' dontenvConfig({ path: path.resolve(__dirname, './.env'), @@ -50,237 +40,93 @@ const metadataToBytes = (metaClass: AnyMetadataClass, obj: T): Uint8Array return Buffer.from(metaClass.encode(obj).finish()) } -const getNextNotificationId = async ( - store: EntityManager | EntityManagerOverlay, - onchain: boolean -) => { - const tag = onchain ? RUNTIME_NOTIFICATION_ID_TAG : OFFCHAIN_NOTIFICATION_ID_TAG - if (store instanceof EntityManagerOverlay) { - const row = await store - .getRepository(NextEntityId as Constructor) - .getOneBy({ entityName: tag }) - const id = parseInt(row?.nextId.toString() || '1') - return id +const withOverlayTransaction = async (cb: (overlay: EntityManagerOverlay) => Promise) => { + const em = await globalEm + const runner = em.connection.createQueryRunner() + await runner.connect() + await runner.startTransaction() + + const overlay = await EntityManagerOverlay.create( + new Store(async () => runner.manager), + // onDbUpdate + async () => { + /* Do nothing */ + } + ) + await cb(overlay) + await overlay.updateDatabase() + await runner.commitTransaction() + await runner.release() +} + +const findNotification = async (em: EntityManager, by: Partial) => { + const notification = await em + .getRepository(Notification) + .createQueryBuilder('n') + .where( + Object.entries(by) + .map(([field, value]) => `n.notification_type->>'${field}' = '${value}'`) + .join(' AND ') + ) + .getOne() + if (!notification) { + throw new Error(`Could not find notification by: ${JSON.stringify({ notificationType: by })}`) } - const row = await store.getRepository(NextEntityId).findOneBy({ entityName: tag }) - const id = parseInt(row?.nextId.toString() || '1') - return id + return notification } -const createOverlay = async () => { - return await EntityManagerOverlay.create(new Store(() => globalEm), (_em: EntityManager) => - Promise.resolve() - ) +const checkNotificationEmailDelivery = async (notificationId: string) => { + const notificationEmailDelivery = await (await globalEm) + .getRepository(NotificationEmailDelivery) + .findOneOrFail({ where: { notificationId }, relations: { attempts: true } }) + expect(notificationEmailDelivery.discard).to.be.false + expect(notificationEmailDelivery.attempts).to.be.empty } describe('notifications tests', () => { let notification: Notification | null - let overlay: EntityManagerOverlay let em: EntityManager before(async () => { em = await globalEm - overlay = await createOverlay() await populateDbWithSeedData() }) - describe('👉 YPP Verify channel', () => { - let notificationId: string - it('verify channel should deposit notification', async () => { - const channelId = '1' - const nextNotificationIdPre = await getNextNotificationId(em, false) - notificationId = OFFCHAIN_NOTIFICATION_ID_TAG + '-' + nextNotificationIdPre - await verifyChannelService(em, [channelId]) - - notification = await em.getRepository(Notification).findOneBy({ - id: notificationId, - }) - const channel = await em.getRepository(Channel).findOneByOrFail({ id: channelId }) - const nextNotificationIdPost = await getNextNotificationId(em, false) - const account = await em - .getRepository(Account) - .findOneBy({ membershipId: channel!.ownerMemberId! }) - expect(notification).not.to.be.null - expect(channel).not.to.be.null - expect(notification!.notificationType.isTypeOf).to.equal('ChannelVerified') - expect(notification!.status.isTypeOf).to.equal('Unread') - expect(notification!.inApp).to.be.true - expect(notification!.recipient.isTypeOf).to.equal('ChannelRecipient') - expect((notification!.recipient as ChannelRecipient).channel).to.equal(channel.id) - 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('verify channel should mark channel as excluded with entity inserted', async () => { - const channelId = '2' - - await verifyChannelService(em, [channelId]) - - const channel = await em.getRepository(Channel).findOneByOrFail({ id: channelId }) - const channelVerification = await em - .getRepository(ChannelVerification) - .findOneBy({ channelId }) - expect(channelVerification).not.to.be.null - expect(channel!.yppStatus.isTypeOf).to.be.equal('YppVerified') - }) - }) - 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 excludeChannelService(em, channelId, rationale) - - notification = await em.getRepository(Notification).findOneBy({ - id: notificationId, - }) - const channel = await em.getRepository(Channel).findOneBy({ id: channelId }) - const nextNotificationIdPost = await getNextNotificationId(em, false) - const account = await em - .getRepository(Account) - .findOneBy({ membershipId: channel!.ownerMemberId! }) - expect(notification).not.to.be.null - expect(channel).not.to.be.null - expect(notification!.notificationType.isTypeOf).to.equal('ChannelExcluded') - expect(notification!.status.isTypeOf).to.equal('Unread') - expect((notification!.notificationType as ChannelExcluded).channelTitle).to.equal( - `test-channel-${channelId}` - ) - expect(notification!.inApp).to.be.true - expect(notification!.recipient.isTypeOf).to.equal('MemberRecipient') - expect((notification!.recipient as MemberRecipient).membership).to.equal( - channel?.ownerMemberId - ) - 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' - - await excludeChannelService(em, channelId, rationale) - - const channel = await em.getRepository(Channel).findOneBy({ id: channelId }) - const exclusion = await em.getRepository(Exclusion).findOneBy({ channelId }) - expect(exclusion).not.to.be.null - expect(exclusion!.rationale).to.equal(rationale) - expect(channel).not.to.be.null - expect(channel!.isExcluded).to.be.true - }) - }) - 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 excludeVideoService(em, videoId, rationale) - - notification = await em.getRepository(Notification).findOneBy({ - id: notificationId, - }) - const video = await em - .getRepository(Video) - .findOne({ where: { id: videoId }, relations: { channel: true } }) - expect(video).not.to.be.null - expect(video!.channel).not.to.be.null - const nextNotificationIdPost = await getNextNotificationId(em, false) - const account = await em - .getRepository(Account) - .findOneBy({ membershipId: video!.channel.ownerMemberId! }) - expect(notification).not.to.be.null - expect(notification!.notificationType.isTypeOf).to.equal('VideoExcluded') - expect(notification!.status.isTypeOf).to.equal('Unread') - expect(notification!.inApp).to.be.true - expect(notification!.recipient.isTypeOf).to.equal('ChannelRecipient') - expect((notification!.recipient as ChannelRecipient).channel).to.equal(video!.channel!.id) - 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' - - await excludeVideoService(em, videoId, rationale) - - const video = await em.getRepository(Video).findOneBy({ id: videoId }) - const exclusion = await em.getRepository(Exclusion).findOneBy({ videoId }) - expect(exclusion).not.to.be.null - expect(exclusion!.rationale).to.equal(rationale) - expect(video).not.to.be.null - expect(video!.isExcluded).to.be.true - }) - }) + // TODO: Set YPP status + // TODO: excludeContent 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]) - notification = await em.getRepository(Notification).findOneBy({ - id: notificationId, + notification = await findNotification(em, { + isTypeOf: 'NftFeaturedOnMarketPlace', + videoId: nftId, }) + notificationId = notification.id const nft = await em .getRepository(OwnedNft) .findOneOrFail({ where: { id: nftId }, relations: { video: { channel: true } } }) + assert(nft.video.channel.ownerMemberId, 'Missing channel owner id') const account = await em .getRepository(Account) - .findOneBy({ membershipId: nft!.video!.channel!.ownerMemberId! }) - const nextNotificationIdPost = await getNextNotificationId(em, false) - expect(notification).not.to.be.null - expect(notification!.notificationType.isTypeOf).to.equal('NftFeaturedOnMarketPlace') - expect((notification!.notificationType as NftFeaturedOnMarketPlace).videoId).to.equal( + .findOneByOrFail({ membershipId: nft.video.channel.ownerMemberId }) + expect(notification.notificationType.isTypeOf).to.equal('NftFeaturedOnMarketPlace') + expect((notification.notificationType as NftFeaturedOnMarketPlace).videoId).to.equal( nft.videoId ) - expect((notification!.notificationType as NftFeaturedOnMarketPlace).videoTitle).to.equal( - nft.video!.title || '??' - ) - expect(notification!.status.isTypeOf).to.equal('Unread') - expect(notification!.inApp).to.be.true - expect(notification!.recipient.isTypeOf).to.equal('ChannelRecipient') - expect((notification!.recipient as ChannelRecipient).channel).to.equal( - nft!.video!.channel!.id + expect((notification.notificationType as NftFeaturedOnMarketPlace).videoTitle).to.equal( + nft.video.title || '??' ) - expect(nextNotificationIdPost.toString()).to.equal((nextNotificationIdPre + 1).toString()) - expect(notification?.accountId).to.equal(account?.id) + expect(notification.status.isTypeOf).to.equal('Unread') + expect(notification.inApp).to.be.true + expect(notification.recipient.isTypeOf).to.equal('ChannelRecipient') + expect((notification.recipient as ChannelRecipient).channel).to.equal(nft.video.channel.id) + 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 - }) + it('notification email entity should be correctly deposited', () => + checkNotificationEmailDelivery(notificationId)) }) describe('👉 New bid made', () => { let nft: OwnedNft @@ -288,140 +134,119 @@ describe('notifications tests', () => { const outbiddedMember = '4' const videoId = '5' let notificationId: string - let nextNotificationIdPre: number before(async () => { const bidAmount = BigInt(100000) - nextNotificationIdPre = await getNextNotificationId(overlay, true) - notificationId = RUNTIME_NOTIFICATION_ID_TAG + '-' + nextNotificationIdPre nft = await em.getRepository(OwnedNft).findOneByOrFail({ videoId }) - - await auctionBidMadeInner( - overlay, - defaultTestBlock(), - 100, - undefined, - memberId, - videoId, - bidAmount - ) + await withOverlayTransaction(async (overlay) => { + await auctionBidMadeInner( + overlay, + defaultTestBlock(), + 100, + undefined, + memberId, + videoId, + bidAmount + ) + }) }) it('should deposit notification for member outbidded', async () => { - notification = (await overlay - .getRepository(Notification) - .getById(notificationId)) as Notification | null - const account = (await overlay + notification = await findNotification(em, { + isTypeOf: 'HigherBidPlaced', + videoId, + }) + notificationId = notification.id + const account = await em .getRepository(Account) - .getOneByRelationOrFail('membershipId', outbiddedMember)) as Account + .findOneByOrFail({ membershipId: outbiddedMember }) - expect(notification).not.to.be.null - expect(notification!.notificationType.isTypeOf).to.equal('HigherBidPlaced') - expect(notification!.status.isTypeOf).to.equal('Unread') - 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) - }) - 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 + expect(notification.notificationType.isTypeOf).to.equal('HigherBidPlaced') + expect(notification.status.isTypeOf).to.equal('Unread') + 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) }) + it('notification email entity should be correctly deposited', () => + checkNotificationEmailDelivery(notificationId)) 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 }) - notification = (await overlay - .getRepository(Notification) - .getByIdOrFail(notificationId)) as Notification - const account = await overlay + .findOneByOrFail({ id: (nft.owner as NftOwnerChannel).channel }) + notification = await findNotification(em, { + isTypeOf: 'CreatorReceivesAuctionBid', + videoId, + }) + notificationId = notification.id + assert(channel.ownerMemberId, 'Missing channel ownerMemberId!') + const account = await em .getRepository(Account) - .getOneByRelationOrFail('membershipId', channel!.ownerMemberId!) + .findOneByOrFail({ membershipId: channel.ownerMemberId }) // complete the missing checks as above - expect(notification).not.to.be.null - expect(notification!.notificationType.isTypeOf).to.equal('CreatorReceivesAuctionBid') - expect(notification!.status.isTypeOf).to.equal('Unread') - expect(notification!.inApp).to.be.true - expect(notification!.recipient.isTypeOf).to.equal('ChannelRecipient') - expect(channel).not.to.be.null - expect((notification!.recipient as ChannelRecipient).channel).to.equal(channel!.id) - expect(notification?.accountId).to.equal(account?.id) - }) - it('notification email entity should be correctly deposited on overlay', 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 + expect(notification.notificationType.isTypeOf).to.equal('CreatorReceivesAuctionBid') + expect(notification.status.isTypeOf).to.equal('Unread') + expect(notification.inApp).to.be.true + expect(notification.recipient.isTypeOf).to.equal('ChannelRecipient') + expect((notification.recipient as ChannelRecipient).channel).to.equal(channel.id) + expect(notification.accountId).to.equal(account.id) }) + it('notification email entity should be correctly deposited', () => + checkNotificationEmailDelivery(notificationId)) }) describe('👉 Video Liked', () => { let notificationId: string - let nextNotificationIdPre: number - const block = { timestamp: 123456 } as any + const videoId = '1' + const block = { height: 123, timestamp: 123456 } as SubstrateBlock const indexInBlock = 1 const extrinsicHash = '0x1234567890abcdef' const metadataMessage: IMemberRemarked = { reactVideo: { - videoId: Long.fromNumber(1), + videoId: Long.fromString(videoId), reaction: ReactVideo.Reaction.LIKE, }, } const event = { isV2001: true, asV2001: ['3', metadataToBytes(MemberRemarked, metadataMessage), undefined], - } as any - before(async () => { - nextNotificationIdPre = await getNextNotificationId(overlay, true) - notificationId = RUNTIME_NOTIFICATION_ID_TAG + '-' + nextNotificationIdPre.toString() - }) + } as unknown as MembersMemberRemarkedEvent it('should process video liked and deposit notification', async () => { - await processMemberRemarkedEvent({ - overlay, - block, - indexInBlock, - extrinsicHash, - event, + await withOverlayTransaction(async (overlay) => { + await processMemberRemarkedEvent({ + overlay, + block, + indexInBlock, + extrinsicHash, + event, + }) }) - const nextNotificationId = await getNextNotificationId(overlay, true) - notification = (await overlay - .getRepository(Notification) - .getByIdOrFail(notificationId)) as Notification + notification = await findNotification(em, { + isTypeOf: 'VideoLiked', + videoId, + }) + notificationId = notification.id 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((nextNotificationIdPre + 1).toString()) - expect(notification!.recipient.isTypeOf).to.equal('ChannelRecipient') - }) - it('notification email entity should be correctly deposited on overlay', 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 + expect(notification.status.isTypeOf).to.equal('Unread') + expect(notification.inApp).to.be.true + expect(notification.recipient.isTypeOf).to.equal('ChannelRecipient') }) + it('notification email entity should be correctly deposited', () => + checkNotificationEmailDelivery(notificationId)) }) describe('👉 Comment Posted To Video', () => { - let nextNotificationIdPre: number let notificationId: string - const block = { timestamp: 123456 } as any + const videoId = '1' + const block = { height: 123, timestamp: 123456 } as SubstrateBlock const indexInBlock = 1 const extrinsicHash = '0x1234567890abcdef' const commentId = backwardCompatibleMetaID(block, indexInBlock) const metadataMessage: IMemberRemarked = { createComment: { - videoId: Long.fromNumber(1), + videoId: Long.fromString(videoId), parentCommentId: null, body: 'test', }, @@ -429,122 +254,89 @@ describe('notifications tests', () => { const event = { isV2001: true, asV2001: ['2', metadataToBytes(MemberRemarked, metadataMessage), undefined], // avoid comment author == creator - } as any - before(async () => { - nextNotificationIdPre = await getNextNotificationId(overlay, true) - notificationId = RUNTIME_NOTIFICATION_ID_TAG + '-' + nextNotificationIdPre.toString() - }) + } as unknown as MembersMemberRemarkedEvent it('should process comment to video and deposit notification', async () => { - await processMemberRemarkedEvent({ - overlay, - block, - indexInBlock, - extrinsicHash, - event, - }) - - const nextNotificationId = await getNextNotificationId(overlay, true) - notification = (await overlay - .getRepository(Notification) - .getByIdOrFail(notificationId)) as Notification | null - - it('notification type is comment posted to video', () => { - expect(notification).not.to.be.null - expect(notification!.notificationType.isTypeOf).to.equal('CommentPostedToVideo') + await withOverlayTransaction(async (overlay) => { + await processMemberRemarkedEvent({ + overlay, + block, + indexInBlock, + extrinsicHash, + event, + }) }) - it('notification data for comment posted to video should be ok', () => { - const notificationData = notification!.notificationType as CommentPostedToVideo - expect(notificationData.videoId).to.equal('1') - expect(notificationData.comentId).to.equal(commentId) - expect(notificationData.memberHandle).to.equal('handle-2') - expect(notificationData.videoTitle).to.equal('test-video-1') + notification = await findNotification(em, { + isTypeOf: 'CommentPostedToVideo', + videoId, }) + notificationId = notification.id - 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((nextNotificationIdPre + 1).toString()) - expect(notification!.recipient.isTypeOf).to.equal('ChannelRecipient') - }) - it('notification email entity should be correctly deposited on overlay', 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 - }) + expect(notification.notificationType.isTypeOf).to.equal('CommentPostedToVideo') + const notificationData = notification.notificationType as CommentPostedToVideo + expect(notificationData.videoId).to.equal('1') + expect(notificationData.comentId).to.equal(commentId) + expect(notificationData.memberHandle).to.equal('handle-2') + expect(notificationData.videoTitle).to.equal('test-video-1') + expect(notification.status.isTypeOf).to.equal('Unread') + expect(notification.inApp).to.be.true + expect(notification.recipient.isTypeOf).to.equal('ChannelRecipient') }) + + it('notification email entity should be correctly deposited', () => + checkNotificationEmailDelivery(notificationId)) describe('👉 Reply To Comment', () => { - let nextNotificationIdPre: number let notificationId: string - const block = { timestamp: 123457 } as any + const videoId = '1' + const block = { height: 123, timestamp: 123457 } as SubstrateBlock const indexInBlock = 1 const metadataMessage = { createComment: { - videoId: Long.fromNumber(1), + videoId: Long.fromString(videoId), parentCommentId: commentId, body: 'reply test', }, } const event = { isV2001: true, - asV2001: ['3', metadataToBytes(MemberRemarked, metadataMessage!), undefined], - } as any + asV2001: ['3', metadataToBytes(MemberRemarked, metadataMessage), undefined], + } as unknown as MembersMemberRemarkedEvent before(async () => { - nextNotificationIdPre = await getNextNotificationId(overlay, true) - notificationId = RUNTIME_NOTIFICATION_ID_TAG + '-' + nextNotificationIdPre.toString() - - await processMemberRemarkedEvent({ - overlay, - block, - indexInBlock, - extrinsicHash, - event, + await withOverlayTransaction(async (overlay) => { + await processMemberRemarkedEvent({ + overlay, + block, + indexInBlock, + extrinsicHash, + event, + }) }) }) - describe('should process reply to comment and deposit notification', () => { - let nextNotificationId: number - before(async () => { - nextNotificationId = await getNextNotificationId(overlay, true) - notification = (await overlay - .getRepository(Notification) - .getByIdOrFail(notificationId)) as Notification | null + it('should process reply to comment and deposit notification', async () => { + notification = await findNotification(em, { + isTypeOf: 'CommentReply', + videoId, }) + notificationId = notification.id - it('notification type is reply to comment', () => { - expect(notification).not.to.be.null - expect(notification!.notificationType.isTypeOf).to.equal('CommentReply') - expect(notification?.accountId).to.equal('2') - }) - it('notification data for comment reply should be ok', () => { - const notificationData = notification!.notificationType as CommentReply - expect(notificationData.videoId).to.equal('1') - expect(notificationData.memberHandle).to.equal('handle-3') - expect(notificationData.commentId).to.equal(backwardCompatibleMetaID(block, indexInBlock)) - expect(notificationData.videoTitle).to.equal('test-video-1') - expect(notification!.recipient.isTypeOf).to.equal('MemberRecipient') - expect((notification!.recipient as MemberRecipient).membership).to.equal( - '2', - 'member recipient should be parent comment author' - ) - }) - 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((nextNotificationIdPre + 1).toString()) - }) - it('notification email entity should be correctly deposited on overlay', 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 - }) + expect(notification.notificationType.isTypeOf).to.equal('CommentReply') + expect(notification.accountId).to.equal('2') + const notificationData = notification.notificationType as CommentReply + expect(notificationData.videoId).to.equal('1') + expect(notificationData.memberHandle).to.equal('handle-3') + expect(notificationData.commentId).to.equal(backwardCompatibleMetaID(block, indexInBlock)) + expect(notificationData.videoTitle).to.equal('test-video-1') + expect(notification.recipient.isTypeOf).to.equal('MemberRecipient') + expect((notification.recipient as MemberRecipient).membership).to.equal( + '2', + 'member recipient should be parent comment author' + ) + expect(notification.status.isTypeOf).to.equal('Unread') + expect(notification.inApp).to.be.true }) + it('notification email entity should be correctly deposited', () => + checkNotificationEmailDelivery(notificationId)) }) }) }) diff --git a/src/tests/integration/testUtils.ts b/src/tests/integration/testUtils.ts index 799476ece..273ce9584 100644 --- a/src/tests/integration/testUtils.ts +++ b/src/tests/integration/testUtils.ts @@ -66,7 +66,8 @@ export async function populateDbWithSeedData() { cumulativeRewardClaimed: 0n, cumulativeReward: 0n, cumulativeRevenue: BigInt(0), - yppStatus: new YppUnverified(), + yppStatus: new YppUnverified({ timestamp: new Date() }), + isYtSyncEnabled: true, }) const video = new Video({ id: i.toString(), diff --git a/src/tests/migrations/.env b/src/tests/migrations/.env index a8f3c9080..467eaf214 100644 --- a/src/tests/migrations/.env +++ b/src/tests/migrations/.env @@ -16,6 +16,9 @@ PROCESSOR_PROMETHEUS_PORT=3337 GQL_PORT=4350 # Auth api port AUTH_API_PORT=4074 +# RabbitMQ +RABBITMQ_PORT=5672 +RABBITMQ_URL=amqp://orion_rabbitmq # Archive gateway url ARCHIVE_GATEWAY_URL=${CUSTOM_ARCHIVE_GATEWAY_URL:-http://localhost:8888/graphql} @@ -31,16 +34,10 @@ KILL_SWITCH_ON=false VIDEO_VIEW_PER_USER_TIME_LIMIT=10 # Operator API secret OPERATOR_SECRET=this-is-not-so-secret-change-it -# every 50 views video relevance score will be recalculated -VIDEO_RELEVANCE_VIEWS_TICK=50 -# [ -# newness (negative number of days since created) weight, -# views weight, -# comments weight, -# rections weights, -# [joystream creation weight, YT creation weight] -# ] -RELEVANCE_WEIGHTS="[1, 0.03, 0.3, 0.5, [7,3]]" +# every 10 views video relevance score will be recalculated +VIDEO_RELEVANCE_VIEWS_TICK=10 +# every time a channel is followed / unfollowed, its weight will be recalculated +CHANNEL_WEIGHT_FOLLOWS_TICK=1 MAX_CACHED_ENTITIES=1000 APP_PRIVATE_KEY=this-is-not-so-secret-change-it SESSION_EXPIRY_AFTER_INACTIVITY_MINUTES=60 diff --git a/src/tests/migrations/migration.test.ts b/src/tests/migrations/migration.test.ts deleted file mode 100644 index 4f11e9610..000000000 --- a/src/tests/migrations/migration.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { EntityManager } from 'typeorm' -import { globalEm } from '../../utils/globalEm' -import { Account, Channel, NextEntityId, NotificationPreference } from '../../model' -import { expect } from 'chai' - -const arePrefsAllTrue = (account: Account) => - Object.values(account.notificationPreferences).every((v: NotificationPreference) => { - return v.inAppEnabled && v.emailEnabled - }) -const queryAccount = async (membershipId: string, em: EntityManager) => { - return await em.getRepository(Account).findOneOrFail({ - where: { membershipId }, - relations: { notifications: true }, - }) -} - -describe('Migration from 3.0.2 to 3.2.0', () => { - let em: EntityManager - const aliceMembershipId = '16' - const bobMembershipId = '17' - before(async () => { - em = await globalEm - }) - describe('Accounts are migrated propelty', () => { - let aliceAccount: Account - let bobAccount: Account - before(async () => { - aliceAccount = await queryAccount(aliceMembershipId, em) - bobAccount = await queryAccount(bobMembershipId, em) - }) - it('should keep the accounts and add the preference Field all true', () => { - expect(arePrefsAllTrue(aliceAccount)).to.be.true - expect(arePrefsAllTrue(bobAccount)).to.be.true - }) - it('notification accounts should be empty', () => { - expect(aliceAccount.notifications).to.have.lengthOf(0) - expect(bobAccount.notifications).to.have.lengthOf(0) - }) - it('referrer channel id should be null', () => { - expect(aliceAccount.referrerChannelId).to.be.null - expect(bobAccount.referrerChannelId).to.be.null - }) - }) - describe('Channels are migrated properly', () => { - const aliceChannelId = '1' - let aliceChannel: Channel - before(async () => { - aliceChannel = await em.getRepository(Channel).findOneByOrFail({ id: aliceChannelId }) - }) - it('channel should have ypp status marked as unverified', () => { - expect(aliceChannel.yppStatus.isTypeOf).to.equal('YppUnverified') - }) - }) - describe('Next Entity Ids migrated', () => { - let row: NextEntityId - describe('Account case', () => { - before(async () => { - row = await em.getRepository(NextEntityId).findOneByOrFail({ entityName: 'Account' }) - }) - it('should have the next id set to 3', () => { - expect(row.nextId.toString()).to.equal('3') - }) - }) - }) -}) diff --git a/src/utils/OrionVideoLanguageManager.ts b/src/utils/OrionVideoLanguageManager.ts index 5e059fb27..feb090296 100644 --- a/src/utils/OrionVideoLanguageManager.ts +++ b/src/utils/OrionVideoLanguageManager.ts @@ -37,7 +37,7 @@ export class OrionVideoLanguageManager { const videos = await em.query(` SELECT id, title, description - FROM admin.video + FROM curator.video WHERE id in (${[...this.videoToDetect.values()].map((id) => `'${id}'`).join(',')}) `) diff --git a/src/utils/VideoRelevanceManager.ts b/src/utils/VideoRelevanceManager.ts deleted file mode 100644 index e1f5f3eb4..000000000 --- a/src/utils/VideoRelevanceManager.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { EntityManager } from 'typeorm' -import { config, ConfigVariable } from './config' -import { globalEm } from './globalEm' - -export const SECONDS_PER_DAY = 60 * 60 * 24 - -type VideoRelevanceManagerLoops = { - fullUpdateLoopTime: number - scheduledUpdateLoopTime: number -} - -export class VideoRelevanceManager { - private channelsToUpdate: Set = new Set() - private _isVideoRelevanceEnabled = false - - public get isVideoRelevanceEnabled(): boolean { - return this._isVideoRelevanceEnabled - } - - async init({ - fullUpdateLoopTime, - scheduledUpdateLoopTime, - }: VideoRelevanceManagerLoops): Promise { - const em = await globalEm - - this.updateLoop(em, scheduledUpdateLoopTime) - .then(() => { - /* Do nothing */ - }) - .catch((err) => { - console.error(err) - process.exit(-1) - }) - - this.updateLoop(em, fullUpdateLoopTime) - .then(() => { - /* Do nothing */ - }) - .catch((err) => { - console.error(err) - process.exit(-1) - }) - } - - turnOnVideoRelevanceManager() { - this._isVideoRelevanceEnabled = true - } - - scheduleRecalcForChannel(id: string | null | undefined) { - if (id) { - this.channelsToUpdate.add(id) - } - } - - async updateVideoRelevanceValue(em: EntityManager, forceUpdateAll?: boolean) { - if (!this._isVideoRelevanceEnabled || !(this.channelsToUpdate.size || forceUpdateAll)) { - return - } - - const [ - newnessWeight, - viewsWeight, - commentsWeight, - reactionsWeight, - [joystreamTimestampWeight, ytTimestampWeight] = [7, 3], - defaultChannelWeight, - ] = await config.get(ConfigVariable.RelevanceWeights, em) - const channelWeight = defaultChannelWeight ?? 1 - const weightedEpoch = ` - ( - ( - extract(epoch from video.created_at) * ${joystreamTimestampWeight} + - CASE - WHEN ( - video.published_before_joystream IS NOT NULL - AND video.published_before_joystream < now() - ) THEN extract(epoch from video.published_before_joystream) - ELSE extract(epoch from video.created_at) - END * ${ytTimestampWeight} - ) / ${joystreamTimestampWeight + ytTimestampWeight} - )` - const weightedMeanVideoAgeDays = ` - ( - (extract(epoch FROM now()) - ${weightedEpoch}) - / ${SECONDS_PER_DAY} - )` - - await em.query(` - WITH videos_with_weight AS ( - SELECT - video.id as videoId, - channel.id as channelId, - ROUND( - ( - 365 * ${newnessWeight} - - LEAST(${weightedMeanVideoAgeDays}, 365) * ${newnessWeight} - + (views_num * ${viewsWeight}) - + (comments_count * ${commentsWeight}) - + (reactions_count * ${reactionsWeight}) - ) * COALESCE(channel.channel_weight, ${channelWeight}), - 2 - ) as videoRelevance - FROM - video - INNER JOIN channel ON video.channel_id = channel.id - ${ - forceUpdateAll - ? '' - : `WHERE video.channel_id in (${[...this.channelsToUpdate.values()] - .map((id) => `'${id}'`) - .join(', ')})` - } - ORDER BY - video.id - ), - - top_channel_score as ( - SELECT - channel.id as channelId, - MAX(videos_with_weight.videoRelevance) as maxChannelRelevance - FROM - channel - INNER JOIN videos_with_weight on videos_with_weight.channelId = channel.id - GROUP BY - channel.id - ), - - ranked_videos AS ( - SELECT - videos_with_weight.videoId, - topChannelVideo.maxChannelRelevance, - ROW_NUMBER() OVER ( - PARTITION BY videos_with_weight.channelId - ORDER BY - videos_with_weight.videoRelevance DESC, - videos_with_weight.videoId - ) as rank - FROM - videos_with_weight - LEFT JOIN top_channel_score as topChannelVideo ON videos_with_weight.channelId = topChannelVideo.channelId - ) - - UPDATE - video - SET - video_relevance = CASE - WHEN ranked_videos.rank = 1 THEN ranked_videos.maxChannelRelevance - ELSE 0 - END - FROM - ranked_videos - WHERE - video.id = ranked_videos.videoId; - `) - this.channelsToUpdate.clear() - } - - private async updateLoop(em: EntityManager, intervalMs: number): Promise { - while (true) { - await this.updateVideoRelevanceValue(em) - await new Promise((resolve) => setTimeout(resolve, intervalMs)) - } - } -} diff --git a/src/utils/config.ts b/src/utils/config.ts index 453189dc8..db7806027 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -10,6 +10,7 @@ export enum ConfigVariable { VideoViewPerUserTimeLimit = 'VIDEO_VIEW_PER_USER_TIME_LIMIT', VideoRelevanceViewsTick = 'VIDEO_RELEVANCE_VIEWS_TICK', RelevanceWeights = 'RELEVANCE_WEIGHTS', + RelevanceServiceConfig = 'RELEVANCE_SERVICE_CONFIG', AppPrivateKey = 'APP_PRIVATE_KEY', AppRootDomain = 'APP_ROOT_DOMAIN', SessionExpiryAfterInactivityMinutes = 'SESSION_EXPIRY_AFTER_INACTIVITY_MINUTES', @@ -26,6 +27,7 @@ export enum ConfigVariable { AppNameAlt = 'APP_NAME_ALT', NotificationAssetRoot = 'NOTIFICATION_ASSET_ROOT', CommentTipTiers = 'COMMENT_TIP_TIERS', + ChannelWeightFollowsTick = 'CHANNEL_WEIGHT_FOLLOWS_TICK', } const boolType = { @@ -50,6 +52,36 @@ const jsonType = () => ({ export type CommentTipTiers = { [key in CommentTipTier]: number } +export type RelevanceWeights = { + channel: { + crtVolumeWeight: number + crtLiquidityWeight: number + followersWeight: number + revenueWeight: number + yppTierWeight: number + } + video: { + ageWeight: number + viewsWeight: number + commentsWeight: number + reactionsWeight: number + ageSubWeights: { + joystreamAgeWeight: number + youtubeAgeWeight: number + } + // TODO: Tips + } +} + +export type RelevanceServiceConfig = { + populateBackgroundQueueInterval: number + updateLoopInterval: number + channelsPerIteration: number + videosPerChannelLimit: number + videosPerChannelSelectTop: number + ageScoreHalvingDays: number +} + export const configVariables = { [ConfigVariable.SupportNoCategoryVideo]: boolType, [ConfigVariable.SupportNewCategories]: boolType, @@ -57,8 +89,9 @@ export const configVariables = { [ConfigVariable.KillSwitch]: boolType, [ConfigVariable.VideoViewPerUserTimeLimit]: intType, [ConfigVariable.VideoRelevanceViewsTick]: intType, - [ConfigVariable.RelevanceWeights]: - jsonType<[number, number, number, number, [number, number], number]>(), + [ConfigVariable.ChannelWeightFollowsTick]: intType, + [ConfigVariable.RelevanceWeights]: jsonType(), + [ConfigVariable.RelevanceServiceConfig]: jsonType(), [ConfigVariable.AppPrivateKey]: stringType, [ConfigVariable.SessionMaxDurationHours]: intType, [ConfigVariable.SessionExpiryAfterInactivityMinutes]: intType, @@ -79,17 +112,65 @@ export const configVariables = { type TypeOf = ReturnType<(typeof configVariables)[C]['deserialize']> +export const configDefaults: { [C in ConfigVariable]?: TypeOf } = { + [ConfigVariable.RelevanceWeights]: { + channel: { + crtVolumeWeight: 0.2, + crtLiquidityWeight: 0.2, + followersWeight: 0.2, + revenueWeight: 0.2, + yppTierWeight: 0.2, + }, + video: { + ageWeight: 0.6, + ageSubWeights: { + joystreamAgeWeight: 0.1, + youtubeAgeWeight: 0.9, + }, + viewsWeight: 0.1, + commentsWeight: 0.15, + reactionsWeight: 0.15, + }, + }, + [ConfigVariable.RelevanceServiceConfig]: { + populateBackgroundQueueInterval: 12 * 60 * 60 * 1_000, // 12 hours + updateLoopInterval: 100, // 100 ms + channelsPerIteration: 100, + videosPerChannelLimit: 10, + videosPerChannelSelectTop: 1, + ageScoreHalvingDays: 30, + }, +} + class Config { + getDefault(name: C) { + // Use env value if exists + const serializedValue = process.env[name] + if (serializedValue === undefined) { + // Otherwise fallback to hardcoded value + if (configDefaults[name] !== undefined) { + return configDefaults[name] as TypeOf + } + throw new Error(`${name} has no default value`) + } + return configVariables[name].deserialize(serializedValue) as TypeOf + } + async get(name: C, em: EntityManager): Promise> { - const serialized = await withHiddenEntities(em, async () => { - return (await em.findOneBy(GatewayConfig, { id: name }))?.value ?? process.env[name] + const dbValue = await withHiddenEntities(em, async () => { + return (await em.findOneBy(GatewayConfig, { id: name }))?.value }) - if (serialized === undefined) { - throw new Error(`Cannot determine value of config variable ${name}`) + // Fallback to default value if not found in DB + if (dbValue === undefined) { + try { + return this.getDefault(name) + } catch (e) { + throw new Error(`Missing value of config variable: ${name}`) + } } - return configVariables[name].deserialize(serialized) as TypeOf + return configVariables[name].deserialize(dbValue) as TypeOf } async set(name: C, value: TypeOf, em: EntityManager): Promise { diff --git a/src/utils/customMigrations/setOrionLanguageProvider.ts b/src/utils/customMigrations/setOrionLanguageProvider.ts index e80f2b0e7..be996ec63 100644 --- a/src/utils/customMigrations/setOrionLanguageProvider.ts +++ b/src/utils/customMigrations/setOrionLanguageProvider.ts @@ -24,7 +24,7 @@ export async function updateVideoLanguages(em: EntityManager, videos: VideoUpdat })) const query = ` - UPDATE admin.video AS v SET + UPDATE curator.video AS v SET orion_language = c.orion_language FROM (VALUES ${videosWithDetections .map((_, idx) => `($${idx * 2 + 1}, $${idx * 2 + 2})`) @@ -48,7 +48,7 @@ export async function detectVideoLanguageWithProvider() { const videos: VideoUpdateType[] = await em.query(` SELECT id, title, description - FROM admin.video + FROM curator.video ORDER BY id::INTEGER ASC OFFSET ${cursor} LIMIT ${batchSize} diff --git a/src/utils/nextEntityId.ts b/src/utils/nextEntityId.ts deleted file mode 100644 index ff8738068..000000000 --- a/src/utils/nextEntityId.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { EntityManager } from 'typeorm' -import { NextEntityId } from '../model' -import { AnyEntity, Constructor, EntityManagerOverlay } from './overlay' - -// used to retrieve the next id for an entity from NextEntityId table using either EntityManager or Overlay -export async function getNextIdForEntity( - store: EntityManager | EntityManagerOverlay, - entityName: string -): Promise { - // Get next entity id from overlay (this will mostly be used in the mappings context) - if (store instanceof EntityManagerOverlay) { - const row = await store - .getRepository(NextEntityId as Constructor) - .getOneBy({ entityName: entityName }) - - const id = parseInt(row?.nextId.toString() || '1') - - // Update the id to be the next one in the overlay - if (row) { - row.nextId++ - } else { - store - .getRepository(NextEntityId as Constructor) - .new({ entityName, nextId: id + 1 }) - } - - return id - } - - // Get next entity id from EntityManager (this will mostly be used in the graphql-server/auth-api context) - let row: NextEntityId | null - if (process.env.TESTING === 'true' || process.env.TESTING === '1') { - row = await store.getRepository(NextEntityId).findOne({ - where: { entityName }, - }) - } else { - row = await store.getRepository(NextEntityId).findOne({ - where: { entityName }, - lock: { mode: 'pessimistic_write' }, - }) - } - const id = parseInt(row?.nextId.toString() || '1') - return id -} diff --git a/src/utils/notification/helpers.ts b/src/utils/notification/helpers.ts index b14c57f96..ec938009c 100644 --- a/src/utils/notification/helpers.ts +++ b/src/utils/notification/helpers.ts @@ -4,7 +4,6 @@ import { AccountNotificationPreferences, Channel, Event, - NextEntityId, Notification, NotificationEmailDelivery, NotificationPreference, @@ -15,7 +14,6 @@ import { } from '../../model' import { getCurrentBlockHeight } from '../blockHeight' import { uniqueId } from '../crypto' -import { getNextIdForEntity } from '../nextEntityId' import { EntityManagerOverlay, Flat } from '../overlay' export const RUNTIME_NOTIFICATION_ID_TAG = 'RuntimeNotification' @@ -157,11 +155,8 @@ async function addOffChainNotification( notificationType: NotificationType, dispatchBlock: number ) { - // get notification Id from orion_db in any case - const nextNotificationId = await getNextIdForEntity(em, OFFCHAIN_NOTIFICATION_ID_TAG) - const notification = createNotification( - `${OFFCHAIN_NOTIFICATION_ID_TAG}-${nextNotificationId}`, + `${OFFCHAIN_NOTIFICATION_ID_TAG}-${uniqueId()}`, account.id, recipient, notificationType, @@ -176,8 +171,6 @@ async function addOffChainNotification( if (pref.emailEnabled) { await createEmailNotification(em, notification) } - - await saveNextNotificationId(em, nextNotificationId + 1, OFFCHAIN_NOTIFICATION_ID_TAG) } async function addRuntimeNotification( @@ -188,10 +181,7 @@ async function addRuntimeNotification( event: Event, dispatchBlock: number ) { - // get notification Id from orion_db in any case - const nextNotificationId = await getNextIdForEntity(overlay, RUNTIME_NOTIFICATION_ID_TAG) - - const runtimeNotificationId = `${RUNTIME_NOTIFICATION_ID_TAG}-${nextNotificationId}` + const runtimeNotificationId = `${RUNTIME_NOTIFICATION_ID_TAG}-${uniqueId()}` // check that on-notification is not already present in orion_db in case the processor has been restarted (but not orion_db) const existingNotification = await overlay @@ -300,18 +290,6 @@ export const addNotification = async ( } } -async function saveNextNotificationId( - em: EntityManager, - nextNotificationId: number, - entityName: string -) { - const nextEntityId = new NextEntityId({ - entityName, - nextId: nextNotificationId, - }) - await em.save(nextEntityId) -} - const JOY_DECIMAL = 10 export const formatJOY = (hapiAmount: bigint | number): string => { const [intPart, decPart] = splitInt(String(hapiAmount), JOY_DECIMAL) diff --git a/src/utils/offchainState.ts b/src/utils/offchainState.ts index bc89e9709..c970546d4 100644 --- a/src/utils/offchainState.ts +++ b/src/utils/offchainState.ts @@ -4,7 +4,7 @@ import { createParseStream, createStringifyStream } from 'big-json' import fs from 'fs' import { snakeCase } from 'lodash' import path from 'path' -import { EntityManager, ValueTransformer } from 'typeorm' +import { EntityManager, EntityMetadata, ValueTransformer } from 'typeorm' import * as model from '../model' import { AccountNotificationPreferences, @@ -19,16 +19,6 @@ import { EntityManagerOverlay } from './overlay' const DEFAULT_EXPORT_PATH = path.resolve(__dirname, '../../db/export/export.json') -type CamelToSnakeCase = S extends `${infer T}${infer U}` - ? T extends Capitalize - ? `_${Lowercase}${CamelToSnakeCase}` - : `${T}${CamelToSnakeCase}` - : S - -type SnakeCaseKeys = { - [K in keyof T as CamelToSnakeCase]: T[K] -} - type ClassConstructors = { [K in keyof T]: T[K] extends new (...args: any[]) => any ? T[K] : never } @@ -43,9 +33,6 @@ const exportedStateMap: ExportedStateMap = { VideoViewEvent: true, ChannelFollow: true, Report: true, - Exclusion: true, - ChannelVerification: true, - ChannelSuspension: true, GatewayConfig: true, NftFeaturingRequest: true, VideoHero: true, @@ -59,10 +46,9 @@ const exportedStateMap: ExportedStateMap = { NotificationEmailDelivery: true, EmailDeliveryAttempt: true, Token: true, - NextEntityId: true, OrionOffchainCursor: true, UserInteractionCount: true, - Channel: ['isExcluded', 'videoViewsNum', 'followsNum', 'yppStatus', 'channelWeight'], + Channel: ['isExcluded', 'videoViewsNum', 'followsNum', 'yppStatus', 'isYtSyncEnabled'], Video: ['isExcluded', 'viewsNum', 'orionLanguage', 'includeInHomeFeed'], Comment: ['isExcluded'], OwnedNft: ['isFeatured'], @@ -161,11 +147,34 @@ function migrateExportDataToV400(data: ExportedData): ExportedData { return data } +function migrateExportDataToV500( + data: ExportedData & { + ChannelVerification?: unknown + ChannelSuspension?: unknown + Exclusion?: unknown + NextEntityId?: unknown + } +): ExportedData { + // Skip entities that don't exist / should not be exported to 5.0 + delete data.ChannelSuspension + delete data.Exclusion + delete data.ChannelVerification + delete data.NextEntityId + // Skip Channel's yppStatus and channelWeight + data.Channel?.values.forEach((v) => { + delete v.yppStatus + delete v.channelWeight + }) + + return data +} + export class OffchainState { private logger = createLogger('offchainState') private _isImported = false private migrations: Migrations = { + '5.0.0': migrateExportDataToV500, '4.0.0': migrateExportDataToV400, '3.2.0': migrateExportDataToV320, '3.0.0': migrateExportDataToV300, @@ -243,7 +252,14 @@ export class OffchainState { // construct proper JSONB objects from raw data return Object.fromEntries( Object.entries(data).map(([entityName, { type, values }]) => { - const metadata = em.connection.getMetadata(entityName) + let metadata: EntityMetadata + try { + metadata = em.connection.getMetadata(entityName) + } catch { + // If no metadata avilable - skip transformation + this.logger.warn(`Metadata not available for entity ${entityName}!`) + return [entityName, { type, values }] + } const jsonbColumns = metadata.columns.filter((c) => c.type === 'jsonb') values = (values as any[]).map((value) => { @@ -252,8 +268,12 @@ export class OffchainState { const transformer = column.transformer as ValueTransformer | undefined if (value[propertyName] && transformer) { const rawValue = value[propertyName] - const transformedValue = transformer.from(rawValue) - value[propertyName] = transformedValue + try { + const transformedValue = transformer.from(rawValue) + value[propertyName] = transformedValue + } catch (e) { + this.logger.warn(`Couldn't transform ${entityName}.${propertyName} value!`) + } } }) return value @@ -280,26 +300,6 @@ export class OffchainState { return data } - private async importNextEntityIdCounters( - overlay: EntityManagerOverlay, - entityName: string, - data: model.NextEntityId[] - ) { - const em = overlay.getEm() - assert(entityName === 'NextEntityId') - for (const record of data) { - if (em.connection.hasMetadata(record.entityName as string)) { - // reason: during migration the overlay would write to the database the - // old nextId, to avoid that directly set the 'nextId' in the Overlay - overlay - .getRepository(model[record.entityName as keyof typeof model] as any) - .setNextEntityId(record.nextId as number) - } else { - await em.getRepository(entityName).upsert(record, ['entityName']) - } - } - } - public async import( overlay: EntityManagerOverlay, exportFilePath = DEFAULT_EXPORT_PATH @@ -375,16 +375,7 @@ export class OffchainState { } entities left)...` ) - // UPSERT operation specifically for NextEntityId - if (entityName === 'NextEntityId') { - await this.importNextEntityIdCounters( - overlay, - entityName, - batch as model.NextEntityId[] - ) - } else { - await em.getRepository(entityName).insert(batch) - } + await em.getRepository(entityName).insert(batch) } } this.logger.info( diff --git a/src/utils/overlay.ts b/src/utils/overlay.ts index 6b57cea1d..a511afd03 100644 --- a/src/utils/overlay.ts +++ b/src/utils/overlay.ts @@ -304,17 +304,33 @@ export class RepositoryOverlay { .map(([id]) => id) } + // Acquire all necessary locks before comitting the database updates + async acquireLocks(): Promise { + const ids = this.getAllToBeSaved() + .map((e) => e.id) + .concat(this.getAllIdsToBeRemoved()) + if (ids.length) { + // Lock the entities FOR UPDATE (pessimistic_write) + await this.repository + .createQueryBuilder('e') + .select('e.id') + .where(`e.id IN (:...ids)`, { ids }) + .setLock('pessimistic_write') + .orderBy(`e.id`, 'ASC') + .execute() + } + } + // Execute all scheduled entity inserts/updates async executeScheduledUpdates(): Promise { const logger = Logger.get() const toBeSaved = this.getAllToBeSaved() if (toBeSaved.length) { - if (process.env.TESTING !== 'true' && process.env.TESTING !== '1') { - logger.info(`Saving ${toBeSaved.length} ${this.entityName} entities...`) - logger.debug( - `Ids of ${this.entityName} entities to save: ${toBeSaved.map((e) => e.id).join(', ')}` - ) - } + // Execute the updates + logger.info(`Saving ${toBeSaved.length} ${this.entityName} entities...`) + logger.debug( + `Ids of ${this.entityName} entities to save: ${toBeSaved.map((e) => e.id).join(', ')}` + ) await this.repository.save(toBeSaved) } } @@ -324,6 +340,7 @@ export class RepositoryOverlay { const logger = Logger.get() const toBeRemoved = this.getAllIdsToBeRemoved().map((id) => new this.EntityClass({ id })) if (toBeRemoved.length) { + // Execute the removals logger.info(`Removing ${toBeRemoved.length} ${this.entityName} entities...`) logger.debug( `Ids of ${this.entityName} entities to remove: ${toBeRemoved.map((e) => e.id).join(', ')}` @@ -351,8 +368,8 @@ export class EntityManagerOverlay { public static async create(store: Store, afterDbUpdate: (em: EntityManager) => Promise) { // FIXME: This is a little hacky, but we really need to access the underlying EntityManager const em = await (store as unknown as { em: () => Promise }).em() - // Add "admin" schema to search path in order to be able to access "hidden" entities - await em.query('SET search_path TO admin,public') + // Add "admin" and "curator" schema to search path in order to be able to access "hidden" entities + await em.query('SET search_path TO admin,curator,public') const nextEntityIds = await em.find(NextEntityId, {}) return new EntityManagerOverlay(em, nextEntityIds, afterDbUpdate) } @@ -385,6 +402,13 @@ export class EntityManagerOverlay { // Update database - "flush" the cached state async updateDatabase() { + // Acquire all locks first in order to avoid deadlocks + await Promise.all( + Array.from(this.repositories.values()).map(async (r) => { + await r.acquireLocks() + }) + ) + // Execute the write operations await Promise.all( Array.from(this.repositories.values()).map(async (r) => { await r.executeScheduledUpdates()