diff --git a/.env.example b/.env.example index faac9527d..08b6660fc 100644 --- a/.env.example +++ b/.env.example @@ -15,7 +15,6 @@ MATTERS_CLOUDFLARE_ACCOUNT_ID= MATTERS_CLOUDFLARE_ACCOUNT_HASH= MATTERS_CLOUDFLARE_API_TOKEN= MATTERS_CLOUDFLARE_TURNSTILE_SECRET_KEY= -MATTERS_VERIFY_CAPTCHA_TOKENS_THRESHOLDS="[0.5, 1.0]" # to be replaced by sns/sqs fanout mode in next release MATTERS_AWS_IPFS_ARTICLES_QUEUE_URL="https://sqs.ap-southeast-1.amazonaws.com/903380195283/my-queue.fifo" @@ -68,7 +67,6 @@ MATTERS_LIKECOIN_PAY_LIKER_ID=developer MATTERS_LIKECOIN_PAY_WEBHOOK_SECRET= MATTERS_TRANSLATE_CREDENTIAL_PATH=.ebextensions/translate-credentials.local.json MATTERS_GCP_PROJECT_ID=matters-2dd78 -MATTERS_RECAPTCHA_KEY= MATTERS_STRIPE_SECRET= MATTERS_STRIPE_WEBHOOK_SECRET= MATTERS_STRIPE_CONNECT_WEBHOOK_SECRET= diff --git a/.eslintignore b/.eslintignore index d3567a420..0f54d6248 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,4 @@ .eslintrc.js src/definitions/schema.d.ts build/* +**/__test__/* diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 5275047c7..738fb9f44 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -9,6 +9,10 @@ on: types: - closed +concurrency: + group: ${{ github.workflow }}-${{ github.base_ref }} + cancel-in-progress: true + env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} @@ -51,14 +55,14 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v2 with: - node-version: '16' + node-version: '18' - name: Cache NPM dependencies uses: actions/cache@v3 id: node_modules_cache with: path: node_modules - key: ${{ runner.os }}-npm-v3-${{ hashFiles('package-lock.json') }} + key: ${{ runner.os }}-v18-npm-v3-${{ hashFiles('package-lock.json') }} - name: Install Dependencies if: steps.node_modules_cache.outputs.cache-hit != 'true' @@ -129,6 +133,27 @@ jobs: # === [END] predeploy === # === `develop` branch === + - name: Start VPN (develop) + if: github.base_ref == 'develop' + run: | + sudo apt-get update \ + && sudo apt-get install openvpn \ + && echo $VPN_AUTH | base64 -d > $VPN_AUTH_PATH \ + && echo $VPN_CONFIG | base64 -d > $VPN_CONFIG_PATH \ + && sudo openvpn --config $VPN_CONFIG_PATH --auth-user-pass $VPN_AUTH_PATH --daemon \ + && sleep 15s + env: + VPN_CONFIG: ${{ secrets.DEVELOP_VPN_CONFIG }} + VPN_CONFIG_PATH: '.github/config.ovpn' + VPN_AUTH: ${{ secrets.DEVELOP_VPN_AUTH }} + VPN_AUTH_PATH: '.github/auth.txt' + + - name: Check DB Connection + if: github.base_ref == 'develop' + run: nc -zv -w 5 $MATTERS_PG_HOST 5432 + env: + MATTERS_PG_HOST: ${{ secrets.DEVELOP_PG_HOST }} + - name: DB Migration (develop) if: github.base_ref == 'develop' run: npm run db:migrate diff --git a/.github/workflows/schema.yml b/.github/workflows/schema.yml index 18f164e86..f1d4c6c03 100644 --- a/.github/workflows/schema.yml +++ b/.github/workflows/schema.yml @@ -25,7 +25,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v2 with: - node-version: '16' + node-version: '18' - name: Get Version id: version @@ -36,7 +36,7 @@ jobs: id: node_modules_cache with: path: node_modules - key: ${{ runner.os }}-npm-v3-${{ hashFiles('package-lock.json') }} + key: ${{ runner.os }}-v18-npm-v3-${{ hashFiles('package-lock.json') }} - name: Install Dependencies if: steps.node_modules_cache.outputs.cache-hit != 'true' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3fcb774bf..d4322d815 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,14 +42,14 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v2 with: - node-version: '16' + node-version: '18' - name: Cache NPM dependencies uses: actions/cache@v3 id: node_modules_cache with: path: node_modules - key: ${{ runner.os }}-npm-v3-${{ hashFiles('package-lock.json') }} + key: ${{ runner.os }}-v18-npm-v3-${{ hashFiles('package-lock.json') }} - name: Install Dependencies if: steps.node_modules_cache.outputs.cache-hit != 'true' @@ -63,6 +63,7 @@ jobs: - name: Test run: npm run test env: + NODE_OPTIONS: "--no-experimental-fetch" CODECOV_TOKEN: de5ab681-0837-4a24-b614-0a29225a7e4c MATTERS_ENV: test MATTERS_LOGGING_LEVEL: warn diff --git a/.gitignore b/.gitignore index 09b431585..6934cd49a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ *.rdb *.log *~ -tags yarn.lock node_modules diff --git a/codegen.json b/codegen.json index ac182f242..8312f66e1 100644 --- a/codegen.json +++ b/codegen.json @@ -29,10 +29,11 @@ "TagOSS": "./tag#Tag", "Collection": "./collection#Collection", "Comment": "./comment#Comment", - "Article": "./draft#Draft", - "ArticleAccess": "./draft#Draft", - "ArticleOSS": "./draft#Draft", - "ArticleContents": "./draft#Draft", + "Article": "./article#Article", + "ArticleAccess": "./article#Article", + "ArticleOSS": "./article#Article", + "ArticleVersion": "./article#ArticleVersion", + "ArticleContents": "./article#ArticleVersion", "Draft": "./draft#Draft", "DraftAccess": "./draft#Draft", "Circle": "./circle#Circle", @@ -62,7 +63,8 @@ "OfficialAnnouncementNotice": "./notification#NoticeItem", "Appreciation": "./appreciation#Appreciation", "OAuthClient": "./user#OAuthClientDB", - "Topic": "./topic#Topic" + "Report": "./report#Report", + "IcymiTopic": "./misc#MattersChoiceTopic" }, "contextType": "./index#Context", "makeResolverTypeCallable": true, diff --git a/db/migrations/20231221154057_alter_report_add_reason.js b/db/migrations/20231221154057_alter_report_add_reason.js new file mode 100644 index 000000000..786b57c53 --- /dev/null +++ b/db/migrations/20231221154057_alter_report_add_reason.js @@ -0,0 +1,28 @@ +const table = { + report: 'report', + report_asset: 'report_asset', +} + +exports.up = async (knex) => { + await knex.schema.dropTable(table.report_asset) + await knex.table(table.report).delete() + await knex.schema.table(table.report, function (t) { + t.enu('reason', [ + 'tort', + 'illegal_advertising', + 'discrimination_insult_hatred', + 'pornography_involving_minors', + 'other', + ]).notNullable() + t.dropColumn('category') + t.renameColumn('user_id', 'reporter_id') + }) +} + +exports.down = async (knex) => { + await knex.schema.table(table.report, function (t) { + t.string('category').notNullable() + t.dropColumn('reason') + t.renameColumn('reporter_id', 'user_id') + }) +} diff --git a/db/migrations/20240126160530_migrate_article_versioning.js b/db/migrations/20240126160530_migrate_article_versioning.js new file mode 100644 index 000000000..73f6d98b8 --- /dev/null +++ b/db/migrations/20240126160530_migrate_article_versioning.js @@ -0,0 +1,83 @@ +const { baseDown } = require('../utils') + +const articleTable = 'article' +const articleContentTable = 'article_content' +const articleVersionTable = 'article_version' + +exports.up = async (knex) => { + // schema migration + + // create new tables, add new columns to article table + await knex('entity_type').insert({ table: articleContentTable }) + await knex.schema.createTable(articleContentTable, (t) => { + t.bigIncrements('id').primary() + t.text('content').notNullable() + t.string('hash').notNullable().unique() + t.timestamp('created_at').defaultTo(knex.fn.now()) + }) + + await knex('entity_type').insert({ table: articleVersionTable }) + await knex.schema.createTable(articleVersionTable, (t) => { + t.bigIncrements('id').primary() + t.bigInteger('article_id').unsigned().notNullable() + t.string('title').notNullable() + t.bigInteger('cover').unsigned() + t.string('summary').notNullable() + t.boolean('summary_customized').notNullable() + t.bigInteger('content_id').unsigned().notNullable() + t.bigInteger('content_md_id').unsigned() + t.specificType('tags', 'text ARRAY').notNullable() + t.specificType('connections', 'text ARRAY').notNullable() + t.integer('word_count').notNullable() + t.string('data_hash') + t.string('media_hash') + t.string('language') + t.bigInteger('circle_id').unsigned() + t.enu('access', ['public', 'paywall']).notNullable() + t.enu('license', [ + 'cc_0', + 'cc_by_nc_nd_2', + 'cc_by_nc_nd_4', + 'arr', + ]).notNullable() + t.string('iscn_id') + t.string('request_for_donation') + t.string('reply_to_donator') + t.boolean('can_comment').notNullable() + t.boolean('sensitive_by_author').notNullable() + t.text('description') + t.timestamp('created_at').defaultTo(knex.fn.now()) + t.timestamp('updated_at').defaultTo(knex.fn.now()) + + t.foreign('cover').references('id').inTable('asset') + t.foreign('article_id').references('id').inTable('article') + t.foreign('circle_id').references('id').inTable('circle') + + t.index('article_id') + t.index(['article_id', 'id']) + t.index('data_hash') + t.index('media_hash') + t.index('iscn_id') + t.index('title') + }) + await knex.schema.alterTable(articleTable, (t) => { + t.boolean('sensitive_by_admin').notNullable().defaultTo(false) + t.setNullable('uuid') + t.setNullable('title') + t.setNullable('slug') + t.setNullable('content') + t.setNullable('summary') + t.setNullable('word_count') + }) + await knex.schema.alterTable('draft', (t) => { + t.setNullable('uuid') + }) +} + +exports.down = async (knex) => { + await baseDown(articleVersionTable)(knex) + await baseDown(articleContentTable)(knex) + await knex.schema.alterTable(articleTable, (t) => { + t.dropColumn('sensitive_by_admin') + }) +} diff --git a/db/migrations/20240126160531_migrate_article_versioning_data.js b/db/migrations/20240126160531_migrate_article_versioning_data.js new file mode 100644 index 000000000..0e88b71d5 --- /dev/null +++ b/db/migrations/20240126160531_migrate_article_versioning_data.js @@ -0,0 +1,104 @@ +exports.up = async (knex) => { + // data migration + + // migrate data from draft to article_content, article_version + await knex.schema.raw(` +DO $$ +DECLARE + draft_record RECORD; + content_hash TEXT; + content_id BIGINT; + content_md_hash TEXT; + content_md_id BIGINT; + article_version_id BIGINT; +BEGIN + RAISE NOTICE 'start data migration'; + FOR draft_record IN + SELECT * FROM draft WHERE article_id IS NOT NULL AND publish_state='published' ORDER BY id + LOOP + RAISE NOTICE ' processing draft %', draft_record.id; + + -- get content_id, content_md_id + + content_hash := md5(COALESCE (draft_record.content, '')); + SELECT id INTO content_id FROM article_content WHERE hash = content_hash; + IF NOT FOUND THEN + INSERT INTO article_content (content, hash) VALUES (COALESCE (draft_record.content, ''), content_hash) RETURNING id into content_id; + END IF; + RAISE NOTICE ' content_id %', content_id; + + content_md_hash := md5(draft_record.content_md); + IF content_md_hash IS NULL THEN + content_md_id := NULL; + ELSE + SELECT id INTO content_md_id FROM article_content WHERE hash = content_md_hash; + IF NOT FOUND THEN + INSERT INTO article_content (content, hash) VALUES (draft_record.content_md, content_md_hash) RETURNING id into content_md_id; + END IF; + END IF; + RAISE NOTICE ' content_md_id %', content_md_id; + + -- insert article_version table + + INSERT INTO article_version ( + article_id, + title, + cover, + summary, + summary_customized, + content_id, + content_md_id, + tags, + connections, + word_count, + data_hash, + media_hash, + language, + circle_id, + access, + license, + iscn_id, + request_for_donation, + reply_to_donator, + can_comment, + sensitive_by_author, + created_at, + updated_at + ) VALUES ( + draft_record.article_id, + draft_record.title, + draft_record.cover, + COALESCE (draft_record.summary, ''), + draft_record.summary_customized, + content_id, + content_md_id, + COALESCE (draft_record.tags, '{}'), + COALESCE (draft_record.collection, '{}'), + COALESCE (draft_record.word_count, 0), + draft_record.data_hash, + draft_record.media_hash, + draft_record.language, + draft_record.circle_id, + draft_record.access, + draft_record.license, + draft_record.iscn_id, + draft_record.request_for_donation, + draft_record.reply_to_donator, + draft_record.can_comment, + draft_record.sensitive_by_author, + draft_record.created_at, + draft_record.updated_at + ) RETURNING id INTO article_version_id; + RAISE NOTICE ' article_version_id %', article_version_id; + IF draft_record.sensitive_by_admin = true THEN + UPDATE article SET sensitive_by_admin = true WHERE id = draft_record.article_id; + END IF; + END LOOP; +END +$$; + `) +} + +exports.down = () => { + // do nothing +} diff --git a/db/migrations/20240130120000_alter_article_count.js b/db/migrations/20240130120000_alter_article_count.js new file mode 100644 index 000000000..be006a9ca --- /dev/null +++ b/db/migrations/20240130120000_alter_article_count.js @@ -0,0 +1,71 @@ +const { alterEnumString } = require('../utils') + +const topicView = 'article_count_view' +const topicMaterialized = 'article_count_materialized' + +exports.up = async (knex) => { + // remove dependency on article.title + // DDL belowed derived from 20210204115557_alter_comment_type.js + + await knex.raw(/* sql */ ` + DROP VIEW IF EXISTS ${topicView} CASCADE; + DROP MATERIALIZED VIEW IF EXISTS ${topicMaterialized} CASCADE; + `) + + await knex.raw(/*sql*/ ` + CREATE VIEW ${topicView} AS + SELECT + id, + comments_total, + commenters_7d, + commenters_1d, + recent_comment_since, + ( + comments_total * 1 / 5 + commenters_7d * 10 + commenters_1d * 50 * ( + CASE WHEN recent_comment_since <= 8100 THEN + sqrt(8100 / recent_comment_since) + ELSE + 1 + END)) * ( + CASE WHEN commenters_7d > 2 THEN + 1 + ELSE + 0 + END) AS score + FROM ( + SELECT + article.id, + count(*) AS comments_total, + count(DISTINCT ( + CASE WHEN now() - "comment"."created_at" <= '1 week' THEN + "comment"."author_id" + END)) AS commenters_7d, + count(DISTINCT ( + CASE WHEN now() - "comment"."created_at" <= '1 day' THEN + "comment"."author_id" + END)) AS commenters_1d, + extract(epoch FROM now() - max("comment"."created_at")) AS recent_comment_since + FROM + article + LEFT JOIN comment ON "article"."id" = "comment"."target_id" + WHERE + "comment"."state" = 'active' + AND "comment"."type" = 'article' + GROUP BY + article.id) AS comment_score; + + CREATE materialized VIEW ${topicMaterialized} AS + SELECT + * + FROM + ${topicView}; + + CREATE UNIQUE INDEX ${topicMaterialized}_id on public.${topicMaterialized} (id); + `) +} + +exports.down = async (knex) => { + await knex.raw( + /*sql*/ `DROP MATERIALIZED VIEW IF EXISTS ${topicMaterialized} CASCADE` + ) +} diff --git a/db/migrations/20240130120001_alter_article_hottest.js b/db/migrations/20240130120001_alter_article_hottest.js new file mode 100644 index 000000000..7054f5eb7 --- /dev/null +++ b/db/migrations/20240130120001_alter_article_hottest.js @@ -0,0 +1,141 @@ +const view = `article_hottest_view` +const materialized = `article_hottest_materialized` +const index = 'article_hottest_materialized_score_index' + +const time_window = 3 +const donation_decay_factor = 0.5 +const boost = 1 +const boost_window = 3 +const matty_donation_decay_factor = 0.95 +const circle_boost = 2 + +exports.up = async (knex) => { + // remove dependency on `article.title` and `article.media_hash` and create index + // DDL belowed derived from 20231221080000_update_hottest_feed-tag-boost.js + + await knex.raw(/*sql*/ ` +CREATE OR REPLACE AGGREGATE mul(real) ( SFUNC = float4mul, STYPE=real ); +-- or the generic version: CREATE OR REPLACE FUNCTION mul(anyelement, anyelement) RETURNS anyelement LANGUAGE sql AS 'SELECT $1 * coalesce($2, 1)' ; + +-- example: clamp(subject, min, max) +CREATE OR REPLACE FUNCTION clamp(real, real, real) RETURNS real +AS 'SELECT GREATEST($2, LEAST($3, $1));' +LANGUAGE SQL IMMUTABLE RETURNS NULL ON NULL INPUT; + +DROP VIEW IF EXISTS ${view} CASCADE; + +CREATE VIEW ${view} AS +WITH original_score AS ( + select max(read_time_efficiency_boost) as max_efficiency from + ( + select + a.id, + case when extract(epoch from now()-a.created_at) <= ${boost_window}*3600 then ${boost}*(sum(arc.read_time)::decimal/least(extract(epoch from now()-a.created_at)::decimal + 1, ${time_window}*24*3600))^0.5 + when ac.article_id is not null and extract(epoch from now()-a.created_at) < 24*3600 then ${circle_boost}*(sum(arc.read_time)::decimal/least(extract(epoch from now()-a.created_at)::decimal + 1, ${time_window}*24*3600))^0.5 + else (sum(arc.read_time)::decimal/least(extract(epoch from now()-a.created_at)::decimal + 1, ${time_window}*24*3600))^0.5 end as read_time_efficiency_boost + from article a + join public.user u on a.author_id = u.id + join article_read_count arc on a.id = arc.article_id + left join article_circle ac on ac.article_id = a.id + where a.state = 'active' + and arc.created_at >= to_timestamp((extract(epoch from now()) - ${time_window}*24*3600)) + and arc.user_id is not null + and a.id not in (select entrance_id from article_connection where article_id = 8079 ) + group by a.id, ac.article_id, ac.created_at + ) t +), tag_boost_rank AS ( + SELECT *, percent_rank() OVER(ORDER BY boost) + FROM tag_boost + WHERE created_at >= '2023-12-01' +) + + select article.id, article.created_at, 'https://matters.town/@-/' || article.id as link, + (COALESCE(clamp(tag_boost_eff, 0.5, 2), 1.0) * COALESCE(t.score, 0)) AS score, + tag_boost_eff, -- adjust boost_eff in range [0.5, 2] + COALESCE(t.score, 0) as score_prev -- save the previous score without tag boost for comparison + from article + left join + ( + select + case when ac.id is not null then 1 else 0 end as is_from_circle, + t1.*, + t2.latest_transaction, t3.latest_transaction_matty, t2.count_normal_transaction, + (select max_efficiency from original_score) as max_efficiency, + greatest( + (select max_efficiency from original_score)*coalesce((${donation_decay_factor}^coalesce(t2.count_normal_transaction, 1))^(extract(epoch from now()-t2.latest_transaction)/3600) ::numeric(6,0), 0), + (select max_efficiency from original_score)*coalesce(${matty_donation_decay_factor}^(extract(epoch from now()-t3.latest_transaction_matty)/3600) ::numeric(6,0), 0) + ) as donation_score, + t1.read_time_efficiency_boost + greatest( + (select max_efficiency from original_score)*coalesce((${donation_decay_factor}^coalesce(t2.count_normal_transaction, 1))^(extract(epoch from now()-t2.latest_transaction)/3600) ::numeric(6,0), 0), + (select max_efficiency from original_score)*coalesce(${matty_donation_decay_factor}^(extract(epoch from now()-t3.latest_transaction_matty)/3600) ::numeric(6,0), 0) + ) as score, + tag_boost_eff + from + ( + select + a.id, + a.created_at, + u.display_name, + 'https://matters.town/@-/' || a.id as link, + sum(arc.read_time) as read_seconds_in_time_window, + (sum(arc.read_time)::decimal/least(extract(epoch from now()-a.created_at)::decimal + 1, ${time_window}*24*3600))^0.5 as read_time_efficiency, + case when extract(epoch from now()-a.created_at) <= ${boost_window}*3600 then ${boost}*(sum(arc.read_time)::decimal/least(extract(epoch from now()-a.created_at)::decimal + 1, ${time_window}*24*3600))^0.5 + when ac.article_id is not null and extract(epoch from now()-ac.created_at) < 24*3600 then ${circle_boost}*(sum(arc.read_time)::decimal/least(extract(epoch from now()-a.created_at)::decimal + 1, ${time_window}*24*3600))^0.5 + else (sum(arc.read_time)::decimal/least(extract(epoch from now()-a.created_at)::decimal + 1, ${time_window}*24*3600))^0.5 end as read_time_efficiency_boost + from article a + join public.user u on a.author_id = u.id + join article_read_count arc on a.id = arc.article_id + left join article_circle ac on ac.article_id = a.id + where a.state = 'active' + and arc.created_at > to_timestamp((extract(epoch from now()) - ${time_window}*24*3600)) + and arc.user_id is not null + group by a.id, ac.article_id, ac.created_at, u.display_name + ) t1 + left join + ( + select target_id, max(updated_at) as latest_transaction, count(1) as count_normal_transaction + from transaction + where target_type = 4 and state = 'succeeded' and purpose = 'donation' + -- and (currency = 'LIKE' and amount >= 100 or currency = 'HKD') + -- and sender_id = 0 + and ((currency = 'HKD' and amount >= 5) or (currency = 'USDT' and amount >= 0.5)) + and sender_id not in (81, 6, 11, 89281, 93960) + group by target_id + ) t2 on t1.id = t2.target_id + left join + ( + select target_id, + max(updated_at) FILTER(WHERE sender_id IN (81, 6, 11, 89281, 93960)) AS latest_transaction_matty + from transaction + where target_type = 4 and state = 'succeeded' and purpose = 'donation' + group by target_id + ) t3 on t1.id = t3.target_id -- Matty boost + LEFT JOIN ( + SELECT article_id, + MAX(percent_rank) AS max_tag_boost, MIN(percent_rank) AS min_tag_boost, + -- AVG(percent_rank)*2.0-1 AS tag_boost_eff -- in the range of [-1.0, 1.0) + mul(boost) AS tag_boost_eff + FROM article_tag JOIN tag_boost_rank USING(tag_id) + GROUP BY article_id + ) t4 ON t4.article_id=t1.id + + left join article_circle ac on t1.id = ac.article_id + where t1.id not in (select entrance_id from article_connection where article_id = 8079 ) --to block all articles in the Complaint Area + ) t on article.id = t.id + where article.state = 'active' + order by score desc, created_at desc; + + create materialized view ${materialized} as + select * + from ${view}; + + CREATE UNIQUE INDEX ${materialized}_id ON public.${materialized} (id); + CREATE INDEX ${index} ON ${materialized}(score DESC NULLS LAST); + `) +} + +exports.down = function (knex) { + knex.raw(/*sql*/ ` + drop view ${view} cascade; + `) +} diff --git a/db/migrations/20240130120002_alter_curation_tag_materialized.js b/db/migrations/20240130120002_alter_curation_tag_materialized.js new file mode 100644 index 000000000..32fc00af7 --- /dev/null +++ b/db/migrations/20240130120002_alter_curation_tag_materialized.js @@ -0,0 +1,36 @@ +/** + * This migration script is for generating curation tags ID list. + * + */ +const view = 'curation_tag_materialized' + +exports.up = async (knex) => { + // remove dependency on article.title + // DDL belowed derived from 20220420023434_update_curation_tag_materialized.js + + // drop current materialized view + await knex.raw(`DROP MATERIALIZED VIEW IF EXISTS ${view}`) + + // recreate new materialized view + await knex.raw(` + CREATE MATERIALIZED VIEW ${view} AS + SELECT t.id, RANDOM() AS uuid, SUM(t1.sum_read_time) AS sum_read_time_top_n + FROM tag t JOIN + ( + SELECT at.tag_id, at.article_id, art.sum_read_time, rank() over (partition by at.tag_id order by art.sum_read_time desc) + FROM article_tag at JOIN article a ON at.article_id = a.id AND a.created_at > now() - Interval '14 day' + JOIN article_read_time_materialized art ON at.article_id = art.article_id + ) t1 ON t.id = t1.tag_id + WHERE t1.rank <= 20 + AND t.description IS NOT NULL + AND t.deleted = FALSE + GROUP BY t.id + ORDER BY sum_read_time_top_n DESC; + + CREATE UNIQUE INDEX ${view}_id on public.${view} (id); + `) +} + +exports.down = async (knex) => { + await knex.raw(`DROP MATERIALIZED VIEW IF EXISTS ${view}`) +} diff --git a/db/migrations/20240130120003_alter_user_activity_view.js b/db/migrations/20240130120003_alter_user_activity_view.js new file mode 100644 index 000000000..dcbe14156 --- /dev/null +++ b/db/migrations/20240130120003_alter_user_activity_view.js @@ -0,0 +1,172 @@ +const period = 30 +const materialized_view_name = 'user_activity_materialized' + +exports.up = async (knex) => { + // remove dependency on article.title and create index + // DDL belowed derived from 20230914084208_alter_user_activity_view.js + + await knex.raw(/*sql*/ ` + DROP MATERIALIZED VIEW IF EXISTS ${materialized_view_name} CASCADE; + + CREATE MATERIALIZED VIEW ${materialized_view_name} AS + WITH + article_period AS ( + SELECT article.id, article.author_id, article.state, article.created_at FROM article + LEFT JOIN "user" on article.author_id = "user".id + WHERE "user".state = 'active' + AND article.state = 'active' + AND article.created_at >= now() - interval '${period} day' + ), + circle_boardcast_period AS ( + SELECT comment.* FROM comment + WHERE state = 'active' + AND "type" = 'circle_broadcast' + AND parent_comment_id IS NULL + AND created_at >= now() - interval '${period} day' + ), + circle_period AS ( + SELECT circle.* FROM circle + WHERE circle.state = 'active' + AND circle.created_at >= now() - interval '${period} day' + ), + cirlce_subscription_period AS ( + SELECT circle_subscription_item.* FROM circle_subscription_item + LEFT JOIN "user" on circle_subscription_item.user_id = "user".id + WHERE circle_subscription_item.archived = FALSE + AND "user".state = 'active' + AND circle_subscription_item.created_at >= now() - interval '${period} day' + ), + user_follow_period AS ( + SELECT action_user.* FROM action_user + LEFT JOIN "user" on action_user.target_id = "user".id + WHERE action = 'follow' + AND "user".state = 'active' + AND action_user.created_at >= now() - interval '${period} day' + ), + article_donation_period AS ( + SELECT transaction.* FROM transaction + LEFT JOIN article on transaction.target_id = article.id + LEFT JOIN "user" on transaction.recipient_id = "user".id + WHERE transaction.state = 'succeeded' + AND article.state = 'active' + AND "user".state = 'active' + AND transaction.purpose = 'donation' + AND transaction.created_at >= now() - interval '${period} day' + ), + article_tag_period AS ( + SELECT article_tag.* FROM article_tag + LEFT JOIN article on article_tag.article_id = article.id + WHERE article_tag.selected = TRUE + AND article.state = 'active' + AND article_tag.created_at >= now() - interval '${period} day' + ) + + SELECT * FROM ( + SELECT + id AS id, + 'UserPublishArticleActivity' AS "type", + author_id AS actor_id, + id AS node_id, + 'Article' AS node_type, + null AS target_id, + null AS target_type, + created_at + FROM article_period + + UNION + SELECT + id AS id, + 'UserBroadcastCircleActivity' AS "type", + author_id AS actor_id, + id AS node_id, + 'Comment' AS node_type, + target_id, + 'Circle' AS target_type, + created_at + FROM circle_boardcast_period + + UNION + SELECT + id AS id, + 'UserCreateCircleActivity' AS "type", + owner AS actor_id, + id AS node_id, + 'Circle' AS node_type, + null AS target_id, + null AS target_type, + created_at + FROM circle_period + + UNION + SELECT + cirlce_subscription_period.id as id, + 'UserSubscribeCircleActivity' AS "type", + user_id AS actor_id, + circle.id AS node_id, + 'Circle' AS node_type, + null AS target_id, + null AS target_type, + cirlce_subscription_period.created_at + FROM cirlce_subscription_period + LEFT JOIN circle_price ON circle_price.id = price_id + LEFT JOIN circle ON circle.id = circle_price.circle_id + WHERE circle.state = 'active' + + UNION + SELECT + id as id, + 'UserFollowUserActivity' AS "type", + user_id AS actor_id, + target_id AS node_id, + 'User' AS node_type, + null AS target_id, + null AS target_type, + created_at + FROM user_follow_period + + UNION + SELECT + id as id, + 'UserDonateArticleActivity' AS "type", + sender_id AS actor_id, + target_id AS node_id, + 'Article' AS node_type, + null AS target_id, + null AS target_type, + created_at + FROM article_donation_period + + UNION + SELECT + article_tag_period.id as id, + 'UserAddArticleTagActivity' AS "type", + article.author_id AS actor_id, + article_id AS node_id, + 'Article' AS node_type, + tag_id AS target_id, + 'Tag' AS target_type, + article_tag_period.created_at + FROM article_tag_period LEFT JOIN article ON article.id = article_id + ) AS user_activity; + `) + + // add indexes + await knex.schema.table(materialized_view_name, (t) => { + t.index('id') + .index('type') + .index('actor_id') + .index('created_at') + .index(['node_id', 'node_type']) + .index(['target_id', 'target_type']) + }) + // add unique index for refresh concurrently + await knex.schema.table(materialized_view_name, (t) => { + t.unique(['id', 'type'], { useConstraint: false }) + }) +} + +exports.down = async (knex) => { + await knex.raw( + /*sql*/ `DROP MATERIALIZED VIEW IF EXISTS ${materialized_view_name} CASCADE` + ) +} diff --git a/db/migrations/20240208144112_create_article_version_newest_view.js b/db/migrations/20240208144112_create_article_version_newest_view.js new file mode 100644 index 000000000..ace8be0c9 --- /dev/null +++ b/db/migrations/20240208144112_create_article_version_newest_view.js @@ -0,0 +1,15 @@ +const table = 'article_version_newest' + +exports.up = async (knex) => + knex.raw(/*sql*/ ` + create view ${table} as + SELECT a.* + FROM article_version a + LEFT OUTER JOIN article_version b + ON a.article_id= b.article_id AND a.id < b.id + WHERE b.id IS NULL; + `) + +exports.down = function (knex) { + return knex.raw(/*sql*/ `drop view ${table}`) +} diff --git a/db/migrations/20240219000030_alter_article_short_hash.js b/db/migrations/20240219000030_alter_article_short_hash.js new file mode 100644 index 000000000..de2b25a3a --- /dev/null +++ b/db/migrations/20240219000030_alter_article_short_hash.js @@ -0,0 +1,16 @@ +const table = 'article' + +const newColumn = 'short_hash' + +exports.up = async (knex) => { + await knex.schema.table(table, (t) => { + t.string(newColumn).unique() // add .notNullable() after filled short_hash for all rows + t.index(newColumn) + }) +} + +exports.down = async (knex) => { + await knex.schema.table(table, (t) => { + t.dropColumn(newColumn) + }) +} diff --git a/db/migrations/20240226131552_migrate_article_version_id_foreign_key.js b/db/migrations/20240226131552_migrate_article_version_id_foreign_key.js new file mode 100644 index 000000000..83ed8d66d --- /dev/null +++ b/db/migrations/20240226131552_migrate_article_version_id_foreign_key.js @@ -0,0 +1,34 @@ +exports.up = async (knex) => { + // add article_version_id field + await knex.schema.alterTable('comment', (t) => { + t.bigInteger('article_version_id') + t.foreign('article_version_id').references('id').inTable('article_version') + }) + await knex.schema.alterTable('transaction', (t) => { + t.bigInteger('article_version_id') + t.foreign('article_version_id').references('id').inTable('article_version') + }) + await knex.schema.alterTable('action_article', (t) => { + t.bigInteger('article_version_id') + t.foreign('article_version_id').references('id').inTable('article_version') + }) + await knex.schema.alterTable('article_translation', (t) => { + t.bigInteger('article_version_id') + t.foreign('article_version_id').references('id').inTable('article_version') + }) +} + +exports.down = async (knex) => { + await knex.schema.alterTable('article_translation', (t) => { + t.dropColumn('article_version_id') + }) + await knex.schema.alterTable('comment', (t) => { + t.dropColumn('article_version_id') + }) + await knex.schema.alterTable('transaction', (t) => { + t.dropColumn('article_version_id') + }) + await knex.schema.alterTable('action_article', (t) => { + t.dropColumn('article_version_id') + }) +} diff --git a/db/migrations/20240226131553_migrate_article_version_id_data.js b/db/migrations/20240226131553_migrate_article_version_id_data.js new file mode 100644 index 000000000..7210e96c0 --- /dev/null +++ b/db/migrations/20240226131553_migrate_article_version_id_data.js @@ -0,0 +1,52 @@ +exports.up = async (knex) => { + // populate article_version_id + await knex.raw(` + UPDATE comment + SET article_version_id = ( + SELECT article_version.id + FROM article_version + WHERE article_version.article_id = comment.target_id + AND article_version.created_at <= comment.created_at + ORDER BY article_version.created_at DESC + LIMIT 1 + ) + WHERE type = 'article' + `) + await knex.raw(` + UPDATE transaction + SET article_version_id = ( + SELECT article_version.id + FROM article_version + WHERE article_version.article_id = transaction.target_id + AND article_version.created_at <= transaction.created_at + ORDER BY article_version.created_at DESC + LIMIT 1 + ) WHERE target_type = 4 + `) + await knex.raw(` + UPDATE action_article + SET article_version_id = ( + SELECT article_version.id + FROM article_version + WHERE article_version.article_id = action_article.target_id + AND article_version.created_at <= action_article.created_at + ORDER BY article_version.created_at DESC + LIMIT 1 + ) + `) + await knex.raw(` + UPDATE article_translation + SET article_version_id = ( + SELECT article_version.id + FROM article_version + WHERE article_version.article_id = article_translation.article_id + AND article_version.created_at <= article_translation.created_at + ORDER BY article_version.created_at DESC + LIMIT 1 + ) + `) +} + +exports.down = () => { + // do nothing +} diff --git a/db/migrations/20240227085353_alter_article_translation_unique_key.js b/db/migrations/20240227085353_alter_article_translation_unique_key.js new file mode 100644 index 000000000..1d616845b --- /dev/null +++ b/db/migrations/20240227085353_alter_article_translation_unique_key.js @@ -0,0 +1,15 @@ +const article_translation_table = 'article_translation' + +exports.up = async (knex) => { + await knex.schema.table(article_translation_table, (t) => { + t.dropUnique(['article_id', 'language']) + t.unique(['article_version_id', 'language']) + }) +} + +exports.down = async (knex) => { + await knex.schema.table(article_translation_table, (t) => { + t.dropUnique(['article_version_id', 'language']) + t.unique(['article_id', 'language']) + }) +} diff --git a/db/migrations/20240307104431_create_matters_choice_topic_table.js b/db/migrations/20240307104431_create_matters_choice_topic_table.js new file mode 100644 index 000000000..7da0ce406 --- /dev/null +++ b/db/migrations/20240307104431_create_matters_choice_topic_table.js @@ -0,0 +1,22 @@ +const { baseDown } = require('../utils') + +const table = 'matters_choice_topic' + +exports.up = async (knex) => { + await knex('entity_type').insert({ table }) + await knex.schema.createTable(table, (t) => { + t.bigIncrements('id').primary() + t.string('title').notNullable() + t.string('note') + t.specificType('articles', 'bigint ARRAY') + t.integer('pin_amount').unsigned().notNullable() + t.enum('state', ['published', 'editing', 'archived']).notNullable() + t.timestamp('published_at') + t.timestamp('created_at').defaultTo(knex.fn.now()) + t.timestamp('updated_at').defaultTo(knex.fn.now()) + + t.index('state') + }) +} + +exports.down = baseDown(table) diff --git a/db/migrations/20240326181309_drop_topic_chapter_related_tables.js b/db/migrations/20240326181309_drop_topic_chapter_related_tables.js new file mode 100644 index 000000000..c79eddd9a --- /dev/null +++ b/db/migrations/20240326181309_drop_topic_chapter_related_tables.js @@ -0,0 +1,35 @@ +exports.up = async (knex) => { + // delete data + + const covers = await knex('topic').select('cover') + const assetIds = covers.map(({ cover }) => cover) + + await knex('article_topic').del() + await knex('article_chapter').del() + await knex('chapter').del() + await knex('topic').del() + + if (assetIds.length > 0) { + await knex('asset_map').whereIn('asset_id', assetIds).del() + await knex('asset').whereIn('id', assetIds).del() + } + + // drop tables + + const deleteTable = async (table) => { + await knex('entity_type') + .where({ + table, + }) + .del() + await knex.schema.dropTable(table) + } + await deleteTable('article_topic') + await deleteTable('article_chapter') + await deleteTable('chapter') + await deleteTable('topic') +} + +exports.down = () => { + // do nothing +} diff --git a/db/migrations/20240515080000_create_mat_views_tags_view.js b/db/migrations/20240515080000_create_mat_views_tags_view.js new file mode 100644 index 000000000..ee9e7f693 --- /dev/null +++ b/db/migrations/20240515080000_create_mat_views_tags_view.js @@ -0,0 +1,209 @@ +// coverted from +// https://github.com/thematters/matters-metabase/blob/main/sql/stale-tags-create-table-view.sql +// from AnalysisDB's regular daily view format to materialized view; + +const schema = 'mat_views' +const materialized_view_name = 'tags_view_materialized' + +exports.up = async (knex) => { + await knex.raw(/*sql*/ ` +CREATE SCHEMA IF NOT EXISTS "${schema}" ; + +CREATE OR REPLACE FUNCTION "${schema}".slug(input text) RETURNS text AS $f$ + SELECT COALESCE( + NULLIF(trim(both '-' from regexp_replace(input, '\\W+', '-', 'g')), ''), + -- NULLIF(trim(both '-' from regexp_replace(input, '[()()@''’"<>,.?;&&!│||/##、,.…:;「」《》?!\\\\+—\\-ㅤ\\s]+', '-', 'g')), ''), + NULLIF(trim(both '-' from regexp_replace(input, '\\s+', '-', 'g')), ''), + input + ) +$f$ LANGUAGE SQL IMMUTABLE RETURNS NULL ON NULL INPUT; + +CREATE OR REPLACE FUNCTION "${schema}".is_conforming_tag(input text) RETURNS boolean AS $f$ + SELECT length(input)<=40 AND input ~ '^\\w+$' -- regular expression match +$f$ LANGUAGE SQL IMMUTABLE RETURNS NULL ON NULL INPUT; + + +DROP MATERIALIZED VIEW IF EXISTS "${schema}".${materialized_view_name} CASCADE; + +WITH temp_tag AS ( + SELECT id, created_at, updated_at, remark, deleted, starts_with_hash, same_as_slug, cover, description, editors, creator, owner, major_tag_id, is_major_tag, is_conforming_tag, + content_orig, COALESCE(slug, "${schema}".slug(lower(content))) AS slug + FROM ( + SELECT id ::int, created_at, updated_at, remark, deleted, cover, description, editors ::int[], creator, owner, id ::int AS major_tag_id, + /* (major_tag_id IS NULL OR major_tag_id=id) */ NULL AS is_major_tag, NULL AS slug, + "${schema}".is_conforming_tag(content), + content AS content_orig, + (starts_with(content, '#') OR starts_with(content, '#')) AS starts_with_hash, + (content = "${schema}".slug(content)) AS same_as_slug, + content -- use t2.content alias + FROM public.tag t1 + ) t1 +), temp_article_tag_stats_by_id AS ( + SELECT tag_id ::int, + COUNT(DISTINCT article_id) ::int AS tag_articles, COUNT(DISTINCT author_id) ::int AS tag_authors, + (MAX(at.created_at) + '1 day'::interval - '1 microsecond'::interval) ::date - MIN(at.created_at) ::date AS span_days + FROM public.article_tag at JOIN public.article a ON article_id=a.id AND a.state IN ('active') + GROUP BY 1 +), temp_tag_uses AS ( + SELECT tag_id ::int, ARRAY_AGG(DISTINCT article_id) AS article_ids, COUNT(DISTINCT article_id) ::int + -- FROM article_tag + FROM public.article_tag at JOIN public.article a ON article_id=a.id AND a.state IN ('active') + GROUP BY 1 + HAVING COUNT(article_id)>0 +), temp_tag_common_count AS ( + SELECT t.tag_id, tag_rel_id, count_common, + tu1.count AS count_target, tu2.count AS count_rel, + (tu1.count + tu2.count - count_common) AS count_union, temp_tag.is_conforming_tag + FROM ( + SELECT tu.tag_id ::int, at.tag_id ::int AS tag_rel_id, COUNT(*) ::int AS count_common + FROM temp_tag_uses tu + JOIN article_tag at ON at.article_id =ANY(tu.article_ids) AND (at.tag_id <> tu.tag_id) + GROUP BY 1, 2 + ) t + JOIN temp_tag_uses tu1 ON tu1.tag_id=t.tag_id + JOIN temp_tag_uses tu2 ON tu2.tag_id=t.tag_rel_id + LEFT JOIN temp_tag ON tu2.tag_id=temp_tag.id -- WHERE tag.is_conforming_tag +), temp_tag_similarity AS ( + SELECT *, -- (count1+count2-count_common) AS count_union, + ROUND(count_common ::numeric / count_union, 5) AS similarity, + ROUND((count_common ::numeric / count_target + count_common ::numeric / count_rel)/2, 5) AS diff_similarity, + -- ROUND(count_common ::numeric / count_union * count_rel, 5) AS sim_to_target, + rank() OVER(PARTITION BY tag_id ORDER BY + (count_common ::numeric * count_rel / count_union) DESC, -- count_rel * similarity DESC + (count_common ::numeric / count_union) DESC, count_common DESC) -- FILTER (WHERE is_conforming_tag) + FROM temp_tag_common_count t +), temp_article_tag_rels_by_id AS ( + SELECT tag_id, to_jsonb((ARRAY_AGG( + (to_jsonb(ts.*) - '{rank,tag_id,count_target,is_conforming_tag}'::text[] -- || jsonb_build_object('tag_content', temp_tag.content) + ) ORDER BY rank ASC) FILTER (WHERE is_conforming_tag) + )[1:100]) AS top_rels + FROM temp_tag_similarity ts + -- JOIN temp_tag ON ts.tag_rel_id=temp_tag.id + WHERE ts.is_conforming_tag AND + rank<=130 -- extract only first 100 from + GROUP BY 1 +), action_tag_stats AS ( + SELECT target_id AS tag_id, action, COUNT(*) ::int AS count, + COUNT(DISTINCT user_id) ::int AS num_users, + MIN(created_at) AS earliest, + MAX(created_at) AS latest, + -- MAX(created_at) ::date - MIN(created_at) ::date AS span_days + (MAX(created_at) + '1 day'::interval - '1 microsecond'::interval) ::date - MIN(created_at) ::date AS span_days + FROM public.action_tag + GROUP BY 1, 2 +), article_tag_stats AS ( + SELECT t.slug, -- COUNT(*) ::int AS count, + COUNT(DISTINCT at.article_id) FILTER(WHERE a.state IN ('active')) ::int AS num_articles, + COUNT(DISTINCT a.author_id) FILTER(WHERE a.state IN ('active')) ::int AS num_authors, + COUNT(DISTINCT at.article_id) FILTER(WHERE age(at.created_at) <= '3 months'::interval AND a.state IN ('active')) ::int AS num_articles_r3m, + COUNT(DISTINCT a.author_id) FILTER(WHERE age(at.created_at) <= '3 months'::interval AND a.state IN ('active')) ::int AS num_authors_r3m, + COUNT(DISTINCT at.article_id) FILTER(WHERE age(at.created_at) <= '1 month'::interval AND a.state IN ('active')) ::int AS num_articles_r1m, + COUNT(DISTINCT a.author_id) FILTER(WHERE age(at.created_at) <= '1 month'::interval AND a.state IN ('active')) ::int AS num_authors_r1m, + COUNT(DISTINCT at.article_id) FILTER(WHERE age(at.created_at) <= '2 weeks'::interval AND a.state IN ('active')) ::int AS num_articles_r2w, + COUNT(DISTINCT a.author_id) FILTER(WHERE age(at.created_at) <= '2 weeks'::interval AND a.state IN ('active')) ::int AS num_authors_r2w, + COUNT(DISTINCT at.article_id) FILTER(WHERE age(at.created_at) <= '1 week'::interval AND a.state IN ('active')) ::int AS num_articles_r1w, + COUNT(DISTINCT a.author_id) FILTER(WHERE age(at.created_at) <= '1 week'::interval AND a.state IN ('active')) ::int AS num_authors_r1w, + (MAX(at.created_at) + '1 day'::interval - '1 microsecond'::interval) ::date - MIN(at.created_at) ::date AS span_days, + MIN(at.created_at) AS earliest_use, + MAX(at.created_at) AS latest_use, + to_jsonb(ARRAY_AGG(DISTINCT jsonb_build_object( + 'id', t.id, 'tag', t.content_orig, 'id_slug', (t.id || '-' || t.slug), + 'tag_articles', tag_articles, 'tag_authors', tag_authors, + 'same_as_slug', t.same_as_slug, -- t.content_orig = pg_temp.slug(t.content_orig), + 'starts_with_hash', t.starts_with_hash, -- (starts_with(content, '#') OR starts_with(content, '#')), + -- 'is_major_tag', t.is_major_tag, -- t.major_tag_id=t.id, + 'is_conforming_tag', t.is_conforming_tag, + 'span_days', at.span_days, 'created_at', t.created_at, + 'deleted', t.deleted, + 'url', ('/tags/' || rtrim(encode(('Tag:' || t.id) ::bytea, 'base64'), '=')) + ) -- ORDER BY tag_articles DESC + )) AS dups, + -- to_jsonb(ARRAY_AGG(DISTINCT to_jsonb(t.*) || jsonb_build_object('editors', t.editors ::int[]))) AS details, + to_jsonb(ARRAY_AGG(DISTINCT to_jsonb(action_tag_stats.*)) FILTER (WHERE action_tag_stats.count IS NOT NULL) ) AS action_details + FROM temp_tag t + LEFT JOIN ( + SELECT * + FROM public.article_tag at + JOIN temp_article_tag_stats_by_id USING (tag_id) + ) at ON t.id=at.tag_id + LEFT JOIN action_tag_stats USING (tag_id) + LEFT JOIN public.article a ON at.article_id=a.id AND a.state IN ('active') + GROUP BY 1 +), tag_slug_aliases AS ( + SELECT slug, + ARRAY_AGG(id_slug_orig + ORDER BY is_major_tag DESC NULLS LAST, deleted ASC NULLS LAST, -- false'deleted' ASC, x->'is_major_tag' DESC, x->'is_conforming_tag' DESC, x->'starts_with_hash' ASC, x->'same_as_slug' DESC, + x->'tag_authors' DESC, x->'tag_articles' DESC, x->'span_days' DESC, (x->>'created_at')::timestamptz ASC, x->'id' ASC + )) AS dups, action_details + FROM temp_tag t + LEFT JOIN temp_article_tag_stats_by_id tag_stats ON tag_stats.tag_id=t.id + LEFT JOIN temp_article_tag_rels_by_id tag_rels ON tag_rels.tag_id=t.id + LEFT JOIN article_tag_stats at2 USING (slug) + -- LEFT JOIN article_tag_stats_by_slug atst USING (slug) + -- LEFT JOIN article_tag_authors_by_slug atat USING (slug) + -- LEFT JOIN tag_read_time_by_slug artst USING (slug) + LEFT JOIN tag_slug_aliases USING (slug) +) t1 +ORDER BY id_slug, + deleted ASC NULLS FIRST, + is_major_tag DESC NULLS LAST, + is_conforming_tag DESC NULLS LAST, + starts_with_hash ASC NULLS LAST, + same_as_slug DESC, tag_authors DESC NULLS LAST, tag_articles DESC NULLS LAST, span_days DESC NULLS LAST, created_at ASC +; `) +} + +exports.down = async (knex) => { + await knex.raw( + /*sql*/ `DROP MATERIALIZED VIEW IF EXISTS ${schema}.${materialized_view_name} CASCADE;` + ) +} diff --git a/db/migrations/20240515080100_create_mat_views_tags_view.js b/db/migrations/20240515080100_create_mat_views_tags_view.js new file mode 100644 index 000000000..451acabd5 --- /dev/null +++ b/db/migrations/20240515080100_create_mat_views_tags_view.js @@ -0,0 +1,221 @@ +// coverted from +// https://github.com/thematters/matters-metabase/blob/main/sql/stale-tags-create-table-view.sql +// from AnalysisDB's regular daily view format to materialized view; + +const schema = 'mat_views' +const materialized_view_name = 'tags_lasts_view_materialized' + +exports.up = async (knex) => { + await knex.raw(/*sql*/ ` +CREATE SCHEMA IF NOT EXISTS "${schema}" ; + +CREATE OR REPLACE FUNCTION "${schema}".slug(input text) RETURNS text AS $f$ + SELECT COALESCE( + NULLIF(trim(both '-' from regexp_replace(input, '\\W+', '-', 'g')), ''), + -- NULLIF(trim(both '-' from regexp_replace(input, '[()()@''’"<>,.?;&&!│||/##、,.…:;「」《》?!\\\\+—\\-ㅤ\\s]+', '-', 'g')), ''), + NULLIF(trim(both '-' from regexp_replace(input, '\\s+', '-', 'g')), ''), + input + ) +$f$ LANGUAGE SQL IMMUTABLE RETURNS NULL ON NULL INPUT; + +CREATE OR REPLACE FUNCTION "${schema}".is_conforming_tag(input text) RETURNS boolean AS $f$ + SELECT length(input)<=40 AND input ~ '^\\w+$' -- regular expression match +$f$ LANGUAGE SQL IMMUTABLE RETURNS NULL ON NULL INPUT; + + +DROP MATERIALIZED VIEW IF EXISTS "${schema}"."${materialized_view_name}" CASCADE; + +CREATE MATERIALIZED VIEW "${schema}"."${materialized_view_name}" AS + +WITH temp_tag AS ( + SELECT id, created_at, updated_at, remark, deleted, starts_with_hash, same_as_slug, cover, description, editors, creator, owner, major_tag_id, is_major_tag, is_conforming_tag, + content_orig, COALESCE(slug, "${schema}".slug(lower(content))) AS slug + FROM ( + SELECT id ::int, created_at, updated_at, remark, deleted, cover, description, editors ::int[], creator, owner, id ::int AS major_tag_id, + /* (major_tag_id IS NULL OR major_tag_id=id) */ NULL AS is_major_tag, NULL AS slug, + "${schema}".is_conforming_tag(content), + content AS content_orig, + (starts_with(content, '#') OR starts_with(content, '#')) AS starts_with_hash, + (content = "${schema}".slug(content)) AS same_as_slug, + content -- use t2.content alias + FROM public.tag t1 + ) t1 +), temp_article_tag_stats_by_id AS ( + SELECT tag_id ::int, + COUNT(DISTINCT article_id) ::int AS tag_articles, COUNT(DISTINCT author_id) ::int AS tag_authors, + (MAX(at.created_at) + '1 day'::interval - '1 microsecond'::interval) ::date - MIN(at.created_at) ::date AS span_days + FROM public.article_tag at JOIN public.article a ON article_id=a.id AND a.state IN ('active') + GROUP BY 1 +), temp_tag_uses AS ( + SELECT tag_id ::int, ARRAY_AGG(DISTINCT article_id) AS article_ids, COUNT(DISTINCT article_id) ::int + -- FROM article_tag + FROM public.article_tag at JOIN public.article a ON article_id=a.id AND a.state IN ('active') + GROUP BY 1 + HAVING COUNT(article_id)>0 +), temp_tag_common_count AS ( + SELECT t.tag_id, tag_rel_id, count_common, + tu1.count AS count_target, tu2.count AS count_rel, + (tu1.count + tu2.count - count_common) AS count_union, temp_tag.is_conforming_tag + FROM ( + SELECT tu.tag_id ::int, at.tag_id ::int AS tag_rel_id, COUNT(*) ::int AS count_common + FROM temp_tag_uses tu + JOIN article_tag at ON at.article_id =ANY(tu.article_ids) AND (at.tag_id <> tu.tag_id) + GROUP BY 1, 2 + ) t + JOIN temp_tag_uses tu1 ON tu1.tag_id=t.tag_id + JOIN temp_tag_uses tu2 ON tu2.tag_id=t.tag_rel_id + LEFT JOIN temp_tag ON tu2.tag_id=temp_tag.id -- WHERE tag.is_conforming_tag +), temp_tag_similarity AS ( + SELECT *, -- (count1+count2-count_common) AS count_union, + ROUND(count_common ::numeric / count_union, 5) AS similarity, + ROUND((count_common ::numeric / count_target + count_common ::numeric / count_rel)/2, 5) AS diff_similarity, + -- ROUND(count_common ::numeric / count_union * count_rel, 5) AS sim_to_target, + rank() OVER(PARTITION BY tag_id ORDER BY + (count_common ::numeric * count_rel / count_union) DESC, -- count_rel * similarity DESC + (count_common ::numeric / count_union) DESC, count_common DESC) -- FILTER (WHERE is_conforming_tag) + FROM temp_tag_common_count t +), temp_article_tag_rels_by_id AS ( + SELECT tag_id, to_jsonb((ARRAY_AGG( + (to_jsonb(ts.*) - '{rank,tag_id,count_target,is_conforming_tag}'::text[] -- || jsonb_build_object('tag_content', temp_tag.content) + ) ORDER BY rank ASC) FILTER (WHERE is_conforming_tag) + )[1:100]) AS top_rels + FROM temp_tag_similarity ts + -- JOIN temp_tag ON ts.tag_rel_id=temp_tag.id + WHERE ts.is_conforming_tag AND + rank<=130 -- extract only first 100 from + GROUP BY 1 +), action_tag_stats AS ( + SELECT target_id AS tag_id, action, COUNT(*) ::int AS count, + COUNT(DISTINCT user_id) ::int AS num_users, + MIN(created_at) AS earliest, + MAX(created_at) AS latest, + -- MAX(created_at) ::date - MIN(created_at) ::date AS span_days + (MAX(created_at) + '1 day'::interval - '1 microsecond'::interval) ::date - MIN(created_at) ::date AS span_days + FROM public.action_tag + GROUP BY 1, 2 +), article_tag_stats AS ( + SELECT t.slug, -- COUNT(*) ::int AS count, + COUNT(DISTINCT at.article_id) FILTER(WHERE a.state IN ('active')) ::int AS num_articles, + COUNT(DISTINCT a.author_id) FILTER(WHERE a.state IN ('active')) ::int AS num_authors, + COUNT(DISTINCT at.article_id) FILTER(WHERE age(at.created_at) <= '3 months'::interval AND a.state IN ('active')) ::int AS num_articles_r3m, + COUNT(DISTINCT a.author_id) FILTER(WHERE age(at.created_at) <= '3 months'::interval AND a.state IN ('active')) ::int AS num_authors_r3m, + COUNT(DISTINCT at.article_id) FILTER(WHERE age(at.created_at) <= '1 month'::interval AND a.state IN ('active')) ::int AS num_articles_r1m, + COUNT(DISTINCT a.author_id) FILTER(WHERE age(at.created_at) <= '1 month'::interval AND a.state IN ('active')) ::int AS num_authors_r1m, + COUNT(DISTINCT at.article_id) FILTER(WHERE age(at.created_at) <= '2 weeks'::interval AND a.state IN ('active')) ::int AS num_articles_r2w, + COUNT(DISTINCT a.author_id) FILTER(WHERE age(at.created_at) <= '2 weeks'::interval AND a.state IN ('active')) ::int AS num_authors_r2w, + COUNT(DISTINCT at.article_id) FILTER(WHERE age(at.created_at) <= '1 week'::interval AND a.state IN ('active')) ::int AS num_articles_r1w, + COUNT(DISTINCT a.author_id) FILTER(WHERE age(at.created_at) <= '1 week'::interval AND a.state IN ('active')) ::int AS num_authors_r1w, + (MAX(at.created_at) + '1 day'::interval - '1 microsecond'::interval) ::date - MIN(at.created_at) ::date AS span_days, + MIN(at.created_at) AS earliest_use, + MAX(at.created_at) AS latest_use, + to_jsonb(ARRAY_AGG(DISTINCT jsonb_build_object( + 'id', t.id, 'tag', t.content_orig, 'id_slug', (t.id || '-' || t.slug), + 'tag_articles', tag_articles, 'tag_authors', tag_authors, + 'same_as_slug', t.same_as_slug, -- t.content_orig = pg_temp.slug(t.content_orig), + 'starts_with_hash', t.starts_with_hash, -- (starts_with(content, '#') OR starts_with(content, '#')), + -- 'is_major_tag', t.is_major_tag, -- t.major_tag_id=t.id, + 'is_conforming_tag', t.is_conforming_tag, + 'span_days', at.span_days, 'created_at', t.created_at, + 'deleted', t.deleted, + 'url', ('/tags/' || rtrim(encode(('Tag:' || t.id) ::bytea, 'base64'), '=')) + ) -- ORDER BY tag_articles DESC + )) AS dups, + -- to_jsonb(ARRAY_AGG(DISTINCT to_jsonb(t.*) || jsonb_build_object('editors', t.editors ::int[]))) AS details, + to_jsonb(ARRAY_AGG(DISTINCT to_jsonb(action_tag_stats.*)) FILTER (WHERE action_tag_stats.count IS NOT NULL) ) AS action_details + FROM temp_tag t + LEFT JOIN ( + SELECT * + FROM public.article_tag at + JOIN temp_article_tag_stats_by_id USING (tag_id) + ) at ON t.id=at.tag_id + LEFT JOIN action_tag_stats USING (tag_id) + LEFT JOIN public.article a ON at.article_id=a.id AND a.state IN ('active') + GROUP BY 1 +), tag_slug_aliases AS ( + SELECT slug, + ARRAY_AGG(id_slug_orig + ORDER BY is_major_tag DESC NULLS LAST, deleted ASC NULLS LAST, -- false'deleted' ASC, x->'is_major_tag' DESC, x->'is_conforming_tag' DESC, x->'starts_with_hash' ASC, x->'same_as_slug' DESC, + x->'tag_authors' DESC, x->'tag_articles' DESC, x->'span_days' DESC, (x->>'created_at')::timestamptz ASC, x->'id' ASC + )) AS dups, action_details + FROM temp_tag t + LEFT JOIN temp_article_tag_stats_by_id tag_stats ON tag_stats.tag_id=t.id + LEFT JOIN temp_article_tag_rels_by_id tag_rels ON tag_rels.tag_id=t.id + LEFT JOIN article_tag_stats at2 USING (slug) + -- LEFT JOIN article_tag_stats_by_slug atst USING (slug) + -- LEFT JOIN article_tag_authors_by_slug atat USING (slug) + -- LEFT JOIN tag_read_time_by_slug artst USING (slug) + LEFT JOIN tag_slug_aliases USING (slug) +) t1 +ORDER BY id_slug, + deleted ASC NULLS FIRST, + is_major_tag DESC NULLS LAST, + is_conforming_tag DESC NULLS LAST, + starts_with_hash ASC NULLS LAST, + same_as_slug DESC, tag_authors DESC NULLS LAST, tag_articles DESC NULLS LAST, span_days DESC NULLS LAST, created_at ASC +; + +CREATE UNIQUE INDEX ON "${schema}"."${materialized_view_name}" (id) ; +CREATE UNIQUE INDEX ON "${schema}"."${materialized_view_name}" (id_slug) ; +CREATE UNIQUE INDEX ON "${schema}"."${materialized_view_name}" (content) ; +CREATE UNIQUE INDEX ON "${schema}"."${materialized_view_name}" (slug) ; + +DROP INDEX IF EXISTS "${schema}"."${materialized_view_name}_dup_tag_ids_idx" ; +CREATE INDEX "${materialized_view_name}_dup_tag_ids_idx" ON "${schema}"."${materialized_view_name}" USING gin(dup_tag_ids) ; + +`) +} + +exports.down = async (knex) => { + await knex.raw( + /*sql*/ `DROP MATERIALIZED VIEW IF EXISTS "${schema}"."${materialized_view_name}" CASCADE;` + ) +} diff --git a/db/seeds/04_draft.js b/db/seeds/04_draft.js index c47ad818e..5e45658ea 100644 --- a/db/seeds/04_draft.js +++ b/db/seeds/04_draft.js @@ -6,7 +6,7 @@ exports.seed = function (knex, Promise) { .then(function () { return knex(table).insert([ { - uuid: '00000000-0000-0000-0000-000000000001', + // uuid: '00000000-0000-0000-0000-000000000001', author_id: '1', title: 'test draft 1', summary: 'Some text of sumamry', @@ -15,7 +15,7 @@ exports.seed = function (knex, Promise) { tags: ['tag1', 'tag2'], }, { - uuid: '00000000-0000-0000-0000-000000000002', + // uuid: '00000000-0000-0000-0000-000000000002', author_id: '2', title: 'test draft 2', summary: 'Some text of sumamry', @@ -23,7 +23,7 @@ exports.seed = function (knex, Promise) { publish_state: 'published', }, { - uuid: '00000000-0000-0000-0000-000000000003', + // uuid: '00000000-0000-0000-0000-000000000003', author_id: '3', title: 'test draft 3', summary: 'Some text of sumamry', @@ -31,7 +31,7 @@ exports.seed = function (knex, Promise) { publish_state: 'published', }, { - uuid: '00000000-0000-0000-0000-000000000004', + // uuid: '00000000-0000-0000-0000-000000000004', author_id: '1', title: 'test draft 4', summary: 'Some text of sumamry', @@ -39,7 +39,7 @@ exports.seed = function (knex, Promise) { publish_state: 'published', }, { - uuid: '00000000-0000-0000-0000-000000000005', + // uuid: '00000000-0000-0000-0000-000000000005', author_id: '7', title: 'test draft 5', summary: 'Some text of sumamry', @@ -47,7 +47,7 @@ exports.seed = function (knex, Promise) { publish_state: 'published', }, { - uuid: '00000000-0000-0000-0000-000000000006', + // uuid: '00000000-0000-0000-0000-000000000006', author_id: 1, title: 'test draft 6', summary: 'Some text of sumamry', diff --git a/db/seeds/05_articles.js b/db/seeds/05_articles.js index 934a5986a..5ab9c89cb 100644 --- a/db/seeds/05_articles.js +++ b/db/seeds/05_articles.js @@ -1,92 +1,162 @@ -const table = 'article' +exports.seed = async function (knex, Promise) { + const draftTable = 'draft' + const articleTable = 'article' + const articleVersionTable = 'article_version' + const articleContentTable = 'article_content' -exports.seed = function (knex, Promise) { - return knex(table) - .del() - .then(function () { - return knex(table).insert([ - { - uuid: '00000000-0000-0000-0000-000000000001', - author_id: 1, - draft_id: 1, - title: 'test article 1', - slug: 'test-article-1', - summary: 'Some text', - word_count: '1000', - data_hash: 'someIpfsDataHash1', - media_hash: 'someIpfsMediaHash1', - content: '
some html string
', - state: 'active', - }, - { - uuid: '00000000-0000-0000-0000-000000000002', - author_id: 2, - draft_id: 2, - title: 'test article 2', - slug: 'test-article-2', - summary: 'Some text', - word_count: '1000', - data_hash: 'someIpfsDataHash2', - media_hash: 'someIpfsMediaHash2', - content: '
some html string
', - state: 'active', - }, - { - uuid: '00000000-0000-0000-0000-000000000003', - author_id: 3, - draft_id: 3, - title: 'test article 3', - slug: 'test-article-3', - summary: 'Some text', - word_count: '1000', - data_hash: 'someIpfsMediaHash3', - media_hash: 'someIpfsMediaHash3', - content: '
some html string
', - state: 'active', - public: true, - }, - { - uuid: '00000000-0000-0000-0000-000000000004', - author_id: 1, - draft_id: 4, - title: 'test article 4', - slug: 'test-article-4', - summary: 'Some text', - word_count: '1000', - data_hash: 'someIpfsMediaHash4', - media_hash: 'someIpfsMediaHash4', - content: '
some html string
', - state: 'active', - public: true, - }, - { - uuid: '00000000-0000-0000-0000-000000000005', - author_id: 7, - draft_id: 5, - title: 'test article 5 active user', - slug: 'test-article-5-active-user', - summary: 'Some text', - word_count: '1000', - data_hash: 'someIpfsMediaHash5', - media_hash: 'someIpfsMediaHash5', - content: '
some html string
', - state: 'active', - public: true, - }, - { - uuid: '00000000-0000-0000-0000-000000000004', - author_id: 1, - draft_id: 6, - title: 'test article 6', - slug: 'test-article-6', - summary: 'Some text', - word_count: 1000, - data_hash: 'someIpfsMediaHash4', - media_hash: 'someIpfsMediaHash4', - content: '
some html string
', - state: 'active', - public: true, - }, - ]) - }) + await knex(articleVersionTable).del() + await knex(articleContentTable).del() + await knex(articleTable).del() + + const rows1 = await knex(articleContentTable) + .insert([ + { + content: '
some html string
', + hash: 'hash1', + }, + ]) + .returning('id') + const contentId1 = rows1[0].id + + const rows2 = await knex(articleTable) + .insert([ + { + author_id: 1, + state: 'active', + short_hash: 'short-hash-1', + }, + { + author_id: 2, + state: 'active', + // short_hash: 'short-hash-2', + }, + { + author_id: 3, + state: 'active', + // short_hash: 'short-hash-3', + }, + { + author_id: 1, + state: 'active', + // short_hash: 'short-hash-4', + }, + { + author_id: 7, + state: 'active', + // short_hash: 'short-hash-5', + }, + { + author_id: 1, + state: 'active', + // short_hash: 'short-hash-6', + }, + ]) + .returning('id') + const articleIds = rows2.map((row) => row.id) + + // update draft article_id + await knex(draftTable).where('id', '2').update({ article_id: articleIds[1] }) + await knex(draftTable).where('id', '3').update({ article_id: articleIds[2] }) + await knex(draftTable).where('id', '4').update({ article_id: articleIds[0] }) + await knex(draftTable).where('id', '5').update({ article_id: articleIds[4] }) + await knex(draftTable).where('id', '6').update({ article_id: articleIds[3] }) + + return knex(articleVersionTable).insert([ + { + article_id: articleIds[0], + content_id: contentId1, + title: 'test article 1', + summary: 'Some text', + summary_customized: true, + word_count: '1000', + data_hash: 'someIpfsDataHash1', + media_hash: 'someIpfsMediaHash1', + tags: [], + connections: [], + access: 'public', + license: 'cc_by_nc_nd_4', + can_comment: 'true', + sensitive_by_author: 'false', + }, + { + article_id: articleIds[1], + content_id: contentId1, + title: 'test article 2', + summary: 'Some text', + summary_customized: true, + word_count: '1000', + data_hash: 'someIpfsDataHash2', + media_hash: 'someIpfsMediaHash2', + tags: [], + connections: [], + access: 'public', + license: 'cc_by_nc_nd_4', + can_comment: 'true', + sensitive_by_author: 'false', + }, + { + article_id: articleIds[2], + content_id: contentId1, + title: 'test article 3', + summary: 'Some text', + summary_customized: true, + word_count: '1000', + data_hash: 'someIpfsMediaHash3', + media_hash: 'someIpfsMediaHash3', + tags: [], + connections: [], + access: 'public', + license: 'cc_by_nc_nd_4', + can_comment: 'true', + sensitive_by_author: 'false', + }, + { + article_id: articleIds[3], + content_id: contentId1, + title: 'test article 4', + summary: 'Some text', + summary_customized: true, + word_count: '1000', + data_hash: 'someIpfsMediaHash4', + media_hash: 'someIpfsMediaHash4', + tags: [], + connections: [], + access: 'public', + license: 'cc_by_nc_nd_4', + can_comment: 'true', + sensitive_by_author: 'false', + }, + { + article_id: articleIds[4], + content_id: contentId1, + title: 'test article 5 active user', + summary: 'Some text', + summary_customized: true, + word_count: '1000', + data_hash: 'someIpfsMediaHash5', + media_hash: 'someIpfsMediaHash5', + tags: [], + connections: [], + access: 'public', + license: 'cc_by_nc_nd_4', + can_comment: 'true', + sensitive_by_author: 'false', + }, + { + article_id: articleIds[5], + content_id: contentId1, + title: 'test article 6', + summary: 'Some text', + summary_customized: true, + word_count: 1000, + data_hash: 'someIpfsMediaHash4', + media_hash: 'someIpfsMediaHash4', + tags: [], + connections: [], + access: 'public', + license: 'cc_by_nc_nd_4', + can_comment: 'true', + sensitive_by_author: 'false', + }, + ]) } diff --git a/db/seeds/08_comments.js b/db/seeds/08_comments.js index d9b7016c4..2f46f596a 100644 --- a/db/seeds/08_comments.js +++ b/db/seeds/08_comments.js @@ -15,6 +15,7 @@ exports.seed = function (knex, Promise) { content: '
Test comment 1
', target_id: 1, target_type_id: 4, + type: 'article', }, { uuid: '00000000-0000-0000-0000-000000000012', @@ -23,6 +24,7 @@ exports.seed = function (knex, Promise) { content: '
Test comment 2
', target_id: 2, target_type_id: 4, + type: 'article', }, { uuid: '00000000-0000-0000-0000-000000000013', @@ -31,6 +33,7 @@ exports.seed = function (knex, Promise) { content: '
Test comment 3
', target_id: 3, target_type_id: 4, + type: 'article', }, { uuid: '00000000-0000-0000-0000-000000000014', @@ -41,6 +44,7 @@ exports.seed = function (knex, Promise) { content: '
Test comment 4
', target_id: 3, target_type_id: 4, + type: 'article', }, { uuid: '00000000-0000-0000-0000-000000000015', @@ -51,6 +55,7 @@ exports.seed = function (knex, Promise) { reply_to: 1, target_id: 1, target_type_id: 4, + type: 'article', }, { uuid: '00000000-0000-0000-0000-000000000016', @@ -59,6 +64,7 @@ exports.seed = function (knex, Promise) { content: '
Test comment 4
', target_id: 5, target_type_id: 4, + type: 'article', }, ]) }) diff --git a/db/seeds/25_report.js b/db/seeds/25_report.js deleted file mode 100644 index f5c7ab7a1..000000000 --- a/db/seeds/25_report.js +++ /dev/null @@ -1,32 +0,0 @@ -const table = { - report: 'report', - report_asset: 'report_asset', -} - -exports.seed = async (knex) => { - // create report - await knex(table.report).insert([ - // article - { - user_id: '3', - category: '1', - article_id: '1', - description: 'description', - }, - // comment - { - user_id: '3', - category: '2', - comment_id: '1', - description: 'description', - }, - ]) - - // create report asset - await knex(table.report_asset).insert([ - { - report_id: '1', - asset_id: '11', - }, - ]) -} diff --git a/db/seeds/30_article_draft_mapping.js b/db/seeds/30_article_draft_mapping.js deleted file mode 100644 index 2ce594b8e..000000000 --- a/db/seeds/30_article_draft_mapping.js +++ /dev/null @@ -1,18 +0,0 @@ -// to migrate parts of article to draft - -const article = 'article' -const draft = 'draft' - -exports.seed = async (knex) => { - const articles = await knex(article).select() - await Promise.all( - articles.map(async (article) => { - return knex(draft).where({ id: article.draft_id }).update({ - article_id: article.id, - data_hash: article.data_hash, - media_hash: article.media_hash, - word_count: article.word_count, - }) - }) - ) -} diff --git a/db/seeds/34_topic.js b/db/seeds/34_topic.js deleted file mode 100644 index dd736b112..000000000 --- a/db/seeds/34_topic.js +++ /dev/null @@ -1,83 +0,0 @@ -exports.seed = async (knex) => { - // topics - await knex('topic').del() - await knex('topic').insert([ - { - title: 'Topic 1', - description: 'Topic 1 description', - user_id: '1', - cover: '1', - order: 0, - public: true, - }, - { - title: 'Topic 2', - description: 'Topic 2 description', - user_id: '1', - cover: '2', - order: 1, - public: true, - }, - { - title: 'Topic 3', - description: 'Topic 3 description', - user_id: '2', - cover: '3', - order: 0, - public: true, - }, - ]) - - // topic articles - await knex('article_topic').del() - await knex('article_topic').insert([ - { - topic_id: '1', - article_id: '1', - order: 0, - }, - { - topic_id: '1', - article_id: '2', - order: 1, - }, - ]) - - // topic chapters - await knex('chapter').del() - await knex('chapter').insert([ - { - title: 'Chapter 1', - description: 'Chapter 1 description', - topic_id: '1', - order: 0, - }, - { - title: 'Chapter 2', - description: 'Chapter 2 description', - topic_id: '1', - order: 1, - }, - { - title: 'Chapter 1', - description: 'Chapter 1 description', - topic_id: '2', - order: 0, - }, - ]) - - // chapter articles - await knex('article_chapter').del() - await knex('article_chapter').insert([ - { - chapter_id: '1', - article_id: '1', - order: 0, - }, - { - chapter_id: '1', - article_id: '2', - order: 1, - }, - ]) -} diff --git a/db/sql/author-tags-create-table-view.sql b/db/sql/author-tags-create-table-view.sql index 987a5fa73..c710903af 100644 --- a/db/sql/author-tags-create-table-view.sql +++ b/db/sql/author-tags-create-table-view.sql @@ -24,7 +24,6 @@ EXPLAIN (ANALYZE, BUFFERS, VERBOSE) CREATE TABLE :schema.:tablename AS WITH author_articles AS ( SELECT author_id, count(*) ::int AS num_articles, - -- (array_agg(concat(id, '-', slug, '-', media_hash) ORDER BY updated_at DESC))[1:5] AS last_5, sum(word_count) ::int AS sum_word_count, max(updated_at) AS last_at FROM public.article @@ -44,19 +43,19 @@ EXPLAIN (ANALYZE, BUFFERS, VERBOSE) CREATE TABLE :schema.:tablename AS ) ORDER BY month DESC)) -- [1:18] ) AS stats FROM ( - SELECT author_id, date_trunc('month', created_at) ::date AS month, + SELECT author_id, date_trunc('month', a.created_at) ::date AS month, MAX(created_at) AS month_last, COUNT(a.id) ::int AS num_articles, -- COUNT(DISTINCT author_id) ::int AS num_authors sum(a.word_count) ::int AS sum_word_count, (ARRAY_AGG(jsonb_build_object( - 'title', a.title, - 'path', concat(a.id, '-', a.slug, '-', a.media_hash), + 'title', avn.title, + 'path', concat(a.id), 'num_apprtors', num_apprtors, 'sum_appreciations', sum_appreciations ) ORDER BY a.created_at DESC))[1:5] AS last_5 - FROM articles_appr JOIN public.article a USING(id) - -- LEFT JOIN appreciation appr ON appr.reference_id = a.id - -- WHERE a.state IN ('active') -- NOT IN ('archived', 'banned') + FROM articles_appr + JOIN public.article a USING(id) + JOIN public.article_version_newest avn ON a.id=avn.article_id GROUP BY 1, 2 ) st GROUP BY 1 @@ -73,12 +72,14 @@ EXPLAIN (ANALYZE, BUFFERS, VERBOSE) CREATE TABLE :schema.:tablename AS COUNT(*) ::int AS num_articles, sum(a.word_count) ::int AS sum_word_count, (ARRAY_AGG(jsonb_build_object( - 'title', a.title, 'date', a.created_at ::date, - 'path', concat(a.id, '-', a.slug, '-', a.media_hash), + 'title', avn.title, 'date', a.created_at ::date, + 'path', concat(a.id), 'num_apprtors', num_apprtors, 'sum_appreciations', sum_appreciations ) ORDER BY a.created_at DESC))[1:5] AS last_5 - FROM public.article a JOIN articles_appr USING(id) + FROM public.article a + JOIN articles_appr USING(id) + JOIN public.article_version_newest avn ON a.id=avn.article_id LEFT JOIN article_tag at ON article_id=a.id -- AND a.state IN ('active') -- NOT IN ('archived', 'banned') GROUP BY 1, 2 ) t LEFT JOIN tag ON tag_id=tag.id @@ -97,8 +98,8 @@ EXPLAIN (ANALYZE, BUFFERS, VERBOSE) CREATE TABLE :schema.:tablename AS SELECT a.author_id, -- (array_agg(a.id || '-' || slug || '-' || media_hash ORDER BY num_readers DESC, sum_read_time DESC))[1:5] AS top_5 to_jsonb(array_agg(DISTINCT jsonb_build_object( - 'title', a.title, - 'path', concat(a.id, '-', slug, '-', media_hash), + 'title', avn.title, + 'path', concat(a.id), 'num_readers', num_readers, 'num_readers_w3m', num_readers_w3m, 'sum_read_time', sum_read_time, 'last_at', last_at ) -- ORDER BY num_readers DESC, sum_read_time DESC @@ -115,7 +116,9 @@ EXPLAIN (ANALYZE, BUFFERS, VERBOSE) CREATE TABLE :schema.:tablename AS FROM article_read_count WHERE user_id IS NOT NULL GROUP BY 1 - ) r JOIN public.article a ON r.article_id = a.id AND a.state IN ('active') -- NOT IN ('archived', 'banned') + ) r + JOIN public.article a ON r.article_id = a.id AND a.state IN ('active') -- NOT IN ('archived', 'banned') + JOIN public.article_version_newest avn ON a.id=avn.article_id GROUP BY 1 ) aa JOIN ( diff --git a/db/sql/create-table-search-index-article.sql b/db/sql/create-table-search-index-article.sql index dae8b10fc..7006c445b 100644 --- a/db/sql/create-table-search-index-article.sql +++ b/db/sql/create-table-search-index-article.sql @@ -4,14 +4,16 @@ DROP TABLE IF EXISTS search_index.article ; CREATE TABLE IF NOT EXISTS search_index.article AS SELECT * FROM ( - SELECT DISTINCT ON (draft.article_id) article.id, - draft.article_id, draft_id, draft.author_id, draft.title AS title_orig, draft.title AS title, -- to be processed by opencc in JS - article.slug, draft.summary, draft.content AS content_orig, - draft.content AS content, '' AS text_content_orig, '' AS text_content, '' AS text_content_converted, -- to be processed by opencc in JS - draft.created_at, article.state, draft.publish_state - FROM draft JOIN article ON article_id=article.id AND article_id IS NOT NULL - WHERE state='active' AND publish_state='published' - ORDER BY draft.article_id DESC NULLS LAST + SELECT DISTINCT ON (article_version.article_id) article.id, + article_version.article_id, article.author_id, article_version.title AS title_orig, article_version.title AS title, -- to be processed by opencc in JS + '' as slug, article_version.summary, article_content.content AS content_orig, + article_content.content AS content, '' AS text_content_orig, '' AS text_content, '' AS text_content_converted, -- to be processed by opencc in JS + article_version.created_at, article.state + FROM public.article_version + JOIN public.article ON article_id=article.id AND article_id IS NOT NULL + JOIN public.article_content ON content_id=article_content.id + WHERE state='active' + ORDER BY article_version.article_id DESC NULLS LAST ) a LEFT JOIN ( SELECT article_id, COUNT(*) ::int AS num_views, MAX(created_at) AS last_read_at diff --git a/db/sql/stale-circles-create-table-view.sql b/db/sql/stale-circles-create-table-view.sql index f74f5f9e7..628435c02 100644 --- a/db/sql/stale-circles-create-table-view.sql +++ b/db/sql/stale-circles-create-table-view.sql @@ -24,13 +24,13 @@ EXPLAIN (ANALYZE, BUFFERS, VERBOSE) CREATE TABLE :schema.:tablename AS WITH uniq_articles AS ( SELECT DISTINCT article_id FROM article_circle ), circle_articles AS ( - SELECT circle_id, a.id, a.author_id, a.title, a.slug, a.word_count, a.data_hash, a.media_hash, ac.created_at, ac.updated_at + SELECT circle_id, a.id, a.author_id, avn.title, '' as slug, avn.word_count, avn.data_hash, avn.media_hash, ac.created_at, ac.updated_at FROM article_circle ac JOIN public.article a ON ac.article_id=a.id AND a.state IN ('active') + JOIN public.article_version_newest avn ON a.id=avn.article_id ), circle_articles_stats AS ( SELECT circle_id, count(*) ::int AS num_articles, - -- (array_agg(concat(id, '-', slug, '-', media_hash) ORDER BY updated_at DESC))[1:5] AS last_5, sum(word_count) ::int AS sum_word_count, max(updated_at) AS last_at FROM circle_articles -- article_circle ac LEFT JOIN public.article a ON ac.article_id=a.id @@ -75,13 +75,14 @@ EXPLAIN (ANALYZE, BUFFERS, VERBOSE) CREATE TABLE :schema.:tablename AS COUNT(a.id) ::int AS num_articles, -- COUNT(DISTINCT author_id) ::int AS num_authors SUM(a.word_count) ::int AS sum_word_count, (ARRAY_AGG(jsonb_build_object( - 'title', a.title, - 'path', concat(a.id, '-', a.slug, '-', a.media_hash), + 'title', avn.title, + 'path', concat(a.id), 'num_apprtors', num_apprtors, 'sum_appreciations', sum_appreciations ) ORDER BY a.created_at DESC))[1:5] AS last_5 FROM articles_appr -- JOIN ( SELECT circle_id, a.* FROM circle_article JOIN public.article a ON article_id=a.id) JOIN circle_articles a USING(id) + JOIN article_version_newest avn ON a.id=avn.article_id -- LEFT JOIN appreciation appr ON appr.reference_id = a.id -- WHERE a.state IN ('active') -- NOT IN ('archived', 'banned') GROUP BY 1, 2 @@ -100,13 +101,14 @@ EXPLAIN (ANALYZE, BUFFERS, VERBOSE) CREATE TABLE :schema.:tablename AS COUNT(*) ::int AS num_articles, sum(a.word_count) ::int AS sum_word_count, (ARRAY_AGG(jsonb_build_object( - 'title', a.title, 'date', a.created_at ::date, - 'path', concat(a.id, '-', a.slug, '-', a.media_hash), + 'title', avn.title, 'date', a.created_at ::date, + 'path', concat(a.id), 'num_apprtors', num_apprtors, 'sum_appreciations', sum_appreciations ) ORDER BY a.created_at DESC))[1:5] AS last_5 FROM circle_articles a -- article_circle ac LEFT JOIN public.article a ON ac.article_id=a.id JOIN articles_appr USING(id) + JOIN article_version_newest avn ON a.id=avn.article_id LEFT JOIN article_tag at ON article_id=a.id -- AND a.state IN ('active') -- NOT IN ('archived', 'banned') GROUP BY 1, 2 ) t LEFT JOIN tag ON tag_id=tag.id @@ -124,8 +126,8 @@ EXPLAIN (ANALYZE, BUFFERS, VERBOSE) CREATE TABLE :schema.:tablename AS ORDER BY num_readers_w3m DESC, num_readers DESC, sum_read_time DESC LIMIT 5 )) AS top_5 FROM ( SELECT circle_id, to_jsonb(array_agg(DISTINCT jsonb_build_object( - 'title', a.title, - 'path', a.id || '-' || slug || '-' || media_hash, + 'title', avn.title, + 'path', a.id, 'num_readers', num_readers, 'num_readers_w3m', num_readers_w3m, 'sum_read_time', sum_read_time, 'last_at', last_at ) -- ORDER BY num_readers DESC, sum_read_time DESC @@ -145,6 +147,7 @@ EXPLAIN (ANALYZE, BUFFERS, VERBOSE) CREATE TABLE :schema.:tablename AS AND article_id IN (SELECT article_id FROM uniq_articles) GROUP BY 1 ) r JOIN circle_articles a ON r.article_id = a.id -- AND a.state IN ('active') -- NOT IN ('archived', 'banned') + JOIN article_version_newest avn ON r.article_id=avn.article_id GROUP BY 1 ) aa JOIN ( diff --git a/db/sql/stale-tags-create-table-view.sql b/db/sql/stale-tags-create-table-view.sql index 26800d468..db33fc54f 100644 --- a/db/sql/stale-tags-create-table-view.sql +++ b/db/sql/stale-tags-create-table-view.sql @@ -9,22 +9,22 @@ CREATE OR REPLACE FUNCTION pg_temp.slug(input text) RETURNS text AS $f$ $f$ LANGUAGE SQL IMMUTABLE RETURNS NULL ON NULL INPUT; CREATE OR REPLACE FUNCTION pg_temp.array_distinct( - anyarray, -- input array + anyarray, -- input array boolean DEFAULT false -- flag to ignore nulls ) RETURNS anyarray AS $f$ - SELECT array_agg(DISTINCT x) - FROM unnest($1) t(x) + SELECT array_agg(DISTINCT x) + FROM unnest($1) t(x) WHERE CASE WHEN $2 THEN x IS NOT NULL ELSE true END; $f$ LANGUAGE SQL IMMUTABLE RETURNS NULL ON NULL INPUT; CREATE OR REPLACE FUNCTION pg_temp.array_uniq_stable(anyarray) RETURNS anyarray AS $f$ SELECT array_agg(distinct_value ORDER BY first_index) -FROM +FROM (SELECT - value AS distinct_value, - min(index) AS first_index - FROM + value AS distinct_value, + min(index) AS first_index + FROM unnest($1) WITH ORDINALITY AS input(value, index) GROUP BY value @@ -238,7 +238,7 @@ WITH article_tag_stats_by_slug AS ( SELECT t.slug, author_id, -- date_trunc('month', at.created_at) ::date AS month, MAX(at.created_at) AS last_use, COUNT(DISTINCT article_id) ::int AS num_articles, -- , COUNT(*) ::int -- COUNT(DISTINCT author_id) ::int AS num_authors - (ARRAY_AGG(concat(a.id, '-', a.slug, '-', a.media_hash) ORDER BY a.created_at DESC))[1:5] AS last_5 + (ARRAY_AGG(concat(a.id) ORDER BY a.created_at DESC))[1:5] AS last_5 FROM public.article_tag at JOIN public.article a ON article_id=a.id AND a.state IN ('active') JOIN pg_temp.tag t ON tag_id=t.id -- WHERE at.created_at >= date_trunc('month', CURRENT_DATE - '18 months'::interval) @@ -297,10 +297,11 @@ SELECT slug, -- SELECT t.id, asset.path AS cover -- FROM ( SELECT id, COALESCE(t.cover, - (SELECT article.cover + (SELECT avn.cover FROM public.article_tag at LEFT JOIN public.article ON at.article_id=article.id AND article.state IN ('active') -- NOT IN ('archived', 'banned') - WHERE at.tag_id = t.id AND article.cover IS NOT NULL + LEFT JOIN public.article_version_newest avn ON at.article_id=avn.article_id + WHERE at.tag_id = t.id AND avn.cover IS NOT NULL ORDER BY at.id ASC LIMIT 1 -- find the earliest one article with cover, re-use as tagCover ) diff --git a/db/sql/stale-users-create-table-view.sql b/db/sql/stale-users-create-table-view.sql index ef9b125e8..19ccf006b 100644 --- a/db/sql/stale-users-create-table-view.sql +++ b/db/sql/stale-users-create-table-view.sql @@ -48,7 +48,7 @@ EXPLAIN (ANALYZE, BUFFERS, VERBOSE) CREATE TABLE :schema.:tablename AS MAX(created_at) AS month_last, COUNT(id) ::int AS num_articles, -- COUNT(DISTINCT author_id) ::int AS num_authors sum(word_count) ::int AS sum_word_count, - (ARRAY_AGG(concat(id, '-', slug, '-', media_hash)))[1:5] AS last_5 + (ARRAY_AGG(concat(id)))[1:5] AS last_5 FROM article -- WHERE created_at >= date_trunc('month', CURRENT_DATE - '18 months'::interval) WHERE state IN ('active') -- NOT IN ('archived') @@ -114,7 +114,7 @@ EXPLAIN (ANALYZE, BUFFERS, VERBOSE) CREATE TABLE :schema.:tablename AS COUNT(DISTINCT a.author_id) ::int AS num_commented_authors, -- SUM((SELECT COUNT(*) FROM regexp_matches(regexp_replace(c.content, '<[^\>]+>', '', 'g'), '\w', 'g'))) ::int AS sum_word_count, -- SUM((SELECT COUNT(*) FROM regexp_matches(regexp_replace(c.content, '<[^\>]+>', '', 'g'), '\w', 'g'))) ::int AS sum_word_count, - (ARRAY_AGG(concat(a.id, '-', a.slug, '-', a.media_hash, '#', + (ARRAY_AGG(concat(a.id, '#', CASE WHEN c.parent_comment_id IS NOT NULL THEN pg_temp.global_id('Comment', c.parent_comment_id ::int) || '-' ELSE '' END, pg_temp.global_id('Comment', c.id ::int) )) )[1:5] AS last_5 diff --git a/docker/Dockerfile b/docker/Dockerfile index 1e7e03cf8..fef3ee2e5 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,14 +1,16 @@ -FROM node:16 +FROM node:18 # install os level packages RUN apt-get update && apt-get -y install curl \ postgresql-client \ vim \ - wget + wget \ + python-is-python3 # install dependencies WORKDIR /var/app COPY package*.json ./ RUN npm install --force +ENV NODE_OPTIONS="--no-experimental-fetch" CMD ["npm", "run", "start"] diff --git a/jest.config.js b/jest.config.js index 22dbc48c1..a59c237d8 100644 --- a/jest.config.js +++ b/jest.config.js @@ -16,9 +16,7 @@ module.exports = { globalSetup: '/db/globalTestSetup.js', coverageDirectory: './coverage/', collectCoverage: true, - globals: { - 'ts-jest': { - diagnostics: false, - }, + transform: { + '^.+\\.tsx?$': ['ts-jest', { diagnostics: false }], }, } diff --git a/package-lock.json b/package-lock.json index 6aadc3a4c..fffeff2b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "matters-server", - "version": "4.28.0", + "version": "4.29.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "matters-server", - "version": "4.28.0", + "version": "4.29.0", "license": "Apache-2.0", "dependencies": { "@apollo/cache-control-types": "^1.0.2", @@ -19,8 +19,8 @@ "@graphql-tools/utils": "^10.0.0", "@keyv/redis": "^2.6.1", "@matters/apollo-response-cache": "^2.0.0-alpha.0", - "@matters/ipns-site-generator": "^0.1.3", - "@matters/matters-editor": "^0.2.2", + "@matters/ipns-site-generator": "^0.1.6", + "@matters/matters-editor": "^0.2.4", "@matters/passport-likecoin": "^1.0.0", "@matters/slugify": "^0.7.3", "@sendgrid/helpers": "^7.7.0", @@ -44,7 +44,7 @@ "fastest-levenshtein": "^1.0.16", "form-data": "^4.0.0", "get-stream": "^6.0.1", - "graphql": "^16.6.0", + "graphql": "^16.8.1", "graphql-constraint-directive": "^5.1.1", "graphql-middleware": "^6.1.34", "graphql-playground-middleware-express": "^1.7.23", @@ -54,6 +54,7 @@ "graphql-upload": "^13.0.0", "helmet": "^7.0.0", "ioredis": "^5.3.2", + "ioredis-mock": "^8.9.0", "ipfs-http-client": "^56.0.3", "js-base64": "^3.7.5", "jsonwebtoken": "^9.0.0", @@ -63,7 +64,7 @@ "meilisearch": "^0.32.3", "mime-types": "^2.1.35", "module-alias": "^2.2.2", - "nanoid": "^3.3.6", + "nanoid": "^3.3.7", "node-fetch": "^2.6.11", "number-precision": "^1.6.0", "oauth-1.0a": "^2.2.6", @@ -93,6 +94,7 @@ "@types/cors": "^2.8.13", "@types/debug": "^4.1.7", "@types/graphql-upload": "^8.0.12", + "@types/ioredis-mock": "^8.2.5", "@types/jest": "^29.5.1", "@types/jsonwebtoken": "^9.0.2", "@types/lodash": "^4.14.194", @@ -127,13 +129,12 @@ "nodemon": "^2.0.22", "npm-run-all": "^4.1.5", "prettier": "^2.8.8", - "redis-memory-server": "^0.6.0", "rimraf": "^5.0.1", "ts-jest": "^29.1.0", "ts-node": "^10.9.1" }, "engines": { - "node": ">=16.14 <17.0" + "node": ">=16.14 <19.0" }, "optionalDependencies": { "bufferutil": "^4.0.7", @@ -171,6 +172,7 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.0", "@jridgewell/trace-mapping": "^0.3.9" @@ -761,11 +763,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", - "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "dev": true, "dependencies": { - "@babel/highlight": "^7.22.5" + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" @@ -775,6 +779,7 @@ "version": "7.22.9", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.9.tgz", "integrity": "sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==", + "dev": true, "engines": { "node": ">=6.9.0" } @@ -783,6 +788,7 @@ "version": "7.22.9", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.9.tgz", "integrity": "sha512-G2EgeufBcYw27U4hhoIwFcgc1XU7TlXJ3mv04oOv1WCuo900U/anZSPzEqNjwdjgffkk2Gs0AN0dW1CKVLcG7w==", + "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.22.5", @@ -811,22 +817,25 @@ "node_modules/@babel/core/node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true }, "node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/generator": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.9.tgz", - "integrity": "sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "dev": true, "dependencies": { - "@babel/types": "^7.22.5", + "@babel/types": "^7.23.6", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -839,6 +848,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, "engines": { "node": ">=6.0.0" } @@ -846,12 +856,14 @@ "node_modules/@babel/generator/node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.14", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true }, "node_modules/@babel/generator/node_modules/@jridgewell/trace-mapping": { "version": "0.3.18", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dev": true, "dependencies": { "@jridgewell/resolve-uri": "3.1.0", "@jridgewell/sourcemap-codec": "1.4.14" @@ -873,6 +885,7 @@ "version": "7.22.9", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.9.tgz", "integrity": "sha512-7qYrNM6HjpnPHJbopxmb8hSPoZ0gsX8IvUS32JGVoy+pU9e5N0nLr1VjJoR6kA4d9dmGLxNYOjeB8sUDal2WMw==", + "dev": true, "dependencies": { "@babel/compat-data": "^7.22.9", "@babel/helper-validator-option": "^7.22.5", @@ -891,6 +904,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, "dependencies": { "yallist": "^3.0.2" } @@ -899,6 +913,7 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, "bin": { "semver": "bin/semver.js" } @@ -906,7 +921,8 @@ "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true }, "node_modules/@babel/helper-create-class-features-plugin": { "version": "7.22.9", @@ -941,20 +957,22 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", - "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", - "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, "dependencies": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" @@ -964,6 +982,7 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, "dependencies": { "@babel/types": "^7.22.5" }, @@ -987,6 +1006,7 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "dev": true, "dependencies": { "@babel/types": "^7.22.5" }, @@ -998,6 +1018,7 @@ "version": "7.22.9", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz", "integrity": "sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==", + "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.22.5", "@babel/helper-module-imports": "^7.22.5", @@ -1028,6 +1049,7 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true, "engines": { "node": ">=6.9.0" } @@ -1053,6 +1075,7 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, "dependencies": { "@babel/types": "^7.22.5" }, @@ -1076,6 +1099,7 @@ "version": "7.22.6", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, "dependencies": { "@babel/types": "^7.22.5" }, @@ -1084,17 +1108,19 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", - "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, "engines": { "node": ">=6.9.0" } @@ -1103,6 +1129,7 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", + "dev": true, "engines": { "node": ">=6.9.0" } @@ -1111,6 +1138,7 @@ "version": "7.22.6", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.6.tgz", "integrity": "sha512-YjDs6y/fVOYFV8hAf1rxd1QvR9wJe1pDBZ2AREKq/SDayfPzgk0PBnVuTCE5X1acEpMMNOVUqoe+OwiZGJ+OaA==", + "dev": true, "dependencies": { "@babel/template": "^7.22.5", "@babel/traverse": "^7.22.6", @@ -1121,12 +1149,13 @@ } }, "node_modules/@babel/highlight": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", - "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.22.5", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "engines": { @@ -1134,9 +1163,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.22.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.7.tgz", - "integrity": "sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", + "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", "bin": { "parser": "bin/babel-parser.js" }, @@ -1160,21 +1189,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-proposal-export-namespace-from": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", - "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-proposal-object-rest-spread": { "version": "7.20.7", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", @@ -1230,28 +1244,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", - "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-flow": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.22.5.tgz", @@ -1604,6 +1596,7 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.5.tgz", "integrity": "sha512-B4pzOXj+ONRmuaQTg05b3y/4DuFz3WcCNAXPLb2Q0GT0TrGKGxNKV4jwsXts+StaM0LQczZbOpj8o1DLPDJIiA==", + "dev": true, "dependencies": { "@babel/helper-module-transforms": "^7.22.5", "@babel/helper-plugin-utils": "^7.22.5", @@ -1746,6 +1739,7 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.5.tgz", "integrity": "sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==", + "dev": true, "dependencies": { "regenerator-runtime": "^0.13.11" }, @@ -1754,32 +1748,34 @@ } }, "node_modules/@babel/template": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", - "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.22.8", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.8.tgz", - "integrity": "sha512-y6LPR+wpM2I3qJrsheCTwhIinzkETbplIgPBbwvqPKc+uljeA5gP+3nP8irdYt1mjQaDnlIcG+dw8OjAco4GXw==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.7.tgz", + "integrity": "sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==", + "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/generator": "^7.22.7", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.22.7", - "@babel/types": "^7.22.5", - "debug": "^4.1.0", + "@babel/parser": "^7.23.6", + "@babel/types": "^7.23.6", + "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { @@ -1787,12 +1783,13 @@ } }, "node_modules/@babel/types": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", - "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -3768,9 +3765,9 @@ } }, "node_modules/@grpc/proto-loader/node_modules/protobufjs": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.3.tgz", - "integrity": "sha512-TtpvOqwB5Gdz/PQmOjgsrGH1nHjAQVCN7JG4A6r1sXRWESL5rNMAiRcBQlCAdKxZcAbstExQePYG8xof/JVRgg==", + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.6.tgz", + "integrity": "sha512-dgJaEDDL6x8ASUZ1YqWciTRrdOuYNzoOf27oHNfdyvKqHr5i0FV7FSLU+aIeFjyFgVxrpTOtQUi0BLLBymZaBw==", "hasInstallScript": true, "dependencies": { "@protobufjs/aspromise": "^1.1.2", @@ -3901,6 +3898,11 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "node_modules/@ioredis/as-callback": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@ioredis/as-callback/-/as-callback-3.0.0.tgz", + "integrity": "sha512-Kqv1rZ3WbgOrS+hgzJ5xG5WQuhvzzSTRYvNeyPMLOAM78MHSnuKI20JeJGbpuAt//LCuP0vsexZcorqW7kWhJg==" + }, "node_modules/@ioredis/commands": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", @@ -4804,6 +4806,7 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, "dependencies": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -4817,6 +4820,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, "engines": { "node": ">=6.0.0" } @@ -4825,6 +4829,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, "engines": { "node": ">=6.0.0" } @@ -4832,12 +4837,14 @@ "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -4865,62 +4872,6 @@ "node": ">= 14" } }, - "node_modules/@linaria/core": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/@linaria/core/-/core-4.2.9.tgz", - "integrity": "sha512-ELcu37VNVOT/PU0L6WDIN+aLzNFyJrqoBYT0CucGOCAmODbojUMCv8oJYRbWzA3N34w1t199dN4UFdfRWFG2rg==", - "dependencies": { - "@linaria/logger": "^4.0.0", - "@linaria/tags": "^4.3.4", - "@linaria/utils": "^4.3.3" - }, - "engines": { - "node": "^12.16.0 || >=13.7.0" - } - }, - "node_modules/@linaria/logger": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@linaria/logger/-/logger-4.0.0.tgz", - "integrity": "sha512-YnBq0JlDWMEkTOK+tMo5yEVR0f5V//6qMLToGcLhTyM9g9i+IDFn51Z+5q2hLk7RdG4NBPgbcCXYi2w4RKsPeg==", - "dependencies": { - "debug": "^4.1.1", - "picocolors": "^1.0.0" - }, - "engines": { - "node": "^12.16.0 || >=13.7.0" - } - }, - "node_modules/@linaria/tags": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/@linaria/tags/-/tags-4.3.5.tgz", - "integrity": "sha512-PgaIi8Vv89YOjc6rpKL/uPg2w4k0rAwAYxcqeXqzKqsEAste5rgB8xp1/KUOG0oAOkPd3MRL6Duj+m0ZwJ3g+g==", - "dependencies": { - "@babel/generator": "^7.20.4", - "@linaria/logger": "^4.0.0", - "@linaria/utils": "^4.3.4" - }, - "engines": { - "node": "^12.16.0 || >=13.7.0" - } - }, - "node_modules/@linaria/utils": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/@linaria/utils/-/utils-4.3.4.tgz", - "integrity": "sha512-vt6WJG54n+KANaqxOfzIIU7aSfFHEWFbnGLsgxL7nASHqO0zezrNA2y2Rrp80zSeTW+wSpbmDM4uJyC9UW1qoA==", - "dependencies": { - "@babel/core": "^7.20.2", - "@babel/plugin-proposal-export-namespace-from": "^7.18.9", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-transform-modules-commonjs": "^7.19.6", - "@babel/traverse": "^7.20.1", - "@babel/types": "^7.20.2", - "@linaria/logger": "^4.0.0", - "babel-merge": "^3.0.0" - }, - "engines": { - "node": "^12.16.0 || >=13.7.0" - } - }, "node_modules/@mapbox/node-pre-gyp": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz", @@ -5045,10 +4996,11 @@ } }, "node_modules/@matters/ipns-site-generator": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@matters/ipns-site-generator/-/ipns-site-generator-0.1.3.tgz", - "integrity": "sha512-SfgfMTYhQGJr75LhpC9K9xxd+WcyJQPKSKXS39Qctot7zLZjbmySHm7gHlLMlcgjNVENLXFS+xYHHwWIJvJzGA==", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@matters/ipns-site-generator/-/ipns-site-generator-0.1.6.tgz", + "integrity": "sha512-WcJbvRNAFfmi3mWMywSVfFJu55f5sVhGP0TpRuKtc2eTSZ9aNqewhBgXJ8jgAepT+ohGXMxDYiDRMysrvkg7rg==", "dependencies": { + "@matters/slugify": "^0.7.3", "@peculiar/webcrypto": "^1.1.6", "cheerio": "^1.0.0-rc.9", "isomorphic-fetch": "^3.0.0", @@ -5057,126 +5009,60 @@ } }, "node_modules/@matters/matters-editor": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@matters/matters-editor/-/matters-editor-0.2.2.tgz", - "integrity": "sha512-tFECZghLH4yvtFz+OM2jqdRQdVMWrfxYsHtEzoXGf8Vypz82YYUbWT62wKN6hdNS7XF94QxPk8zrJfO0vRi7KQ==", - "dependencies": { - "@tiptap/core": "2.1.0-rc.9", - "@tiptap/extension-blockquote": "2.1.0-rc.9", - "@tiptap/extension-bullet-list": "2.1.0-rc.9", - "@tiptap/extension-code": "2.1.0-rc.9", - "@tiptap/extension-code-block": "2.1.0-rc.9", - "@tiptap/extension-document": "2.1.0-rc.9", - "@tiptap/extension-gapcursor": "2.1.0-rc.9", - "@tiptap/extension-hard-break": "2.1.0-rc.9", - "@tiptap/extension-heading": "2.1.0-rc.9", - "@tiptap/extension-history": "2.1.0-rc.9", - "@tiptap/extension-horizontal-rule": "2.1.0-rc.9", - "@tiptap/extension-list-item": "2.1.0-rc.9", - "@tiptap/extension-ordered-list": "2.1.0-rc.9", - "@tiptap/extension-paragraph": "2.1.0-rc.9", - "@tiptap/extension-placeholder": "2.1.0-rc.9", - "@tiptap/extension-strike": "2.1.0-rc.9", - "@tiptap/extension-text": "2.1.0-rc.9", - "@tiptap/pm": "2.1.0-rc.9", - "@tiptap/react": "2.1.0-rc.9", - "@tiptap/suggestion": "2.1.0-rc.9", - "hast-util-to-html": "^8.0.4", - "linkifyjs": "^4.1.1", - "mdast-util-gfm-strikethrough": "^1.0.3", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@matters/matters-editor/-/matters-editor-0.2.4.tgz", + "integrity": "sha512-dmGVnmdgpSekfxEyj1Or5yoNEzky395ZMPFUckyVYILKGvlWFuGnbAWTzEc5vFUtHcFPKqfPZt0r/ocnWyKevg==", + "dependencies": { + "@tiptap/core": "2.2.4", + "@tiptap/extension-blockquote": "2.2.4", + "@tiptap/extension-bullet-list": "2.2.4", + "@tiptap/extension-code": "2.2.4", + "@tiptap/extension-code-block": "2.2.4", + "@tiptap/extension-document": "2.2.4", + "@tiptap/extension-gapcursor": "2.2.4", + "@tiptap/extension-hard-break": "2.2.4", + "@tiptap/extension-heading": "2.2.4", + "@tiptap/extension-history": "2.2.4", + "@tiptap/extension-horizontal-rule": "2.2.4", + "@tiptap/extension-list-item": "2.2.4", + "@tiptap/extension-ordered-list": "2.2.4", + "@tiptap/extension-paragraph": "2.2.4", + "@tiptap/extension-placeholder": "2.2.4", + "@tiptap/extension-strike": "2.2.4", + "@tiptap/extension-text": "2.2.4", + "@tiptap/pm": "2.2.4", + "@tiptap/react": "2.2.4", + "@tiptap/suggestion": "2.2.4", + "hast-util-to-html": "^9.0.0", + "linkifyjs": "^4.1.3", + "mdast-util-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", - "rehype-format": "^4.0.1", - "rehype-parse": "^8.0.4", - "rehype-raw": "^6.1.1", - "rehype-remark": "^9.1.2", - "rehype-rewrite": "^3.0.6", - "rehype-sanitize": "^5.0.1", - "rehype-stringify": "^9.0.3", - "remark-breaks": "^3.0.3", - "remark-directive": "^2.0.1", + "rehype-external-links": "^3.0.0", + "rehype-format": "^5.0.0", + "rehype-parse": "^9.0.0", + "rehype-raw": "^7.0.0", + "rehype-remark": "^10.0.0", + "rehype-rewrite": "^4.0.2", + "rehype-sanitize": "^6.0.0", + "rehype-stringify": "^10.0.0", + "remark-breaks": "^4.0.0", + "remark-directive": "^3.0.0", "remark-directive-rehype": "^0.4.2", - "remark-parse": "^10.0.2", - "remark-rehype": "^10.1.0", - "remark-stringify": "^10.0.3", - "unified": "^10.1.2", - "zeed-dom": "^0.10.5" + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.4", + "zeed-dom": "^0.12.10" }, "engines": { - "node": ">=16.14 <17.0" + "node": ">=16.14 <19.0" }, "peerDependencies": { "react": ">=17.0.0", "react-dom": ">=17.0.0" } }, - "node_modules/@matters/matters-editor/node_modules/micromark-util-chunked": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.0.tgz", - "integrity": "sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/@matters/matters-editor/node_modules/micromark-util-combine-extensions": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.0.tgz", - "integrity": "sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-chunked": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/@matters/matters-editor/node_modules/micromark-util-symbol": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", - "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/@matters/matters-editor/node_modules/micromark-util-types": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.0.tgz", - "integrity": "sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, "node_modules/@matters/passport-likecoin": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@matters/passport-likecoin/-/passport-likecoin-1.0.0.tgz", @@ -5441,53 +5327,9 @@ "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" }, "node_modules/@remirror/core-constants": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-2.0.1.tgz", - "integrity": "sha512-ZR4aihtnnT9lMbhh5DEbsriJRlukRXmLZe7HmM+6ufJNNUDoazc75UX26xbgQlNUqgAqMcUdGFAnPc1JwgAdLQ==", - "dependencies": { - "@babel/runtime": "^7.21.0" - } - }, - "node_modules/@remirror/core-helpers": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@remirror/core-helpers/-/core-helpers-2.0.3.tgz", - "integrity": "sha512-LqIPF4stGG69l9qu/FFicv9d9B+YaItzgDMC5A0CEvDQfKkGD3BfabLmfpnuWbsc06oKGdTduilgWcALLZoYLg==", - "dependencies": { - "@babel/runtime": "^7.21.0", - "@linaria/core": "4.2.9", - "@remirror/core-constants": "^2.0.1", - "@remirror/types": "^1.0.1", - "@types/object.omit": "^3.0.0", - "@types/object.pick": "^1.3.2", - "@types/throttle-debounce": "^2.1.0", - "case-anything": "^2.1.10", - "dash-get": "^1.0.2", - "deepmerge": "^4.3.1", - "fast-deep-equal": "^3.1.3", - "make-error": "^1.3.6", - "object.omit": "^3.0.0", - "object.pick": "^1.3.0", - "throttle-debounce": "^3.0.1" - } - }, - "node_modules/@remirror/types": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@remirror/types/-/types-1.0.1.tgz", - "integrity": "sha512-VlZQxwGnt1jtQ18D6JqdIF+uFZo525WEqrfp9BOc3COPpK4+AWCgdnAWL+ho6imWcoINlGjR/+3b6y5C1vBVEA==", - "dependencies": { - "type-fest": "^2.19.0" - } - }, - "node_modules/@remirror/types/node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-2.0.2.tgz", + "integrity": "sha512-dyHY+sMF0ihPus3O27ODd4+agdHMEmuRdyiZJ2CCWjPV5UFmn17ZbElvk6WOGVE4rdCJKZQCrPV2BcikOMLUGQ==" }, "node_modules/@repeaterjs/repeater": { "version": "3.0.4", @@ -5767,9 +5609,9 @@ } }, "node_modules/@tiptap/core": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.1.0-rc.9.tgz", - "integrity": "sha512-wY4kq3V4cge/c37K5Fh9vGdFJsq/SISrWCfL3A+m1BlQLXYViVyEx5IV3setgcTdh3q+cTUapBRRTDpmUpj4KQ==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.2.4.tgz", + "integrity": "sha512-cRrI8IlLIhCE1hacBQzXIC8dsRvGq6a4lYWQK/BaHuZg21CG7szp3Vd8Ix+ra1f5v0xPOT+Hy+QFNQooRMKMCw==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -5779,9 +5621,9 @@ } }, "node_modules/@tiptap/extension-blockquote": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-2.1.0-rc.9.tgz", - "integrity": "sha512-gQPOY8ZYlA2NwKCv5s4kByON1x7ayXZVGHo7+7xHTKJyTd4J+s3Suv7+IJZJFbkkqRjNlUPTnFcB0ZnUQTvsWg==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-2.2.4.tgz", + "integrity": "sha512-FrfPnn0VgVrUwWLwja1afX99JGLp6PE9ThVcmri+tLwUZQvTTVcCvHoCdOakav3/nge1+aV4iE3tQdyq1tWI9Q==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -5791,9 +5633,9 @@ } }, "node_modules/@tiptap/extension-bubble-menu": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.1.0-rc.9.tgz", - "integrity": "sha512-XJ0CO5XVoX/8YXl3RSDy+FtRfzKXVqB48/gBij6a2+vVenJLtSNvJeHVB/1pLJGJoiYOGj2UrbS1MDhEsaqsKQ==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.2.4.tgz", + "integrity": "sha512-Nx1fS9jcFlhxaTDYlnayz2UulhK6CMaePc36+7PQIVI+u20RhgTCRNr25zKNemvsiM0RPZZVUjlHkxC0l5as1Q==", "dependencies": { "tippy.js": "^6.3.7" }, @@ -5807,9 +5649,9 @@ } }, "node_modules/@tiptap/extension-bullet-list": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.1.0-rc.9.tgz", - "integrity": "sha512-HtrCBiOmR26639zLNa4uL5fjv+cyFcfaR4hTJvkKVxjqIZ1bfr6zufFxe4awfzNDPl6iGyLlSu5+rBk5WrVAIg==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.2.4.tgz", + "integrity": "sha512-z/MPmW8bhRougMuorl6MAQBXeK4rhlP+jBWlNwT+CT8h5IkXqPnDbM1sZeagp2nYfVV6Yc4RWpzimqHHtGnYTA==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -5819,9 +5661,9 @@ } }, "node_modules/@tiptap/extension-code": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.1.0-rc.9.tgz", - "integrity": "sha512-ZSyFeCfn1O1sZQPu9/76MdSlNuX28uOtjbA+8POZElEAf3UXKVpYXX/yy+3kt6x6qT7ei50FbueBS7ocs31vHQ==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.2.4.tgz", + "integrity": "sha512-JB4SJ2mUU/9qXFUf+K5K9szvovnN9AIcCb0f0UlcVBuddKHSqCl3wO3QJgYt44BfQTLMNuyzr+zVqfFd6BNt/g==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -5831,9 +5673,9 @@ } }, "node_modules/@tiptap/extension-code-block": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-2.1.0-rc.9.tgz", - "integrity": "sha512-lkyGGUYEQnBDq/ceGDNBq69gNfROdA/WrvnoxNhEUVRwx0/hv8Enm+8mVbD98cm/VUbwyEb94wsea6aJoltXGA==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-2.2.4.tgz", + "integrity": "sha512-h6WV9TmaBEZmvqe1ezMR83DhCPUap6P2mSR5pwVk0WVq6rvZjfgU0iF3EetBJOeDgPlz7cNe2NMDfVb1nGTM/g==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -5844,9 +5686,9 @@ } }, "node_modules/@tiptap/extension-document": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.1.0-rc.9.tgz", - "integrity": "sha512-vvAB2QvFsxBAkquLX0ijRLaYN0oZ8tXuEGEHZ70IDwiNjsAa1oHuXJUp+l/fBHSWvuG791/g02WIqGNjtdAOEw==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.2.4.tgz", + "integrity": "sha512-z+05xGK0OFoXV1GL+/8bzcZuWMdMA3+EKwk5c+iziG60VZcvGTF7jBRsZidlu9Oaj0cDwWHCeeo6L9SgSh6i2A==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -5856,9 +5698,9 @@ } }, "node_modules/@tiptap/extension-floating-menu": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-2.1.0-rc.9.tgz", - "integrity": "sha512-+B813ZcYTH1G1TJnOJZgPCG/CENqUIke/9rThbw29vWiEO5jpag30bQjw9AZ08s5dFFV9xOz0rcx7ub4834X6g==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-2.2.4.tgz", + "integrity": "sha512-U25l7PEzOmlAPugNRl8t8lqyhQZS6W/+3f92+FdwW9qXju3i62iX/3OGCC3Gv+vybmQ4fbZmMjvl+VDfenNi3A==", "dependencies": { "tippy.js": "^6.3.7" }, @@ -5872,9 +5714,9 @@ } }, "node_modules/@tiptap/extension-gapcursor": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.1.0-rc.9.tgz", - "integrity": "sha512-hwmoX6Aig3gK0APmzrYQR4YF1JTJ7vXVpVcTPta5rGHbWbEpepJZC3PyYzg1ay8tJAaZi5aTJE8tvL1oxquPhg==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.2.4.tgz", + "integrity": "sha512-Y6htT/RDSqkQ1UwG2Ia+rNVRvxrKPOs3RbqKHPaWr3vbFWwhHyKhMCvi/FqfI3d5pViVHOZQ7jhb5hT/a0BmNw==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -5885,9 +5727,9 @@ } }, "node_modules/@tiptap/extension-hard-break": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.1.0-rc.9.tgz", - "integrity": "sha512-9eWs6JfSp8SmKiiVmHwzbJaJudIcw3sF3svPb7Nv0NPLZOGeFdAyk9SC0J9U5xo3SmVJPStGt22uCJMofXwltw==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.2.4.tgz", + "integrity": "sha512-FPvS57GcqHIeLbPKGJa3gnH30Xw+YB1PXXnAWG2MpnMtc2Vtj1l5xaYYBZB+ADdXLAlU0YMbKhFLQO4+pg1Isg==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -5897,9 +5739,9 @@ } }, "node_modules/@tiptap/extension-heading": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-2.1.0-rc.9.tgz", - "integrity": "sha512-c7m9GbzikwfL02mV4sDbzqfeOczKt/XYGt7xmacJrXmScGAl7nrdeiaWIXRD8M3u6PNn+TwFtURwPTZLKCD8cQ==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-2.2.4.tgz", + "integrity": "sha512-gkq7Ns2FcrOCRq7Q+VRYt5saMt2R9g4REAtWy/jEevJ5UV5vA2AiGnYDmxwAkHutoYU0sAUkjqx37wE0wpamNw==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -5909,9 +5751,9 @@ } }, "node_modules/@tiptap/extension-history": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.1.0-rc.9.tgz", - "integrity": "sha512-38TLb7Q6d2EFudzcD4MamRldbp0PfBhzrySU1RA+fseuiXKKLHj7828tcDiesEjUOlW5AO2YGgmO7ljGr0lo9A==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.2.4.tgz", + "integrity": "sha512-FDM32XYF5NU4mzh+fJ8w2CyUqv0l2Nl15sd6fOhQkVxSj8t57z+DUXc9ZR3zkH+1RAagYJo/2Gu3e99KpMr0tg==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -5922,9 +5764,9 @@ } }, "node_modules/@tiptap/extension-horizontal-rule": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.1.0-rc.9.tgz", - "integrity": "sha512-WjnEW0yGL2uqiD7iYxsObFjVspWhDvr4/iVk+TpeFPhCvX8ZeXdy6j05NEa9QI/esdJ0t3e3g9XZkAMh7U1Ccg==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.2.4.tgz", + "integrity": "sha512-iCRHjFQQHApWg3R4fkKkJQhWEOdu1Fdc4YEAukdOXPSg3fg36IwjvsMXjt9SYBtVZ+iio3rORCZGXyMvgCH9uw==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -5935,9 +5777,9 @@ } }, "node_modules/@tiptap/extension-list-item": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.1.0-rc.9.tgz", - "integrity": "sha512-vICjBG122blY92JXmjkg0SpTaWNxsybNDwXTxvMOMHMamStHb3bwbuhT0eYBFbLRkyNghYppi2CEjiNCYPHfNA==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.2.4.tgz", + "integrity": "sha512-lPLKGKsHpM9ClUa8n7GEUn8pG6HCYU0vFruIy3l2t6jZdHkrgBnYtVGMZ13K8UDnj/hlAlccxku0D0P4mA1Vrg==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -5947,9 +5789,9 @@ } }, "node_modules/@tiptap/extension-ordered-list": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.1.0-rc.9.tgz", - "integrity": "sha512-lpSBeGvlRxUwqoEUtjbZBt2XfV6aSIPBcXapYNNbz6aZrjzBuq9PnHZ6cRp7r13b/5uJRuZY6zPsdWlwo0DjRg==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.2.4.tgz", + "integrity": "sha512-TpFy140O9Af1JciXt+xwqYUXxcJ6YG8zi/B5UDJujp+FH5sCmlYYBBnWxiFMhVaj6yEmA2eafu1qUkic/1X5Aw==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -5959,9 +5801,9 @@ } }, "node_modules/@tiptap/extension-paragraph": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.1.0-rc.9.tgz", - "integrity": "sha512-I6owNBPw5slpy4PjBxuxb3/OBVNUgYHk7ncNTnz97HWbzpvu7hedb6k82ioVY5w+X6gHlSBnixJ6lEjwWwA4Mw==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.2.4.tgz", + "integrity": "sha512-m1KwyvTNJxsq7StbspbcOhxO4Wk4YpElDbqOouWi+H4c8azdpI5Pn96ZqhFeE9bSyjByg6OcB/wqoJsLbeFWdQ==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -5971,9 +5813,9 @@ } }, "node_modules/@tiptap/extension-placeholder": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-2.1.0-rc.9.tgz", - "integrity": "sha512-/CyjlpJpzmD0Q7zhmK8m71KAq6s5O7P+x8HMrGLkvR3igtuRLMxqrVPPivw7RqMJuha0OqRhAnhnOu+O8d/uSg==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-2.2.4.tgz", + "integrity": "sha512-UL4Fn9T33SoS7vdI3NnSxBJVeGUIgCIutgXZZ5J8CkcRoDIeS78z492z+6J+qGctHwTd0xUL5NzNJI82HfiTdg==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -5984,9 +5826,9 @@ } }, "node_modules/@tiptap/extension-strike": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-2.1.0-rc.9.tgz", - "integrity": "sha512-BzsciaqtvgP3RXVGtFmzH/SFzBFz9HVLPEwPrl3w8h0XDBbDYBISvuENkEvsVkv6f7Gp+PrHpzWuZiAArgIvug==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-2.2.4.tgz", + "integrity": "sha512-/a2EwQgA+PpG17V2tVRspcrIY0SN3blwcgM7lxdW4aucGkqSKnf7+91dkhQEwCZ//o8kv9mBCyRoCUcGy6S5Xg==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -5996,9 +5838,9 @@ } }, "node_modules/@tiptap/extension-text": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.1.0-rc.9.tgz", - "integrity": "sha512-Tgnnq9M/9bLhI/4NMGnxTp4Li/gLc4h0wNHeE/uIyfToKsle2MfolRFz86BfVFDMFB2fFPVEVBHaDgq2at1e8A==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.2.4.tgz", + "integrity": "sha512-NlKHMPnRJXB+0AGtDlU0P2Pg+SdesA2lMMd7JzDUgJgL7pX2jOb8eUqSeOjFKuSzFSqYfH6C3o6mQiNhuQMv+g==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -6008,28 +5850,28 @@ } }, "node_modules/@tiptap/pm": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.1.0-rc.9.tgz", - "integrity": "sha512-ORtAcmEQ2MKmzVgi8Et2iRpNcaIJnbbiiKSD2PsCJAU2pt2t5vC3+417Gce8wS4Vxo15RmtiT6ib481roAT0OA==", - "dependencies": { - "prosemirror-changeset": "^2.2.0", - "prosemirror-collab": "^1.3.0", - "prosemirror-commands": "^1.3.1", - "prosemirror-dropcursor": "^1.5.0", - "prosemirror-gapcursor": "^1.3.1", - "prosemirror-history": "^1.3.0", - "prosemirror-inputrules": "^1.2.0", - "prosemirror-keymap": "^1.2.0", - "prosemirror-markdown": "^1.10.1", - "prosemirror-menu": "^1.2.1", - "prosemirror-model": "^1.18.1", - "prosemirror-schema-basic": "^1.2.0", - "prosemirror-schema-list": "^1.2.2", - "prosemirror-state": "^1.4.1", - "prosemirror-tables": "^1.3.0", - "prosemirror-trailing-node": "^2.0.2", - "prosemirror-transform": "^1.7.0", - "prosemirror-view": "^1.28.2" + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.2.4.tgz", + "integrity": "sha512-Po0klR165zgtinhVp1nwMubjyKx6gAY9kH3IzcniYLCkqhPgiqnAcCr61TBpp4hfK8YURBS4ihvCB1dyfCyY8A==", + "dependencies": { + "prosemirror-changeset": "^2.2.1", + "prosemirror-collab": "^1.3.1", + "prosemirror-commands": "^1.5.2", + "prosemirror-dropcursor": "^1.8.1", + "prosemirror-gapcursor": "^1.3.2", + "prosemirror-history": "^1.3.2", + "prosemirror-inputrules": "^1.3.0", + "prosemirror-keymap": "^1.2.2", + "prosemirror-markdown": "^1.12.0", + "prosemirror-menu": "^1.2.4", + "prosemirror-model": "^1.19.4", + "prosemirror-schema-basic": "^1.2.2", + "prosemirror-schema-list": "^1.3.0", + "prosemirror-state": "^1.4.3", + "prosemirror-tables": "^1.3.5", + "prosemirror-trailing-node": "^2.0.7", + "prosemirror-transform": "^1.8.0", + "prosemirror-view": "^1.32.7" }, "funding": { "type": "github", @@ -6037,12 +5879,12 @@ } }, "node_modules/@tiptap/react": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-2.1.0-rc.9.tgz", - "integrity": "sha512-VyThuIA/bhxxvcSUitgL4gBbflOZrtKCEeLqjjtd/wrxKkGEKIMlxzpVBTG3zMsqSc7MIiu3RdetMcOw1pNquQ==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-2.2.4.tgz", + "integrity": "sha512-HkYmMZWcETPZn3KpzdDg/ns2TKeFh54TvtCEInA4ljYtWGLoZc/A+KaiEtMIgVs+Mo1XwrhuoNGjL9c0OK2HJw==", "dependencies": { - "@tiptap/extension-bubble-menu": "^2.1.0-rc.9", - "@tiptap/extension-floating-menu": "^2.1.0-rc.9" + "@tiptap/extension-bubble-menu": "^2.2.4", + "@tiptap/extension-floating-menu": "^2.2.4" }, "funding": { "type": "github", @@ -6056,9 +5898,9 @@ } }, "node_modules/@tiptap/suggestion": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/suggestion/-/suggestion-2.1.0-rc.9.tgz", - "integrity": "sha512-GN0ooEMzkUe/+iid9wpksZMRCcgDz8vImfZ7PACsu9hkhhgNd2oDYBxljWH4o89nXnOib/OCeIh88b32H+Xy2A==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/suggestion/-/suggestion-2.2.4.tgz", + "integrity": "sha512-g6HHsKM6K3asW+ZlwMYyLCRqCRaswoliZOQofY4iZt5ru5HNTSzm3YW4XSyW5RGXJIuc319yyrOFgtJ3Fyu5rQ==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -6240,11 +6082,6 @@ "@types/send": "*" } }, - "node_modules/@types/extend": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/extend/-/extend-3.0.1.tgz", - "integrity": "sha512-R1g/VyKFFI2HLC1QGAeTtCBWCo6n75l41OnsVYNbmKG+kempOESaodf6BeJyUM3Q0rKa/NQcTHbB2+66lNnxLw==" - }, "node_modules/@types/glob": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz", @@ -6305,6 +6142,16 @@ "integrity": "sha512-e+2rjEwK6KDaNOm5Aa9wNGgyS9oSZU/4pfSMMPYNOfjvFI0WVXm29+ITRFr6aKDvvKo7uU1jV68MW4ScsfDi7Q==", "dev": true }, + "node_modules/@types/ioredis-mock": { + "version": "8.2.5", + "resolved": "https://registry.npmjs.org/@types/ioredis-mock/-/ioredis-mock-8.2.5.tgz", + "integrity": "sha512-cZyuwC9LGtg7s5G9/w6rpy3IOZ6F/hFR0pQlWYZESMo1xQUYbDpa6haqB4grTePjsGzcB/YLBFCjqRunK5wieg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "ioredis": ">=5" + } + }, "node_modules/@types/is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@types/is-stream/-/is-stream-1.1.0.tgz", @@ -6437,9 +6284,9 @@ } }, "node_modules/@types/mdast": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz", - "integrity": "sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.3.tgz", + "integrity": "sha512-LsjtqsyF+d2/yFOYaN22dHZI1Cpwkrj+g06G8+qtUKlhovPW89YhqSnfKtMbkgmEtYpH2gydRNULd6y8mciAFg==", "dependencies": { "@types/unist": "*" } @@ -6530,16 +6377,6 @@ "@types/express": "*" } }, - "node_modules/@types/object.omit": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/object.omit/-/object.omit-3.0.0.tgz", - "integrity": "sha512-I27IoPpH250TUzc9FzXd0P1BV/BMJuzqD3jOz98ehf9dQqGkxlq+hO1bIqZGWqCg5bVOy0g4AUVJtnxe0klDmw==" - }, - "node_modules/@types/object.pick": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/object.pick/-/object.pick-1.3.2.tgz", - "integrity": "sha512-sn7L+qQ6RLPdXRoiaE7bZ/Ek+o4uICma/lBFPyJEKDTPTBP1W8u0c4baj3EiS4DiqLs+Hk+KUGvMVJtAw3ePJg==" - }, "node_modules/@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -6547,11 +6384,6 @@ "dev": true, "optional": true }, - "node_modules/@types/parse5": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-6.0.3.tgz", - "integrity": "sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==" - }, "node_modules/@types/passport": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.12.tgz", @@ -6641,11 +6473,6 @@ "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "dev": true }, - "node_modules/@types/throttle-debounce": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/throttle-debounce/-/throttle-debounce-2.1.0.tgz", - "integrity": "sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ==" - }, "node_modules/@types/triple-beam": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.2.tgz", @@ -6692,16 +6519,6 @@ "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", "dev": true }, - "node_modules/@types/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==", - "dev": true, - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.59.7", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.7.tgz", @@ -6953,6 +6770,11 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" + }, "node_modules/@whatwg-node/events": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/@whatwg-node/events/-/events-0.0.3.tgz", @@ -7169,6 +6991,7 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, "dependencies": { "color-convert": "^1.9.0" }, @@ -7219,18 +7042,18 @@ } }, "node_modules/are-we-there-yet": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", + "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", "dependencies": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" } }, "node_modules/are-we-there-yet/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -7582,26 +7405,6 @@ "node": ">=8" } }, - "node_modules/babel-merge": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/babel-merge/-/babel-merge-3.0.0.tgz", - "integrity": "sha512-eBOBtHnzt9xvnjpYNI5HmaPp/b2vMveE5XggzqHnQeHJ8mFIBrBv6WZEVIj5jJ2uwTItkqKo9gWzEEcBxEq0yw==", - "dependencies": { - "deepmerge": "^2.2.1", - "object.omit": "^3.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/babel-merge/node_modules/deepmerge": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", - "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/babel-plugin-istanbul": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", @@ -7953,6 +7756,7 @@ "version": "4.21.9", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz", "integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==", + "dev": true, "funding": [ { "type": "opencollective", @@ -8011,15 +7815,6 @@ "isarray": "^1.0.0" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -8150,6 +7945,7 @@ "version": "1.0.30001517", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001517.tgz", "integrity": "sha512-Vdhm5S11DaFVLlyiKu4hiUTkpZu+y1KA/rZZqVQfOD5YdDT/eQKlkt7NaE0WGOFgX32diqt9MiP9CAiFeRklaA==", + "dev": true, "funding": [ { "type": "opencollective", @@ -8176,17 +7972,6 @@ "upper-case-first": "^2.0.2" } }, - "node_modules/case-anything": { - "version": "2.1.13", - "resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.13.tgz", - "integrity": "sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==", - "engines": { - "node": ">=12.13" - }, - "funding": { - "url": "https://github.com/sponsors/mesqueeb" - } - }, "node_modules/catharsis": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", @@ -8219,6 +8004,7 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -8353,17 +8139,6 @@ "url": "https://github.com/sponsors/fb55" } }, - "node_modules/cheerio/node_modules/parse5": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", - "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", - "dependencies": { - "entities": "^4.4.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -8674,7 +8449,7 @@ "node_modules/code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==", "engines": { "node": ">=0.10.0" } @@ -8840,12 +8615,6 @@ "node": ">=4.0.0" } }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", - "dev": true - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -8931,9 +8700,9 @@ "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, "node_modules/cors": { "version": "2.8.5", @@ -9062,9 +8831,19 @@ } }, "node_modules/css-selector-parser": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-1.4.1.tgz", - "integrity": "sha512-HYPSb7y/Z7BNDCOrakL4raGO2zltZkbeXyAd6Tg9obzix6QhzxCotdBl6VT0Dv4vZfJGVz3WL/xaEI9Ly3ul0g==" + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-3.0.5.tgz", + "integrity": "sha512-3itoDFbKUNx1eKmVpYMFyqKX04Ww9osZ+dLgrk6GEv6KMVeXUhUnp4I5X+evw+u3ZxVU6RFXSSRxlTeMh8bA+g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ] }, "node_modules/css-what": { "version": "6.1.0", @@ -9125,11 +8904,6 @@ "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==" }, - "node_modules/dash-get": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/dash-get/-/dash-get-1.0.2.tgz", - "integrity": "sha512-4FbVrHDwfOASx7uQVxeiCTo7ggSdYZbqs8lH+WU6ViypPlDbe9y6IP5VVUDQBv9DcnyaiPT5XT0UWHgJ64zLeQ==" - }, "node_modules/dataloader": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.2.2.tgz", @@ -9327,7 +9101,7 @@ "node_modules/detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", "bin": { "detect-libc": "bin/detect-libc.js" }, @@ -9345,9 +9119,9 @@ } }, "node_modules/devlop": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.0.0.tgz", - "integrity": "sha512-DNY7Ok32YUNiFjTw9sNVqUET5c2/cqbOdDxnsI6MkfQOvMcAULqPVqABm/An9IGVRP4ulHEvpo3/w2Potw3cfQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", "dependencies": { "dequal": "^2.0.0" }, @@ -9552,7 +9326,8 @@ "node_modules/electron-to-chromium": { "version": "1.4.477", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.477.tgz", - "integrity": "sha512-shUVy6Eawp33dFBFIoYbIwLHrX0IZ857AlH9ug2o4rvbWmpaCUdBpQ5Zw39HRrfzAFm4APJE9V+E2A/WB0YqJw==" + "integrity": "sha512-shUVy6Eawp33dFBFIoYbIwLHrX0IZ857AlH9ug2o4rvbWmpaCUdBpQ5Zw39HRrfzAFm4APJE9V+E2A/WB0YqJw==", + "dev": true }, "node_modules/emittery": { "version": "0.13.1", @@ -10749,41 +10524,6 @@ "url": "https://github.com/sponsors/jaydenseric" } }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, - "node_modules/extract-zip/node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/fast-decode-uri-component": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", @@ -10903,20 +10643,34 @@ "integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==", "dev": true }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/fecha": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" }, + "node_modules/fengari": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/fengari/-/fengari-0.1.4.tgz", + "integrity": "sha512-6ujqUuiIYmcgkGz8MGAdERU57EIluGGPSUgGPTsco657EHa+srq0S3/YUl/r9kx1+D+d4rGfYObd+m8K22gB1g==", + "dependencies": { + "readline-sync": "^1.4.9", + "sprintf-js": "^1.1.1", + "tmp": "^0.0.33" + } + }, + "node_modules/fengari-interop": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/fengari-interop/-/fengari-interop-0.1.3.tgz", + "integrity": "sha512-EtZ+oTu3kEwVJnoymFPBVLIbQcCoy9uWCVnMA6h3M/RqHkUBsLYp29+RRHf9rKr6GwjubWREU1O7RretFIXjHw==", + "peerDependencies": { + "fengari": "^0.1.0" + } + }, + "node_modules/fengari/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" + }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -10994,23 +10748,6 @@ "node": ">= 0.8" } }, - "node_modules/find-cache-dir": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", - "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", - "dev": true, - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/avajs/find-cache-dir?sponsor=1" - } - }, "node_modules/find-node-modules": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/find-node-modules/-/find-node-modules-2.1.2.tgz", @@ -11021,12 +10758,6 @@ "merge": "^2.1.0" } }, - "node_modules/find-package-json": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/find-package-json/-/find-package-json-1.2.0.tgz", - "integrity": "sha512-+SOGcLGYDJHtyqHd87ysBhmaeQ95oWspDKnMXBrnQ9Eq4OkLNqejgoaD8xVWu6GPa0B6roa6KinCMEMcVeqONw==", - "dev": true - }, "node_modules/find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", @@ -11101,9 +10832,9 @@ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" }, "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", + "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==", "funding": [ { "type": "individual", @@ -11294,7 +11025,7 @@ "node_modules/gauge": { "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "integrity": "sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==", "dependencies": { "aproba": "^1.0.3", "console-control-strings": "^1.0.0", @@ -11309,7 +11040,7 @@ "node_modules/gauge/node_modules/ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", "engines": { "node": ">=0.10.0" } @@ -11317,7 +11048,7 @@ "node_modules/gauge/node_modules/is-fullwidth-code-point": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", "dependencies": { "number-is-nan": "^1.0.0" }, @@ -11328,7 +11059,7 @@ "node_modules/gauge/node_modules/string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", "dependencies": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -11341,7 +11072,7 @@ "node_modules/gauge/node_modules/strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", "dependencies": { "ansi-regex": "^2.0.0" }, @@ -11390,6 +11121,7 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, "engines": { "node": ">=6.9.0" } @@ -11562,6 +11294,7 @@ "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, "engines": { "node": ">=4" } @@ -11720,9 +11453,9 @@ "dev": true }, "node_modules/graphql": { - "version": "16.6.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.6.0.tgz", - "integrity": "sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==", + "version": "16.8.1", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz", + "integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==", "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } @@ -12104,6 +11837,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true, "engines": { "node": ">=4" } @@ -12162,28 +11896,63 @@ "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" }, "node_modules/hast-util-embedded": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-embedded/-/hast-util-embedded-3.0.0.tgz", + "integrity": "sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA==", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-is-element": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-embedded/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/hast-util-from-html": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/hast-util-embedded/-/hast-util-embedded-2.0.1.tgz", - "integrity": "sha512-QUdSOP1/o+/TxXtpPFXR2mUg2P+ySrmlX7QjwHZCXqMFyYk7YmcGSvqRW+4XgXAoHifdE1t2PwFaQK33TqVjSw==", + "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.1.tgz", + "integrity": "sha512-RXQBLMl9kjKVNkJTIO6bZyb2n+cUH8LFaSSzo82jiLT6Tfc+Pt7VQCS+/h3YwG4jaNE2TA2sdJisGWR+aJrp0g==", "dependencies": { - "hast-util-is-element": "^2.0.0" + "@types/hast": "^3.0.0", + "devlop": "^1.1.0", + "hast-util-from-parse5": "^8.0.0", + "parse5": "^7.0.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-from-html/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/hast-util-from-parse5": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-7.1.2.tgz", - "integrity": "sha512-Nz7FfPBuljzsN3tCQ4kCBKqdNhQE2l0Tn+X1ubgKBPRoiDIu1mL08Cfw4k7q71+Duyaw7DXDN+VTAp4Vh3oCOw==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.1.tgz", + "integrity": "sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ==", "dependencies": { - "@types/hast": "^2.0.0", - "@types/unist": "^2.0.0", - "hastscript": "^7.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^8.0.0", "property-information": "^6.0.0", - "vfile": "^5.0.0", - "vfile-location": "^4.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" }, "funding": { @@ -12191,42 +11960,107 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-from-parse5/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/hast-util-from-parse5/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "node_modules/hast-util-from-parse5/node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5/node_modules/hastscript": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-8.0.0.tgz", + "integrity": "sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-has-property": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/hast-util-has-property/-/hast-util-has-property-2.0.1.tgz", - "integrity": "sha512-X2+RwZIMTMKpXUzlotatPzWj8bspCymtXH3cfG3iQKV+wPF53Vgaqxi/eLqGck0wKq1kS9nvoB1wchbCPEL8sg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-has-property/-/hast-util-has-property-3.0.0.tgz", + "integrity": "sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA==", + "dependencies": { + "@types/hast": "^3.0.0" + }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-has-property/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/hast-util-is-body-ok-link": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hast-util-is-body-ok-link/-/hast-util-is-body-ok-link-2.0.0.tgz", - "integrity": "sha512-S58hCexyKdD31vMsErvgLfflW6vYWo/ixRLPJTtkOvLld24vyI8vmYmkgLA5LG3la2ME7nm7dLGdm48gfLRBfw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-body-ok-link/-/hast-util-is-body-ok-link-3.0.0.tgz", + "integrity": "sha512-VFHY5bo2nY8HiV6nir2ynmEB1XkxzuUffhEGeVx7orbu/B1KaGyeGgMZldvMVx5xWrDlLLG/kQ6YkJAMkBEx0w==", "dependencies": { - "@types/hast": "^2.0.0", - "hast-util-has-property": "^2.0.0", - "hast-util-is-element": "^2.0.0" + "@types/hast": "^3.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-is-body-ok-link/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/hast-util-is-element": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-2.1.3.tgz", - "integrity": "sha512-O1bKah6mhgEq2WtVMk+Ta5K7pPMqsBBlmzysLdcwKVrqzZQ0CHqUPiIVspNhAG1rvxpvJjtGee17XfauZYKqVA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", "dependencies": { - "@types/hast": "^2.0.0", - "@types/unist": "^2.0.0" + "@types/hast": "^3.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-is-element/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/hast-util-parse-selector": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-3.1.1.tgz", @@ -12240,35 +12074,45 @@ } }, "node_modules/hast-util-phrasing": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hast-util-phrasing/-/hast-util-phrasing-2.0.2.tgz", - "integrity": "sha512-yGkCfPkkfCyiLfK6KEl/orMDr/zgCnq/NaO9HfULx6/Zga5fso5eqQA5Ov/JZVqACygvw9shRYWgXNcG2ilo7w==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-phrasing/-/hast-util-phrasing-3.0.1.tgz", + "integrity": "sha512-6h60VfI3uBQUxHqTyMymMZnEbNl1XmEGtOxxKYL7stY2o601COo62AWAYBQR9lZbYXYSBoxag8UpPRXK+9fqSQ==", "dependencies": { - "@types/hast": "^2.0.0", - "hast-util-embedded": "^2.0.0", - "hast-util-has-property": "^2.0.0", - "hast-util-is-body-ok-link": "^2.0.0", - "hast-util-is-element": "^2.0.0" + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-has-property": "^3.0.0", + "hast-util-is-body-ok-link": "^3.0.0", + "hast-util-is-element": "^3.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/hast-util-raw": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-7.2.3.tgz", - "integrity": "sha512-RujVQfVsOrxzPOPSzZFiwofMArbQke6DJjnFfceiEbFh7S05CbPt0cYN+A5YeD3pso0JQk6O1aHBnx9+Pm2uqg==", + "node_modules/hast-util-phrasing/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", "dependencies": { - "@types/hast": "^2.0.0", - "@types/parse5": "^6.0.0", - "hast-util-from-parse5": "^7.0.0", - "hast-util-to-parse5": "^7.0.0", - "html-void-elements": "^2.0.0", - "parse5": "^6.0.0", - "unist-util-position": "^4.0.0", - "unist-util-visit": "^4.0.0", - "vfile": "^5.0.0", + "@types/unist": "*" + } + }, + "node_modules/hast-util-raw": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.0.2.tgz", + "integrity": "sha512-PldBy71wO9Uq1kyaMch9AHIghtQvIwxBUkv823pKmkTM3oV1JxtsTNYdevMxvUHqcnOAuO65JKU2+0NOxc2ksA==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" }, @@ -12277,37 +12121,61 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-raw/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/hast-util-raw/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, "node_modules/hast-util-sanitize": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hast-util-sanitize/-/hast-util-sanitize-4.1.0.tgz", - "integrity": "sha512-Hd9tU0ltknMGRDv+d6Ro/4XKzBqQnP/EZrpiTbpFYfXv/uOhWeKc+2uajcbEvAEH98VZd7eII2PiXm13RihnLw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/hast-util-sanitize/-/hast-util-sanitize-5.0.1.tgz", + "integrity": "sha512-IGrgWLuip4O2nq5CugXy4GI2V8kx4sFVy5Hd4vF7AR2gxS0N9s7nEAVUyeMtZKZvzrxVsHt73XdTsno1tClIkQ==", "dependencies": { - "@types/hast": "^2.0.0" + "@types/hast": "^3.0.0", + "@ungap/structured-clone": "^1.2.0", + "unist-util-position": "^5.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-sanitize/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/hast-util-select": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/hast-util-select/-/hast-util-select-5.0.5.tgz", - "integrity": "sha512-QQhWMhgTFRhCaQdgTKzZ5g31GLQ9qRb1hZtDPMqQaOhpLBziWcshUS0uCR5IJ0U1jrK/mxg35fmcq+Dp/Cy2Aw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/hast-util-select/-/hast-util-select-6.0.2.tgz", + "integrity": "sha512-hT/SD/d/Meu+iobvgkffo1QecV8WeKWxwsNMzcTJsKw1cKTQKSR/7ArJeURLNJF9HDjp9nVoORyNNJxrvBye8Q==", "dependencies": { - "@types/hast": "^2.0.0", - "@types/unist": "^2.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", "bcp-47-match": "^2.0.0", "comma-separated-tokens": "^2.0.0", - "css-selector-parser": "^1.0.0", + "css-selector-parser": "^3.0.0", + "devlop": "^1.0.0", "direction": "^2.0.0", - "hast-util-has-property": "^2.0.0", - "hast-util-to-string": "^2.0.0", - "hast-util-whitespace": "^2.0.0", + "hast-util-has-property": "^3.0.0", + "hast-util-to-string": "^3.0.0", + "hast-util-whitespace": "^3.0.0", "not": "^0.1.0", "nth-check": "^2.0.0", "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0", - "unist-util-visit": "^4.0.0", + "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" }, "funding": { @@ -12315,18 +12183,32 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-select/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/hast-util-select/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, "node_modules/hast-util-to-html": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-8.0.4.tgz", - "integrity": "sha512-4tpQTUOr9BMjtYyNlt0P50mH7xj0Ks2xpo8M943Vykljf99HW6EzulIoJP1N3eKOSScEHzyzi9dm7/cn0RfGwA==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.0.tgz", + "integrity": "sha512-IVGhNgg7vANuUA2XKrT6sOIIPgaYZnmLx3l/CCOAK0PtgfoHrZwX7jCSYyFxHTrGmC6S9q8aQQekjp4JPZF+cw==", "dependencies": { - "@types/hast": "^2.0.0", - "@types/unist": "^2.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", - "hast-util-raw": "^7.0.0", - "hast-util-whitespace": "^2.0.0", - "html-void-elements": "^2.0.0", + "hast-util-raw": "^9.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", @@ -12337,39 +12219,60 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/hast-util-to-mdast": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/hast-util-to-mdast/-/hast-util-to-mdast-8.4.1.tgz", - "integrity": "sha512-tfmBLASuCgyhCzpkTXM5kU8xeuS5jkMZ17BYm2YftGT5wvgc7uHXTZ/X8WfNd6F5NV/IGmrLsuahZ+jXQir4zQ==", + "node_modules/hast-util-to-html/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", "dependencies": { - "@types/extend": "^3.0.0", - "@types/hast": "^2.0.0", - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "extend": "^3.0.0", - "hast-util-has-property": "^2.0.0", - "hast-util-is-element": "^2.0.0", - "hast-util-phrasing": "^2.0.0", - "hast-util-to-text": "^3.0.0", - "mdast-util-phrasing": "^3.0.0", - "mdast-util-to-string": "^3.0.0", - "rehype-minify-whitespace": "^5.0.0", + "@types/unist": "*" + } + }, + "node_modules/hast-util-to-html/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "node_modules/hast-util-to-mdast": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/hast-util-to-mdast/-/hast-util-to-mdast-10.1.0.tgz", + "integrity": "sha512-DsL/SvCK9V7+vfc6SLQ+vKIyBDXTk2KLSbfBYkH4zeF/uR1yBajHRhkzuaUSGOB1WJSTieJBdHwxlC+HLKvZZw==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-phrasing": "^3.0.0", + "hast-util-to-html": "^9.0.0", + "hast-util-to-text": "^4.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "mdast-util-to-string": "^4.0.0", + "rehype-minify-whitespace": "^6.0.0", "trim-trailing-lines": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit": "^4.0.0" + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-to-mdast/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/hast-util-to-parse5": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-7.1.0.tgz", - "integrity": "sha512-YNRgAJkH2Jky5ySkIqFXTQiaqcAtJyVE+D5lkN6CdtOqrnkLfGYYrEcKuHOJZlp+MwjSwuD3fZuawI+sic/RBw==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz", + "integrity": "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==", "dependencies": { - "@types/hast": "^2.0.0", + "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", @@ -12380,42 +12283,82 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-to-parse5/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/hast-util-to-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-2.0.0.tgz", - "integrity": "sha512-02AQ3vLhuH3FisaMM+i/9sm4OXGSq1UhOOCpTLLQtHdL3tZt7qil69r8M8iDkZYyC0HCFylcYoP+8IO7ddta1A==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.0.tgz", + "integrity": "sha512-OGkAxX1Ua3cbcW6EJ5pT/tslVb90uViVkcJ4ZZIMW/R33DX/AkcJcRrPebPwJkHYwlDHXz4aIwvAAaAdtrACFA==", "dependencies": { - "@types/hast": "^2.0.0" + "@types/hast": "^3.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-to-string/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/hast-util-to-text": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-3.1.2.tgz", - "integrity": "sha512-tcllLfp23dJJ+ju5wCCZHVpzsQQ43+moJbqVX3jNWPB7z/KFC4FyZD6R7y94cHL6MQ33YtMZL8Z0aIXXI4XFTw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.0.tgz", + "integrity": "sha512-EWiE1FSArNBPUo1cKWtzqgnuRQwEeQbQtnFJRYV1hb1BWDgrAlBU0ExptvZMM/KSA82cDpm2sFGf3Dmc5Mza3w==", "dependencies": { - "@types/hast": "^2.0.0", - "@types/unist": "^2.0.0", - "hast-util-is-element": "^2.0.0", - "unist-util-find-after": "^4.0.0" + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-to-text/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/hast-util-to-text/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, "node_modules/hast-util-whitespace": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz", - "integrity": "sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dependencies": { + "@types/hast": "^3.0.0" + }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-whitespace/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/hastscript": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-7.2.0.tgz", @@ -12483,18 +12426,18 @@ } }, "node_modules/html-void-elements": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz", - "integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, "node_modules/html-whitespace-sensitive-tag-names": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/html-whitespace-sensitive-tag-names/-/html-whitespace-sensitive-tag-names-2.0.0.tgz", - "integrity": "sha512-SQdIvTTtnHAx72xGUIUudvVOCjeWvV1U7rvSFnNGxTGRw3ZC7RES4Gw6dm1nMYD60TXvm6zjk/bWqgNc5pjQaw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-whitespace-sensitive-tag-names/-/html-whitespace-sensitive-tag-names-3.0.0.tgz", + "integrity": "sha512-KlClZ3/Qy5UgvpvVvDomGhnQhNWH5INE8GwvSIQ9CWt1K0zbbXrl7eN5bWaafOZgtmO3jMPwUqmrmEwinhPq1w==", "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" @@ -12954,6 +12897,25 @@ "url": "https://opencollective.com/ioredis" } }, + "node_modules/ioredis-mock": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/ioredis-mock/-/ioredis-mock-8.9.0.tgz", + "integrity": "sha512-yIglcCkI1lvhwJVoMsR51fotZVsPsSk07ecTCgRTRlicG0Vq3lke6aAaHklyjmRNRsdYAgswqC2A0bPtQK4LSw==", + "dependencies": { + "@ioredis/as-callback": "^3.0.0", + "@ioredis/commands": "^1.2.0", + "fengari": "^0.1.4", + "fengari-interop": "^0.1.3", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12.22" + }, + "peerDependencies": { + "@types/ioredis-mock": "^8", + "ioredis": "^5" + } + }, "node_modules/ip-regex": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", @@ -13152,6 +13114,17 @@ "node": ">=0.10.0" } }, + "node_modules/is-absolute-url": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-4.0.1.tgz", + "integrity": "sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-alphabetical": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", @@ -13249,28 +13222,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "engines": { - "node": ">=4" - } - }, "node_modules/is-builtin-module": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", @@ -13346,17 +13297,6 @@ "resolved": "https://registry.npmjs.org/is-electron/-/is-electron-2.2.0.tgz", "integrity": "sha512-SpMppC2XR3YdxSzczXReBjqs2zGscWQpBIKqwXYBFic0ERaxNVgwLCHwOLZeESfdJQjX0RDvrJ1lBXX2ij+G1Q==" }, - "node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -13516,17 +13456,6 @@ "node": ">=8" } }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -13707,14 +13636,6 @@ "node": ">=12" } }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/isomorphic-fetch": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", @@ -15633,7 +15554,8 @@ "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true }, "node_modules/js-yaml": { "version": "3.14.1", @@ -15752,6 +15674,7 @@ "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, "bin": { "jsesc": "bin/jsesc" }, @@ -15825,6 +15748,7 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, "bin": { "json5": "lib/cli.js" }, @@ -16053,17 +15977,22 @@ "dev": true }, "node_modules/linkify-it": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz", - "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", "dependencies": { - "uc.micro": "^1.0.1" + "uc.micro": "^2.0.0" } }, + "node_modules/linkify-it/node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==" + }, "node_modules/linkifyjs": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.1.1.tgz", - "integrity": "sha512-zFN/CTVmbcVef+WaDXT63dNzzkfRBKT1j464NJQkV7iSgJU0sLBus9W0HBwnXK13/hf168pbrx/V/bjEHOXNHA==" + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.1.3.tgz", + "integrity": "sha512-auMesunaJ8yfkHvK4gfg1K0SaKX/6Wn9g2Aac/NwX+l5VdmFZzo/hdPGxEOETj+ryRa4/fiOPjeeKURSAJx1sg==" }, "node_modules/listr2": { "version": "4.0.5", @@ -16226,15 +16155,6 @@ "node": ">=8" } }, - "node_modules/lockfile": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/lockfile/-/lockfile-1.0.4.tgz", - "integrity": "sha512-cvbTwETRfsFh4nHsL1eGWapU1XFi5Ot9E85sWAwia7Y7EgB7vfqcZhTKZ+l7hCGxSPoushMv5GKhT5PdLv03WA==", - "dev": true, - "dependencies": { - "signal-exit": "^3.0.2" - } - }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -16266,12 +16186,6 @@ "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" }, - "node_modules/lodash.defaultsdeep": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz", - "integrity": "sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==", - "dev": true - }, "node_modules/lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", @@ -16660,7 +16574,8 @@ "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true }, "node_modules/makeerror": { "version": "1.0.12", @@ -16681,18 +16596,19 @@ } }, "node_modules/markdown-it": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.1.tgz", - "integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.0.0.tgz", + "integrity": "sha512-seFjF0FIcPt4P9U39Bq1JYblX0KZCjDLFFQPHpL5AzHpqPEKtosxmdq/LTVZnjfH7tjt9BxStm+wXcDBNuYmzw==", "dependencies": { "argparse": "^2.0.1", - "entities": "~3.0.1", - "linkify-it": "^4.0.1", - "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.0.0" }, "bin": { - "markdown-it": "bin/markdown-it.js" + "markdown-it": "bin/markdown-it.mjs" } }, "node_modules/markdown-it-anchor": { @@ -16709,16 +16625,15 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, - "node_modules/markdown-it/node_modules/entities": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", - "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } + "node_modules/markdown-it/node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==" + }, + "node_modules/markdown-it/node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==" }, "node_modules/marked": { "version": "4.3.0", @@ -16731,47 +16646,39 @@ "node": ">= 12" } }, - "node_modules/mdast-util-definitions": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz", - "integrity": "sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA==", - "dependencies": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "unist-util-visit": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/mdast-util-directive": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-2.2.4.tgz", - "integrity": "sha512-sK3ojFP+jpj1n7Zo5ZKvoxP1MvLyzVG63+gm40Z/qI00avzdPCYxt7RBMgofwAva9gBjbDBWVRB/i+UD+fUCzQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.0.0.tgz", + "integrity": "sha512-JUpYOqKI4mM3sZcNxmF/ox04XYFFkNwr0CFlrQIkCwbvH0xzMCqkMqAde9wRd80VAhaUrwFwKm2nxretdT1h7Q==", "dependencies": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "mdast-util-from-markdown": "^1.3.0", - "mdast-util-to-markdown": "^1.5.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", - "unist-util-visit-parents": "^5.1.3" + "unist-util-visit-parents": "^6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-directive/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, "node_modules/mdast-util-find-and-replace": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-2.2.2.tgz", - "integrity": "sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.1.tgz", + "integrity": "sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==", "dependencies": { - "@types/mdast": "^3.0.0", + "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.0.0" + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" }, "funding": { "type": "opencollective", @@ -16790,35 +16697,41 @@ } }, "node_modules/mdast-util-from-markdown": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.0.tgz", - "integrity": "sha512-HN3W1gRIuN/ZW295c7zi7g9lVBllMgZE40RxCX37wrTPWXCWtpvOZdfnuK+1WNpvZje6XuJeI3Wnb4TJEUem+g==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.0.tgz", + "integrity": "sha512-n7MTOr/z+8NAX/wmhhDji8O3bRvPTV/U0oTCaZJkjhPSKTPhS3xufVhKGF8s1pJ7Ox4QgoIU7KHseh09S+9rTA==", "dependencies": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", - "mdast-util-to-string": "^3.1.0", - "micromark": "^3.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-decode-string": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "unist-util-stringify-position": "^3.0.0", - "uvu": "^0.5.0" + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-from-markdown/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, "node_modules/mdast-util-gfm-strikethrough": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-1.0.3.tgz", - "integrity": "sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-to-markdown": "^1.3.0" + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" }, "funding": { "type": "opencollective", @@ -16826,12 +16739,12 @@ } }, "node_modules/mdast-util-newline-to-break": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-newline-to-break/-/mdast-util-newline-to-break-1.0.0.tgz", - "integrity": "sha512-491LcYv3gbGhhCrLoeALncQmega2xPh+m3gbsIhVsOX4sw85+ShLFPvPyibxc1Swx/6GtzxgVodq+cGa/47ULg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-newline-to-break/-/mdast-util-newline-to-break-2.0.0.tgz", + "integrity": "sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog==", "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-find-and-replace": "^2.0.0" + "@types/mdast": "^4.0.0", + "mdast-util-find-and-replace": "^3.0.0" }, "funding": { "type": "opencollective", @@ -16839,12 +16752,12 @@ } }, "node_modules/mdast-util-phrasing": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz", - "integrity": "sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", "dependencies": { - "@types/mdast": "^3.0.0", - "unist-util-is": "^5.0.0" + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" }, "funding": { "type": "opencollective", @@ -16852,36 +16765,45 @@ } }, "node_modules/mdast-util-to-hast": { - "version": "12.3.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-12.3.0.tgz", - "integrity": "sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==", + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.1.0.tgz", + "integrity": "sha512-/e2l/6+OdGp/FB+ctrJ9Avz71AN/GRH3oi/3KAx/kMnoUsD6q0woXlDT8lLEeViVKE7oZxE7RXzvO3T8kF2/sA==", "dependencies": { - "@types/hast": "^2.0.0", - "@types/mdast": "^3.0.0", - "mdast-util-definitions": "^5.0.0", - "micromark-util-sanitize-uri": "^1.1.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", - "unist-util-generated": "^2.0.0", - "unist-util-position": "^4.0.0", - "unist-util-visit": "^4.0.0" + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-to-hast/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/mdast-util-to-markdown": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz", - "integrity": "sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz", + "integrity": "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==", "dependencies": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", - "mdast-util-phrasing": "^3.0.0", - "mdast-util-to-string": "^3.0.0", - "micromark-util-decode-string": "^1.0.0", - "unist-util-visit": "^4.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" }, "funding": { @@ -16889,12 +16811,17 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-to-markdown/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, "node_modules/mdast-util-to-string": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.1.1.tgz", - "integrity": "sha512-tGvhT94e+cVnQt8JWE9/b3cUQZWS732TJxXHktvP+BYo62PpYD53Ls/6cC60rW21dW+txxiM4zMdc6abASvZKA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", "dependencies": { - "@types/mdast": "^3.0.0" + "@types/mdast": "^4.0.0" }, "funding": { "type": "opencollective", @@ -16994,9 +16921,9 @@ } }, "node_modules/micromark": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.1.0.tgz", - "integrity": "sha512-6Mj0yHLdUZjHnOPgr5xfWIMqMWS12zDN6iws9SLuSz76W8jTtAv24MN4/CL7gJrl5vtxGInkkqDv/JIoRsQOvA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.0.tgz", + "integrity": "sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==", "funding": [ { "type": "GitHub Sponsors", @@ -17011,26 +16938,26 @@ "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", - "micromark-core-commonmark": "^1.0.1", - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-chunked": "^1.0.0", - "micromark-util-combine-extensions": "^1.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-encode": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-sanitize-uri": "^1.0.0", - "micromark-util-subtokenize": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.1", - "uvu": "^0.5.0" + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-core-commonmark": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.0.6.tgz", - "integrity": "sha512-K+PkJTxqjFfSNkfAhp4GB+cZPfQd6dxtTXnf+RjZOV7T4EEXnvgzOcnp+eSTmpGk9d1S9sL6/lqrgSNn/s0HZA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.0.tgz", + "integrity": "sha512-jThOz/pVmAYUtkroV3D5c1osFXAMv9e0ypGDOIZuCeAe91/sD6BoE2Sjzt30yuXtwOYUmySOhMas/PVyh02itA==", "funding": [ { "type": "GitHub Sponsors", @@ -17043,35 +16970,35 @@ ], "dependencies": { "decode-named-character-reference": "^1.0.0", - "micromark-factory-destination": "^1.0.0", - "micromark-factory-label": "^1.0.0", - "micromark-factory-space": "^1.0.0", - "micromark-factory-title": "^1.0.0", - "micromark-factory-whitespace": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-chunked": "^1.0.0", - "micromark-util-classify-character": "^1.0.0", - "micromark-util-html-tag-name": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-subtokenize": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.1", - "uvu": "^0.5.0" + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-extension-directive": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-2.1.2.tgz", - "integrity": "sha512-brqLEztt14/73snVXYsq9Cv6ng67O+Sy69ZuM0s8ZhN/GFI9rnyXyj0Y0DaCwi648vCImv7/U1H5TzR7wMv5jw==", - "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-factory-whitespace": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "parse-entities": "^4.0.0", - "uvu": "^0.5.0" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-3.0.0.tgz", + "integrity": "sha512-61OI07qpQrERc+0wEysLHMvoiO3s2R56x5u7glHq2Yqq6EHbH4dW25G9GfDdGCDYqA21KE6DWgNSzxSwHc2hSg==", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "parse-entities": "^4.0.0" }, "funding": { "type": "opencollective", @@ -17095,10 +17022,10 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/micromark-extension-gfm-strikethrough/node_modules/micromark-util-character": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.0.1.tgz", - "integrity": "sha512-3wgnrmEAJ4T+mGXAUfMvMAbxU9RDG43XmGce4j6CwPtVxB3vfwXSZ6KhFwDzZ3mZHhmPimMAXg71veiBGzeAZw==", + "node_modules/micromark-factory-destination": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz", + "integrity": "sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==", "funding": [ { "type": "GitHub Sponsors", @@ -17110,32 +17037,15 @@ } ], "dependencies": { + "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, - "node_modules/micromark-extension-gfm-strikethrough/node_modules/micromark-util-chunked": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.0.tgz", - "integrity": "sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-extension-gfm-strikethrough/node_modules/micromark-util-classify-character": { + "node_modules/micromark-factory-label": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.0.tgz", - "integrity": "sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw==", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.0.tgz", + "integrity": "sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw==", "funding": [ { "type": "GitHub Sponsors", @@ -17147,15 +17057,16 @@ } ], "dependencies": { + "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, - "node_modules/micromark-extension-gfm-strikethrough/node_modules/micromark-util-resolve-all": { + "node_modules/micromark-factory-space": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.0.tgz", - "integrity": "sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA==", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz", + "integrity": "sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==", "funding": [ { "type": "GitHub Sponsors", @@ -17167,103 +17078,14 @@ } ], "dependencies": { + "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, - "node_modules/micromark-extension-gfm-strikethrough/node_modules/micromark-util-symbol": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", - "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-extension-gfm-strikethrough/node_modules/micromark-util-types": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.0.tgz", - "integrity": "sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-factory-destination": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.0.0.tgz", - "integrity": "sha512-eUBA7Rs1/xtTVun9TmV3gjfPz2wEwgK5R5xcbIM5ZYAtvGF6JkyaDsj0agx8urXnO31tEO6Ug83iVH3tdedLnw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-factory-label": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.0.2.tgz", - "integrity": "sha512-CTIwxlOnU7dEshXDQ+dsr2n+yxpP0+fn271pu0bwDIS8uqfFcumXpj5mLn3hSC8iw2MUr6Gx8EcKng1dD7i6hg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - } - }, - "node_modules/micromark-factory-space": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.0.0.tgz", - "integrity": "sha512-qUmqs4kj9a5yBnk3JMLyjtWYN6Mzfcx8uJfi5XAveBniDevmZasdGBba5b4QsvRcAkmvGo5ACmSUmyGiKTLZew==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, "node_modules/micromark-factory-title": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.0.2.tgz", - "integrity": "sha512-zily+Nr4yFqgMGRKLpTVsNl5L4PMu485fGFDOQJQBl2NFpjGte1e86zC0da93wf97jrc4+2G2GQudFMHn3IX+A==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.0.tgz", + "integrity": "sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A==", "funding": [ { "type": "GitHub Sponsors", @@ -17275,17 +17097,16 @@ } ], "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-factory-whitespace": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.0.0.tgz", - "integrity": "sha512-Qx7uEyahU1lt1RnsECBiuEbfr9INjQTGa6Err+gF3g0Tx4YEviPbqqGKNv/NrBaE7dVHdn1bVZKM/n5I/Bak7A==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.0.tgz", + "integrity": "sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA==", "funding": [ { "type": "GitHub Sponsors", @@ -17297,16 +17118,16 @@ } ], "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-util-character": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.1.0.tgz", - "integrity": "sha512-agJ5B3unGNJ9rJvADMJ5ZiYjBRyDpzKAOk01Kpi1TKhlT1APx3XZk6eN7RtSz1erbWHC2L8T3xLZ81wdtGRZzg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", + "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", "funding": [ { "type": "GitHub Sponsors", @@ -17318,14 +17139,14 @@ } ], "dependencies": { - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-util-chunked": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.0.0.tgz", - "integrity": "sha512-5e8xTis5tEZKgesfbQMKRCyzvffRRUX+lK/y+DvsMFdabAicPkkZV6gO+FEWi9RfuKKoxxPwNL+dFF0SMImc1g==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.0.tgz", + "integrity": "sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg==", "funding": [ { "type": "GitHub Sponsors", @@ -17337,13 +17158,13 @@ } ], "dependencies": { - "micromark-util-symbol": "^1.0.0" + "micromark-util-symbol": "^2.0.0" } }, "node_modules/micromark-util-classify-character": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.0.0.tgz", - "integrity": "sha512-F8oW2KKrQRb3vS5ud5HIqBVkCqQi224Nm55o5wYLzY/9PwHGXC01tr3d7+TqHHz6zrKQ72Okwtvm/xQm6OVNZA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.0.tgz", + "integrity": "sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw==", "funding": [ { "type": "GitHub Sponsors", @@ -17355,15 +17176,15 @@ } ], "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-util-combine-extensions": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", - "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.0.tgz", + "integrity": "sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ==", "funding": [ { "type": "GitHub Sponsors", @@ -17375,14 +17196,14 @@ } ], "dependencies": { - "micromark-util-chunked": "^1.0.0", - "micromark-util-types": "^1.0.0" + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-util-decode-numeric-character-reference": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.0.0.tgz", - "integrity": "sha512-OzO9AI5VUtrTD7KSdagf4MWgHMtET17Ua1fIpXTpuhclCqD8egFWo85GxSGvxgkGS74bEahvtM0WP0HjvV0e4w==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.1.tgz", + "integrity": "sha512-bmkNc7z8Wn6kgjZmVHOX3SowGmVdhYS7yBpMnuMnPzDq/6xwVA604DuOXMZTO1lvq01g+Adfa0pE2UKGlxL1XQ==", "funding": [ { "type": "GitHub Sponsors", @@ -17394,13 +17215,13 @@ } ], "dependencies": { - "micromark-util-symbol": "^1.0.0" + "micromark-util-symbol": "^2.0.0" } }, "node_modules/micromark-util-decode-string": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.0.2.tgz", - "integrity": "sha512-DLT5Ho02qr6QWVNYbRZ3RYOSSWWFuH3tJexd3dgN1odEuPNxCngTCXJum7+ViRAd9BbdxCvMToPOD/IvVhzG6Q==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.0.tgz", + "integrity": "sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA==", "funding": [ { "type": "GitHub Sponsors", @@ -17413,15 +17234,15 @@ ], "dependencies": { "decode-named-character-reference": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-symbol": "^1.0.0" + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" } }, "node_modules/micromark-util-encode": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.0.1.tgz", - "integrity": "sha512-U2s5YdnAYexjKDel31SVMPbfi+eF8y1U4pfiRW/Y8EFVCy/vgxk/2wWTxzcqE71LHtCuCzlBDRU2a5CQ5j+mQA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz", + "integrity": "sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==", "funding": [ { "type": "GitHub Sponsors", @@ -17434,9 +17255,9 @@ ] }, "node_modules/micromark-util-html-tag-name": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.1.0.tgz", - "integrity": "sha512-BKlClMmYROy9UiV03SwNmckkjn8QHVaWkqoAqzivabvdGcwNGMMMH/5szAnywmsTBUzDsU57/mFi0sp4BQO6dA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.0.tgz", + "integrity": "sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw==", "funding": [ { "type": "GitHub Sponsors", @@ -17449,9 +17270,9 @@ ] }, "node_modules/micromark-util-normalize-identifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.0.0.tgz", - "integrity": "sha512-yg+zrL14bBTFrQ7n35CmByWUTFsgst5JhA4gJYoty4Dqzj4Z4Fr/DHekSS5aLfH9bdlfnSvKAWsAgJhIbogyBg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.0.tgz", + "integrity": "sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w==", "funding": [ { "type": "GitHub Sponsors", @@ -17463,13 +17284,13 @@ } ], "dependencies": { - "micromark-util-symbol": "^1.0.0" + "micromark-util-symbol": "^2.0.0" } }, "node_modules/micromark-util-resolve-all": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.0.0.tgz", - "integrity": "sha512-CB/AGk98u50k42kvgaMM94wzBqozSzDDaonKU7P7jwQIuH2RU0TeBqGYJz2WY1UdihhjweivStrJ2JdkdEmcfw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.0.tgz", + "integrity": "sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA==", "funding": [ { "type": "GitHub Sponsors", @@ -17481,13 +17302,13 @@ } ], "dependencies": { - "micromark-util-types": "^1.0.0" + "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-util-sanitize-uri": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.1.0.tgz", - "integrity": "sha512-RoxtuSCX6sUNtxhbmsEFQfWzs8VN7cTctmBPvYivo98xb/kDEoTCtJQX5wyzIYEmk/lvNFTat4hL8oW0KndFpg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz", + "integrity": "sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==", "funding": [ { "type": "GitHub Sponsors", @@ -17499,15 +17320,15 @@ } ], "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-encode": "^1.0.0", - "micromark-util-symbol": "^1.0.0" + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" } }, "node_modules/micromark-util-subtokenize": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.0.2.tgz", - "integrity": "sha512-d90uqCnXp/cy4G881Ub4psE57Sf8YD0pim9QdjCRNjfas2M1u6Lbt+XZK9gnHL2XFhnozZiEdCa9CNfXSfQ6xA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.0.tgz", + "integrity": "sha512-vc93L1t+gpR3p8jxeVdaYlbV2jTYteDje19rNSS/H5dlhxUYll5Fy6vJ2cDwP8RnsXi818yGty1ayP55y3W6fg==", "funding": [ { "type": "GitHub Sponsors", @@ -17519,16 +17340,16 @@ } ], "dependencies": { - "micromark-util-chunked": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-util-symbol": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.0.1.tgz", - "integrity": "sha512-oKDEMK2u5qqAptasDAwWDXq0tG9AssVwAx3E9bBF3t/shRIGsWIRG+cGafs2p/SnDSOecnt6hZPCE2o6lHfFmQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", + "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", "funding": [ { "type": "GitHub Sponsors", @@ -17541,9 +17362,9 @@ ] }, "node_modules/micromark-util-types": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.0.2.tgz", - "integrity": "sha512-DCfg/T8fcrhrRKTPjRrw/5LLvdGV7BHySf/1LOZx7TzWZdYRjogNtyNq885z3nNallwr3QUKARjqvHqX1/7t+w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.0.tgz", + "integrity": "sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==", "funding": [ { "type": "GitHub Sponsors", @@ -17674,23 +17495,15 @@ "resolved": "https://registry.npmjs.org/module-alias/-/module-alias-2.2.2.tgz", "integrity": "sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q==" }, - "node_modules/mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "engines": { - "node": ">=4" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/msgpackr": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.9.2.tgz", - "integrity": "sha512-xtDgI3Xv0AAiZWLRGDchyzBwU6aq0rwJ+W+5Y4CZhEWtkl/hJtFFLc+3JtGTw7nz1yquxs7nL8q/yA2aqpflIQ==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.10.1.tgz", + "integrity": "sha512-r5VRLv9qouXuLiIBrLpl2d5ZvPt8svdQTl5/vMvE4nzDMyEX4sgW5yWhuBBj5UmgwOTWj8CIdSXn5sAfsHAWIQ==", "optionalDependencies": { "msgpackr-extract": "^3.0.2" } @@ -17762,14 +17575,14 @@ "dev": true }, "node_modules/nan": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", - "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==" + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz", + "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==" }, "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "funding": [ { "type": "github", @@ -17828,9 +17641,9 @@ } }, "node_modules/needle/node_modules/sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", + "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==" }, "node_modules/negotiator": { "version": "0.6.3", @@ -18066,7 +17879,8 @@ "node_modules/node-releases": { "version": "2.0.13", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==" + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true }, "node_modules/nodemon": { "version": "2.0.22", @@ -18321,7 +18135,7 @@ "node_modules/number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==", "engines": { "node": ">=0.10.0" } @@ -18497,28 +18311,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object.omit": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-3.0.0.tgz", - "integrity": "sha512-EO+BCv6LJfu+gBIF3ggLicFebFLN5zqzz/WWJlMFfkMyGth+oBkhxzDl0wx2W4GkLzuQs/FsSkXZb2IMWQqmBQ==", - "dependencies": { - "is-extendable": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object.values": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", @@ -19023,9 +18815,15 @@ } }, "node_modules/parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } }, "node_modules/parse5-htmlparser2-tree-adapter": { "version": "7.0.0", @@ -19039,17 +18837,6 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", - "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", - "dependencies": { - "entities": "^4.4.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -19227,12 +19014,6 @@ "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=" }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true - }, "node_modules/pg": { "version": "8.11.0", "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.0.tgz", @@ -19324,7 +19105,8 @@ "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true }, "node_modules/picomatch": { "version": "2.3.1", @@ -19616,9 +19398,9 @@ } }, "node_modules/prosemirror-inputrules": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.2.1.tgz", - "integrity": "sha512-3LrWJX1+ULRh5SZvbIQlwZafOXqp1XuV21MGBu/i5xsztd+9VD15x6OtN6mdqSFI7/8Y77gYUbQ6vwwJ4mr6QQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.4.0.tgz", + "integrity": "sha512-6ygpPRuTJ2lcOXs9JkefieMst63wVJBgHZGl5QOytN7oSZs3Co/BYbc3Yx9zm9H37Bxw8kVzCnDsihsVsL4yEg==", "dependencies": { "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.0.0" @@ -19634,18 +19416,18 @@ } }, "node_modules/prosemirror-markdown": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.11.0.tgz", - "integrity": "sha512-yP9mZqPRstjZhhf3yykCQNE3AijxARrHe4e7esV9A+gp4cnGOH4QvrKYPpXLHspNWyvJJ+0URH+iIvV5qP1I2Q==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.12.0.tgz", + "integrity": "sha512-6F5HS8Z0HDYiS2VQDZzfZP6A0s/I0gbkJy8NCzzDMtcsz3qrfqyroMMeoSjAmOhDITyon11NbXSzztfKi+frSQ==", "dependencies": { - "markdown-it": "^13.0.1", + "markdown-it": "^14.0.0", "prosemirror-model": "^1.0.0" } }, "node_modules/prosemirror-menu": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.2.2.tgz", - "integrity": "sha512-437HIWTq4F9cTX+kPfqZWWm+luJm95Aut/mLUy+9OMrOml0bmWDS26ceC6SNfb2/S94et1sZ186vLO7pDHzxSw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.2.4.tgz", + "integrity": "sha512-S/bXlc0ODQup6aiBbWVsX/eM+xJgCTAfMq/nLqaO5ID/am4wS0tTCIkzwytmao7ypEtjj39i7YbJjAgO20mIqA==", "dependencies": { "crelt": "^1.0.0", "prosemirror-commands": "^1.0.0", @@ -19654,9 +19436,9 @@ } }, "node_modules/prosemirror-model": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.19.2.tgz", - "integrity": "sha512-RXl0Waiss4YtJAUY3NzKH0xkJmsZupCIccqcIFoLTIKFlKNbIvFDRl27/kQy1FP8iUAxrjRRfIVvOebnnXJgqQ==", + "version": "1.19.4", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.19.4.tgz", + "integrity": "sha512-RPmVXxUfOhyFdayHawjuZCxiROsm9L4FCUA6pWI+l7n2yCBsWy9VpdE1hpDHUS8Vad661YLY9AzqfjLhAKQ4iQ==", "dependencies": { "orderedmap": "^2.0.0" } @@ -19690,9 +19472,9 @@ } }, "node_modules/prosemirror-tables": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.3.4.tgz", - "integrity": "sha512-z6uLSQ1BLC3rgbGwZmpfb+xkdvD7W/UOsURDfognZFYaTtc0gsk7u/t71Yijp2eLflVpffMk6X0u0+u+MMDvIw==", + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.3.7.tgz", + "integrity": "sha512-oEwX1wrziuxMtwFvdDWSFHVUWrFJWt929kVVfHvtTi8yvw+5ppxjXZkMG/fuTdFo+3DXyIPSKfid+Be1npKXDA==", "dependencies": { "prosemirror-keymap": "^1.1.2", "prosemirror-model": "^1.8.1", @@ -19702,19 +19484,17 @@ } }, "node_modules/prosemirror-trailing-node": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-2.0.4.tgz", - "integrity": "sha512-0Yl9w7IdHkaCdqR+NE3FOucePME4OmiGcybnF1iasarEILP5U8+4xTnl53yafULjmwcg1SrSG65Hg7Zk2H2v3g==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-2.0.8.tgz", + "integrity": "sha512-ujRYhSuhQb1Jsarh1IHqb2KoSnRiD7wAMDGucP35DN7j5af6X7B18PfdPIrbwsPTqIAj0fyOvxbuPsWhNvylmA==", "dependencies": { - "@babel/runtime": "^7.21.0", - "@remirror/core-constants": "^2.0.1", - "@remirror/core-helpers": "^2.0.2", + "@remirror/core-constants": "^2.0.2", "escape-string-regexp": "^4.0.0" }, "peerDependencies": { "prosemirror-model": "^1.19.0", "prosemirror-state": "^1.4.2", - "prosemirror-view": "^1.30.2" + "prosemirror-view": "^1.31.2" } }, "node_modules/prosemirror-trailing-node/node_modules/escape-string-regexp": { @@ -19729,17 +19509,17 @@ } }, "node_modules/prosemirror-transform": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.7.3.tgz", - "integrity": "sha512-qDapyx5lqYfxVeUWEw0xTGgeP2S8346QtE7DxkalsXlX89lpzkY6GZfulgfHyk1n4tf74sZ7CcXgcaCcGjsUtA==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.8.0.tgz", + "integrity": "sha512-BaSBsIMv52F1BVVMvOmp1yzD3u65uC3HTzCBQV1WDPqJRQ2LuHKcyfn0jwqodo8sR9vVzMzZyI+Dal5W9E6a9A==", "dependencies": { "prosemirror-model": "^1.0.0" } }, "node_modules/prosemirror-view": { - "version": "1.31.5", - "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.31.5.tgz", - "integrity": "sha512-tobRCDeCp61elR1d97XE/JTL9FDIfswZpWeNs7GKJjAJvWyMGHWYFCq29850p6bbG2bckP+i9n1vT56RifosbA==", + "version": "1.33.1", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.33.1.tgz", + "integrity": "sha512-62qkYgSJIkwIMMCpuGuPzc52DiK1Iod6TWoIMxP4ja6BTD4yO8kCUL64PZ/WhH/dJ9fW0CDO39FhH1EMyhUFEg==", "dependencies": { "prosemirror-model": "^1.16.0", "prosemirror-state": "^1.0.0", @@ -19763,9 +19543,9 @@ "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" }, "node_modules/proto3-json-serializer/node_modules/protobufjs": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.3.tgz", - "integrity": "sha512-TtpvOqwB5Gdz/PQmOjgsrGH1nHjAQVCN7JG4A6r1sXRWESL5rNMAiRcBQlCAdKxZcAbstExQePYG8xof/JVRgg==", + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.6.tgz", + "integrity": "sha512-dgJaEDDL6x8ASUZ1YqWciTRrdOuYNzoOf27oHNfdyvKqHr5i0FV7FSLU+aIeFjyFgVxrpTOtQUi0BLLBymZaBw==", "hasInstallScript": true, "dependencies": { "@protobufjs/aspromise": "^1.1.2", @@ -19786,9 +19566,9 @@ } }, "node_modules/protobufjs": { - "version": "6.11.3", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.3.tgz", - "integrity": "sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==", + "version": "6.11.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz", + "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==", "hasInstallScript": true, "dependencies": { "@protobufjs/aspromise": "^1.1.2", @@ -20054,16 +19834,6 @@ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", "dev": true }, - "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", @@ -20072,6 +19842,14 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "engines": { + "node": ">=6" + } + }, "node_modules/pure-rand": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.2.tgz", @@ -20306,6 +20084,14 @@ "node": ">=8.10.0" } }, + "node_modules/readline-sync": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/readline-sync/-/readline-sync-1.4.10.tgz", + "integrity": "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/receptacle": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/receptacle/-/receptacle-1.3.2.tgz", @@ -20333,94 +20119,6 @@ "node": ">=4" } }, - "node_modules/redis-memory-server": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/redis-memory-server/-/redis-memory-server-0.6.0.tgz", - "integrity": "sha512-jo80uIfGY2goCY7rXXIOxs7rD/hsTtyWKUE1IA1wraugPxpkz21vEjckdkAvEZ+eawlkn0b+53Fv0N1CNfIOVw==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "camelcase": "^6.0.0", - "cross-spawn": "^7.0.3", - "debug": "^4.2.0", - "extract-zip": "^2.0.1", - "find-cache-dir": "^3.3.1", - "find-package-json": "^1.2.0", - "get-port": "^5.1.1", - "https-proxy-agent": "^5.0.0", - "lockfile": "^1.0.4", - "lodash.defaultsdeep": "^4.6.1", - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2", - "semver": "^7.3.2", - "tar": "^6.1.0", - "tmp": "^0.2.1", - "uuid": "8.3.0" - }, - "engines": { - "node": ">=10.15.0" - } - }, - "node_modules/redis-memory-server/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/redis-memory-server/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/redis-memory-server/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/redis-memory-server/node_modules/tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", - "dev": true, - "dependencies": { - "rimraf": "^3.0.0" - }, - "engines": { - "node": ">=8.17.0" - } - }, - "node_modules/redis-memory-server/node_modules/uuid": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz", - "integrity": "sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ==", - "dev": true, - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/redis-parser": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", @@ -20435,7 +20133,8 @@ "node_modules/regenerator-runtime": { "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true }, "node_modules/regexp-tree": { "version": "0.1.27", @@ -20484,128 +20183,209 @@ "jsesc": "bin/jsesc" } }, + "node_modules/rehype-external-links": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rehype-external-links/-/rehype-external-links-3.0.0.tgz", + "integrity": "sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw==", + "dependencies": { + "@types/hast": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-is-element": "^3.0.0", + "is-absolute-url": "^4.0.0", + "space-separated-tokens": "^2.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-external-links/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/rehype-format": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/rehype-format/-/rehype-format-4.0.1.tgz", - "integrity": "sha512-HA92WeqFri00yiClrz54IIpM9no2DH9Mgy5aFmInNODoAYn+hN42a6oqJTIie2nj0HwFyV7JvOYx5YHBphN8mw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/rehype-format/-/rehype-format-5.0.0.tgz", + "integrity": "sha512-kM4II8krCHmUhxrlvzFSptvaWh280Fr7UGNJU5DCMuvmAwGCNmGfi9CvFAQK6JDjsNoRMWQStglK3zKJH685Wg==", "dependencies": { - "@types/hast": "^2.0.0", - "hast-util-embedded": "^2.0.0", - "hast-util-is-element": "^2.0.0", - "hast-util-phrasing": "^2.0.0", - "hast-util-whitespace": "^2.0.0", - "html-whitespace-sensitive-tag-names": "^2.0.0", - "rehype-minify-whitespace": "^5.0.0", - "unified": "^10.0.0", - "unist-util-visit-parents": "^5.0.0" + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "hast-util-phrasing": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "html-whitespace-sensitive-tag-names": "^3.0.0", + "rehype-minify-whitespace": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, + "node_modules/rehype-format/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/rehype-minify-whitespace": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/rehype-minify-whitespace/-/rehype-minify-whitespace-5.0.1.tgz", - "integrity": "sha512-PPp4lWJiBPlePI/dv1BeYktbwkfgXkrK59MUa+tYbMPgleod+4DvFK2PLU0O0O60/xuhHfiR9GUIUlXTU8sRIQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/rehype-minify-whitespace/-/rehype-minify-whitespace-6.0.0.tgz", + "integrity": "sha512-i9It4YHR0Sf3GsnlR5jFUKXRr9oayvEk9GKQUkwZv6hs70OH9q3OCZrq9PpLvIGKt3W+JxBOxCidNVpH/6rWdA==", "dependencies": { - "@types/hast": "^2.0.0", - "hast-util-embedded": "^2.0.0", - "hast-util-is-element": "^2.0.0", - "hast-util-whitespace": "^2.0.0", - "unified": "^10.0.0", - "unist-util-is": "^5.0.0" + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "unist-util-is": "^6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, + "node_modules/rehype-minify-whitespace/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/rehype-parse": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-8.0.4.tgz", - "integrity": "sha512-MJJKONunHjoTh4kc3dsM1v3C9kGrrxvA3U8PxZlP2SjH8RNUSrb+lF7Y0KVaUDnGH2QZ5vAn7ulkiajM9ifuqg==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.0.tgz", + "integrity": "sha512-WG7nfvmWWkCR++KEkZevZb/uw41E8TsH4DsY9UxsTbIXCVGbAs4S+r8FrQ+OtH5EEQAs+5UxKC42VinkmpA1Yw==", "dependencies": { - "@types/hast": "^2.0.0", - "hast-util-from-parse5": "^7.0.0", - "parse5": "^6.0.0", - "unified": "^10.0.0" + "@types/hast": "^3.0.0", + "hast-util-from-html": "^2.0.0", + "unified": "^11.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, + "node_modules/rehype-parse/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/rehype-raw": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-6.1.1.tgz", - "integrity": "sha512-d6AKtisSRtDRX4aSPsJGTfnzrX2ZkHQLE5kiUuGOeEoLpbEulFF4hj0mLPbsa+7vmguDKOVVEQdHKDSwoaIDsQ==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", "dependencies": { - "@types/hast": "^2.0.0", - "hast-util-raw": "^7.2.0", - "unified": "^10.0.0" + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, + "node_modules/rehype-raw/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/rehype-remark": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/rehype-remark/-/rehype-remark-9.1.2.tgz", - "integrity": "sha512-c0fG3/CrJ95zAQ07xqHSkdpZybwdsY7X5dNWvgL2XqLKZuqmG3+vk6kP/4miCnp+R+x/0uKKRSpfXb9aGR8Z5w==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/rehype-remark/-/rehype-remark-10.0.0.tgz", + "integrity": "sha512-+aDXY/icqMFOafJQomVjxe3BAP7aR3lIsQ3GV6VIwpbCD2nvNFOXjGvotMe5p0Ny+Gt6L13DhEf/FjOOpTuUbQ==", "dependencies": { - "@types/hast": "^2.0.0", - "@types/mdast": "^3.0.0", - "hast-util-to-mdast": "^8.3.0", - "unified": "^10.0.0" + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "hast-util-to-mdast": "^10.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, + "node_modules/rehype-remark/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/rehype-rewrite": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/rehype-rewrite/-/rehype-rewrite-3.0.6.tgz", - "integrity": "sha512-REDTNCvsKcAazy8IQWzKp66AhSUDSOIKssSCqNqCcT9sN7JCwAAm3mWGTUdUzq80ABuy8d0D6RBwbnewu1aY1g==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/rehype-rewrite/-/rehype-rewrite-4.0.2.tgz", + "integrity": "sha512-rjLJ3z6fIV11phwCqHp/KRo8xuUCO8o9bFJCNw5o6O2wlLk6g8r323aRswdGBQwfXPFYeSuZdAjp4tzo6RGqEg==", "dependencies": { - "hast-util-select": "~5.0.1", - "unified": "~10.1.1", - "unist-util-visit": "~4.1.0" + "hast-util-select": "^6.0.0", + "unified": "^11.0.3", + "unist-util-visit": "^5.0.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=16.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" } }, "node_modules/rehype-sanitize": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/rehype-sanitize/-/rehype-sanitize-5.0.1.tgz", - "integrity": "sha512-da/jIOjq8eYt/1r9GN6GwxIR3gde7OZ+WV8pheu1tL8K0D9KxM2AyMh+UEfke+FfdM3PvGHeYJU0Td5OWa7L5A==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/rehype-sanitize/-/rehype-sanitize-6.0.0.tgz", + "integrity": "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==", "dependencies": { - "@types/hast": "^2.0.0", - "hast-util-sanitize": "^4.0.0", - "unified": "^10.0.0" + "@types/hast": "^3.0.0", + "hast-util-sanitize": "^5.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, + "node_modules/rehype-sanitize/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/rehype-stringify": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-9.0.3.tgz", - "integrity": "sha512-kWiZ1bgyWlgOxpqD5HnxShKAdXtb2IUljn3hQAhySeak6IOQPPt6DeGnsIh4ixm7yKJWzm8TXFuC/lPfcWHJqw==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.0.tgz", + "integrity": "sha512-1TX1i048LooI9QoecrXy7nGFFbFSufxVRAfc6Y9YMRAi56l+oB0zP51mLSV312uRuvVLPV1opSlJmslozR1XHQ==", "dependencies": { - "@types/hast": "^2.0.0", - "hast-util-to-html": "^8.0.0", - "unified": "^10.0.0" + "@types/hast": "^3.0.0", + "hast-util-to-html": "^9.0.0", + "unified": "^11.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, + "node_modules/rehype-stringify/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/relay-runtime": { "version": "12.0.0", "resolved": "https://registry.npmjs.org/relay-runtime/-/relay-runtime-12.0.0.tgz", @@ -20618,13 +20398,13 @@ } }, "node_modules/remark-breaks": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/remark-breaks/-/remark-breaks-3.0.3.tgz", - "integrity": "sha512-C7VkvcUp1TPUc2eAYzsPdaUh8Xj4FSbQnYA5A9f80diApLZscTDeG7efiWP65W8hV2sEy3JuGVU0i6qr5D8Hug==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-breaks/-/remark-breaks-4.0.0.tgz", + "integrity": "sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==", "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-newline-to-break": "^1.0.0", - "unified": "^10.0.0" + "@types/mdast": "^4.0.0", + "mdast-util-newline-to-break": "^2.0.0", + "unified": "^11.0.0" }, "funding": { "type": "opencollective", @@ -20632,14 +20412,14 @@ } }, "node_modules/remark-directive": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/remark-directive/-/remark-directive-2.0.1.tgz", - "integrity": "sha512-oosbsUAkU/qmUE78anLaJePnPis4ihsE7Agp0T/oqTzvTea8pOiaYEtfInU/+xMOVTS9PN5AhGOiaIVe4GD8gw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/remark-directive/-/remark-directive-3.0.0.tgz", + "integrity": "sha512-l1UyWJ6Eg1VPU7Hm/9tt0zKtReJQNOA4+iDMAxTyZNWnJnFlbS/7zhiel/rogTLQ2vMYwDzSJa4BiVNqGlqIMA==", "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-directive": "^2.0.0", - "micromark-extension-directive": "^2.0.0", - "unified": "^10.0.0" + "@types/mdast": "^4.0.0", + "mdast-util-directive": "^3.0.0", + "micromark-extension-directive": "^3.0.0", + "unified": "^11.0.0" }, "funding": { "type": "opencollective", @@ -20659,13 +20439,14 @@ } }, "node_modules/remark-parse": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.2.tgz", - "integrity": "sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-from-markdown": "^1.0.0", - "unified": "^10.0.0" + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" }, "funding": { "type": "opencollective", @@ -20673,28 +20454,37 @@ } }, "node_modules/remark-rehype": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-10.1.0.tgz", - "integrity": "sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.0.tgz", + "integrity": "sha512-z3tJrAs2kIs1AqIIy6pzHmAHlF1hWQ+OdY4/hv+Wxe35EhyLKcajL33iUEn3ScxtFox9nUvRufR/Zre8Q08H/g==", "dependencies": { - "@types/hast": "^2.0.0", - "@types/mdast": "^3.0.0", - "mdast-util-to-hast": "^12.1.0", - "unified": "^10.0.0" + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, + "node_modules/remark-rehype/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/remark-stringify": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-10.0.3.tgz", - "integrity": "sha512-koyOzCMYoUHudypbj4XpnAKFbkddRMYZHwghnxd7ue5210WzGw6kOBwauJTRUMq16jsovXx8dYNvSSWP89kZ3A==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-to-markdown": "^1.0.0", - "unified": "^10.0.0" + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" }, "funding": { "type": "opencollective", @@ -21018,17 +20808,6 @@ "tslib": "^2.1.0" } }, - "node_modules/sade": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", - "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", - "dependencies": { - "mri": "^1.1.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -21827,6 +21606,7 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, "dependencies": { "has-flag": "^3.0.0" }, @@ -21940,14 +21720,6 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, - "node_modules/throttle-debounce": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz", - "integrity": "sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==", - "engines": { - "node": ">=10" - } - }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -21991,7 +21763,6 @@ "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, "dependencies": { "os-tmpdir": "~1.0.2" }, @@ -22009,6 +21780,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, "engines": { "node": ">=4" } @@ -22084,9 +21856,9 @@ "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" }, "node_modules/trough": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/trough/-/trough-2.1.0.tgz", - "integrity": "sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -22411,23 +22183,28 @@ "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" }, "node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.4.tgz", + "integrity": "sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ==", "dependencies": { - "@types/unist": "^2.0.0", + "@types/unist": "^3.0.0", "bail": "^2.0.0", + "devlop": "^1.0.0", "extend": "^3.0.0", - "is-buffer": "^2.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", - "vfile": "^5.0.0" + "vfile": "^6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, + "node_modules/unified/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, "node_modules/unified/node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -22440,39 +22217,40 @@ } }, "node_modules/unist-util-find-after": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-4.0.1.tgz", - "integrity": "sha512-QO/PuPMm2ERxC6vFXEPtmAutOopy5PknD+Oq64gGwxKtk4xwo9Z97t9Av1obPmGU0IyTa6EKYUfTrK2QJS3Ozw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/unist-util-generated": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.1.tgz", - "integrity": "sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } + "node_modules/unist-util-find-after/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" }, "node_modules/unist-util-is": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", - "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", "dependencies": { - "@types/unist": "^2.0.0" + "@types/unist": "^3.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-is/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, "node_modules/unist-util-map": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/unist-util-map/-/unist-util-map-3.1.3.tgz", @@ -22486,37 +22264,47 @@ } }, "node_modules/unist-util-position": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", - "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", "dependencies": { - "@types/unist": "^2.0.0" + "@types/unist": "^3.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-position/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, "node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", "dependencies": { - "@types/unist": "^2.0.0" + "@types/unist": "^3.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-stringify-position/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, "node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" }, "funding": { "type": "opencollective", @@ -22524,18 +22312,28 @@ } }, "node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-visit-parents/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "node_modules/unist-util-visit/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, "node_modules/unixify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unixify/-/unixify-1.0.0.tgz", @@ -22572,6 +22370,7 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "dev": true, "funding": [ { "type": "opencollective", @@ -22698,39 +22497,6 @@ "uuid": "dist/bin/uuid" } }, - "node_modules/uvu": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz", - "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==", - "dependencies": { - "dequal": "^2.0.0", - "diff": "^5.0.0", - "kleur": "^4.0.3", - "sade": "^1.7.3" - }, - "bin": { - "uvu": "bin.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/uvu/node_modules/diff": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", - "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/uvu/node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "engines": { - "node": ">=6" - } - }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -22822,14 +22588,13 @@ } }, "node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz", + "integrity": "sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==", "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" }, "funding": { "type": "opencollective", @@ -22837,31 +22602,46 @@ } }, "node_modules/vfile-location": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-4.1.0.tgz", - "integrity": "sha512-YF23YMyASIIJXpktBa4vIGLJ5Gs88UB/XePgqPmTa7cDA+JeO3yclbpheQYCHjVHBn/yePzrXuygIL+xbvRYHw==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.2.tgz", + "integrity": "sha512-NXPYyxyBSH7zB5U6+3uDdd6Nybz6o6/od9rk8bp9H8GR3L+cm/fC0uUTbqBmUTnMCUDslAGBOIKNfvvb+gGlDg==", "dependencies": { - "@types/unist": "^2.0.0", - "vfile": "^5.0.0" + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, + "node_modules/vfile-location/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, "node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, + "node_modules/vfile-message/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "node_modules/vfile/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, "node_modules/viem": { "version": "1.21.1", "resolved": "https://registry.npmjs.org/viem/-/viem-1.21.1.tgz", @@ -23426,16 +23206,6 @@ "decamelize": "^1.2.0" } }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", @@ -23458,9 +23228,9 @@ } }, "node_modules/zeed-dom": { - "version": "0.10.5", - "resolved": "https://registry.npmjs.org/zeed-dom/-/zeed-dom-0.10.5.tgz", - "integrity": "sha512-blCwZ4ACAsbGh7tNy8eG+2Ri1Mj9SJmxcYmjw0ijC5b4Oyfm/F2RDFTuFXIiiJaOq3xEcPBhgQpI1zIIfS4v4Q==", + "version": "0.12.10", + "resolved": "https://registry.npmjs.org/zeed-dom/-/zeed-dom-0.12.10.tgz", + "integrity": "sha512-bFao9LxLVC8BOfLS9OGv/JHVDQ+JrR+opn1ZAvcFceGdpAviUurGxE5RQaLH+fGJyOmmpv71OIr92QdcSwLPBg==", "dependencies": { "css-what": "^6.1.0" }, @@ -23492,6 +23262,7 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, "requires": { "@jridgewell/gen-mapping": "^0.3.0", "@jridgewell/trace-mapping": "^0.3.9" @@ -23927,22 +23698,26 @@ } }, "@babel/code-frame": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", - "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "dev": true, "requires": { - "@babel/highlight": "^7.22.5" + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" } }, "@babel/compat-data": { "version": "7.22.9", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.9.tgz", - "integrity": "sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==" + "integrity": "sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==", + "dev": true }, "@babel/core": { "version": "7.22.9", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.9.tgz", "integrity": "sha512-G2EgeufBcYw27U4hhoIwFcgc1XU7TlXJ3mv04oOv1WCuo900U/anZSPzEqNjwdjgffkk2Gs0AN0dW1CKVLcG7w==", + "dev": true, "requires": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.22.5", @@ -23964,21 +23739,24 @@ "convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true }, "semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true } } }, "@babel/generator": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.9.tgz", - "integrity": "sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "dev": true, "requires": { - "@babel/types": "^7.22.5", + "@babel/types": "^7.23.6", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -23987,17 +23765,20 @@ "@jridgewell/resolve-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==" + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true }, "@jridgewell/sourcemap-codec": { "version": "1.4.14", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true }, "@jridgewell/trace-mapping": { "version": "0.3.18", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dev": true, "requires": { "@jridgewell/resolve-uri": "3.1.0", "@jridgewell/sourcemap-codec": "1.4.14" @@ -24018,6 +23799,7 @@ "version": "7.22.9", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.9.tgz", "integrity": "sha512-7qYrNM6HjpnPHJbopxmb8hSPoZ0gsX8IvUS32JGVoy+pU9e5N0nLr1VjJoR6kA4d9dmGLxNYOjeB8sUDal2WMw==", + "dev": true, "requires": { "@babel/compat-data": "^7.22.9", "@babel/helper-validator-option": "^7.22.5", @@ -24030,6 +23812,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, "requires": { "yallist": "^3.0.2" } @@ -24037,12 +23820,14 @@ "semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true }, "yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true } } }, @@ -24072,23 +23857,26 @@ } }, "@babel/helper-environment-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", - "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==" + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true }, "@babel/helper-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", - "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, "requires": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" } }, "@babel/helper-hoist-variables": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, "requires": { "@babel/types": "^7.22.5" } @@ -24106,6 +23894,7 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "dev": true, "requires": { "@babel/types": "^7.22.5" } @@ -24114,6 +23903,7 @@ "version": "7.22.9", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz", "integrity": "sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==", + "dev": true, "requires": { "@babel/helper-environment-visitor": "^7.22.5", "@babel/helper-module-imports": "^7.22.5", @@ -24134,7 +23924,8 @@ "@babel/helper-plugin-utils": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==" + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true }, "@babel/helper-replace-supers": { "version": "7.22.9", @@ -24151,6 +23942,7 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, "requires": { "@babel/types": "^7.22.5" } @@ -24168,29 +23960,34 @@ "version": "7.22.6", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, "requires": { "@babel/types": "^7.22.5" } }, "@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==" + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "dev": true }, "@babel/helper-validator-identifier": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", - "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==" + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true }, "@babel/helper-validator-option": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", - "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==" + "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", + "dev": true }, "@babel/helpers": { "version": "7.22.6", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.6.tgz", "integrity": "sha512-YjDs6y/fVOYFV8hAf1rxd1QvR9wJe1pDBZ2AREKq/SDayfPzgk0PBnVuTCE5X1acEpMMNOVUqoe+OwiZGJ+OaA==", + "dev": true, "requires": { "@babel/template": "^7.22.5", "@babel/traverse": "^7.22.6", @@ -24198,19 +23995,20 @@ } }, "@babel/highlight": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", - "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.22.5", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.22.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.7.tgz", - "integrity": "sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==" + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", + "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==" }, "@babel/plugin-proposal-class-properties": { "version": "7.18.6", @@ -24222,15 +24020,6 @@ "@babel/helper-plugin-utils": "^7.18.6" } }, - "@babel/plugin-proposal-export-namespace-from": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", - "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" - } - }, "@babel/plugin-proposal-object-rest-spread": { "version": "7.20.7", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", @@ -24271,22 +24060,6 @@ "@babel/helper-plugin-utils": "^7.12.13" } }, - "@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", - "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", - "requires": { - "@babel/helper-plugin-utils": "^7.8.3" - } - }, "@babel/plugin-syntax-flow": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.22.5.tgz", @@ -24519,6 +24292,7 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.5.tgz", "integrity": "sha512-B4pzOXj+ONRmuaQTg05b3y/4DuFz3WcCNAXPLb2Q0GT0TrGKGxNKV4jwsXts+StaM0LQczZbOpj8o1DLPDJIiA==", + "dev": true, "requires": { "@babel/helper-module-transforms": "^7.22.5", "@babel/helper-plugin-utils": "^7.22.5", @@ -24607,44 +24381,48 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.5.tgz", "integrity": "sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==", + "dev": true, "requires": { "regenerator-runtime": "^0.13.11" } }, "@babel/template": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", - "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "dev": true, "requires": { - "@babel/code-frame": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" } }, "@babel/traverse": { - "version": "7.22.8", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.8.tgz", - "integrity": "sha512-y6LPR+wpM2I3qJrsheCTwhIinzkETbplIgPBbwvqPKc+uljeA5gP+3nP8irdYt1mjQaDnlIcG+dw8OjAco4GXw==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.7.tgz", + "integrity": "sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==", + "dev": true, "requires": { - "@babel/code-frame": "^7.22.5", - "@babel/generator": "^7.22.7", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.22.7", - "@babel/types": "^7.22.5", - "debug": "^4.1.0", + "@babel/parser": "^7.23.6", + "@babel/types": "^7.23.6", + "debug": "^4.3.1", "globals": "^11.1.0" } }, "@babel/types": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", - "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "dev": true, "requires": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" } }, @@ -26125,9 +25903,9 @@ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, "protobufjs": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.3.tgz", - "integrity": "sha512-TtpvOqwB5Gdz/PQmOjgsrGH1nHjAQVCN7JG4A6r1sXRWESL5rNMAiRcBQlCAdKxZcAbstExQePYG8xof/JVRgg==", + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.6.tgz", + "integrity": "sha512-dgJaEDDL6x8ASUZ1YqWciTRrdOuYNzoOf27oHNfdyvKqHr5i0FV7FSLU+aIeFjyFgVxrpTOtQUi0BLLBymZaBw==", "requires": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", @@ -26227,6 +26005,11 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "@ioredis/as-callback": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@ioredis/as-callback/-/as-callback-3.0.0.tgz", + "integrity": "sha512-Kqv1rZ3WbgOrS+hgzJ5xG5WQuhvzzSTRYvNeyPMLOAM78MHSnuKI20JeJGbpuAt//LCuP0vsexZcorqW7kWhJg==" + }, "@ioredis/commands": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", @@ -26933,6 +26716,7 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, "requires": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -26942,22 +26726,26 @@ "@jridgewell/resolve-uri": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==" + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true }, "@jridgewell/set-array": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==" + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true }, "@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true }, "@jridgewell/trace-mapping": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, "requires": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -26979,50 +26767,6 @@ "ioredis": "^5.3.2" } }, - "@linaria/core": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/@linaria/core/-/core-4.2.9.tgz", - "integrity": "sha512-ELcu37VNVOT/PU0L6WDIN+aLzNFyJrqoBYT0CucGOCAmODbojUMCv8oJYRbWzA3N34w1t199dN4UFdfRWFG2rg==", - "requires": { - "@linaria/logger": "^4.0.0", - "@linaria/tags": "^4.3.4", - "@linaria/utils": "^4.3.3" - } - }, - "@linaria/logger": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@linaria/logger/-/logger-4.0.0.tgz", - "integrity": "sha512-YnBq0JlDWMEkTOK+tMo5yEVR0f5V//6qMLToGcLhTyM9g9i+IDFn51Z+5q2hLk7RdG4NBPgbcCXYi2w4RKsPeg==", - "requires": { - "debug": "^4.1.1", - "picocolors": "^1.0.0" - } - }, - "@linaria/tags": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/@linaria/tags/-/tags-4.3.5.tgz", - "integrity": "sha512-PgaIi8Vv89YOjc6rpKL/uPg2w4k0rAwAYxcqeXqzKqsEAste5rgB8xp1/KUOG0oAOkPd3MRL6Duj+m0ZwJ3g+g==", - "requires": { - "@babel/generator": "^7.20.4", - "@linaria/logger": "^4.0.0", - "@linaria/utils": "^4.3.4" - } - }, - "@linaria/utils": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/@linaria/utils/-/utils-4.3.4.tgz", - "integrity": "sha512-vt6WJG54n+KANaqxOfzIIU7aSfFHEWFbnGLsgxL7nASHqO0zezrNA2y2Rrp80zSeTW+wSpbmDM4uJyC9UW1qoA==", - "requires": { - "@babel/core": "^7.20.2", - "@babel/plugin-proposal-export-namespace-from": "^7.18.9", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-transform-modules-commonjs": "^7.19.6", - "@babel/traverse": "^7.20.1", - "@babel/types": "^7.20.2", - "@linaria/logger": "^4.0.0", - "babel-merge": "^3.0.0" - } - }, "@mapbox/node-pre-gyp": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz", @@ -27122,10 +26866,11 @@ } }, "@matters/ipns-site-generator": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@matters/ipns-site-generator/-/ipns-site-generator-0.1.3.tgz", - "integrity": "sha512-SfgfMTYhQGJr75LhpC9K9xxd+WcyJQPKSKXS39Qctot7zLZjbmySHm7gHlLMlcgjNVENLXFS+xYHHwWIJvJzGA==", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@matters/ipns-site-generator/-/ipns-site-generator-0.1.6.tgz", + "integrity": "sha512-WcJbvRNAFfmi3mWMywSVfFJu55f5sVhGP0TpRuKtc2eTSZ9aNqewhBgXJ8jgAepT+ohGXMxDYiDRMysrvkg7rg==", "requires": { + "@matters/slugify": "^0.7.3", "@peculiar/webcrypto": "^1.1.6", "cheerio": "^1.0.0-rc.9", "isomorphic-fetch": "^3.0.0", @@ -27134,79 +26879,51 @@ } }, "@matters/matters-editor": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@matters/matters-editor/-/matters-editor-0.2.2.tgz", - "integrity": "sha512-tFECZghLH4yvtFz+OM2jqdRQdVMWrfxYsHtEzoXGf8Vypz82YYUbWT62wKN6hdNS7XF94QxPk8zrJfO0vRi7KQ==", - "requires": { - "@tiptap/core": "2.1.0-rc.9", - "@tiptap/extension-blockquote": "2.1.0-rc.9", - "@tiptap/extension-bullet-list": "2.1.0-rc.9", - "@tiptap/extension-code": "2.1.0-rc.9", - "@tiptap/extension-code-block": "2.1.0-rc.9", - "@tiptap/extension-document": "2.1.0-rc.9", - "@tiptap/extension-gapcursor": "2.1.0-rc.9", - "@tiptap/extension-hard-break": "2.1.0-rc.9", - "@tiptap/extension-heading": "2.1.0-rc.9", - "@tiptap/extension-history": "2.1.0-rc.9", - "@tiptap/extension-horizontal-rule": "2.1.0-rc.9", - "@tiptap/extension-list-item": "2.1.0-rc.9", - "@tiptap/extension-ordered-list": "2.1.0-rc.9", - "@tiptap/extension-paragraph": "2.1.0-rc.9", - "@tiptap/extension-placeholder": "2.1.0-rc.9", - "@tiptap/extension-strike": "2.1.0-rc.9", - "@tiptap/extension-text": "2.1.0-rc.9", - "@tiptap/pm": "2.1.0-rc.9", - "@tiptap/react": "2.1.0-rc.9", - "@tiptap/suggestion": "2.1.0-rc.9", - "hast-util-to-html": "^8.0.4", - "linkifyjs": "^4.1.1", - "mdast-util-gfm-strikethrough": "^1.0.3", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@matters/matters-editor/-/matters-editor-0.2.4.tgz", + "integrity": "sha512-dmGVnmdgpSekfxEyj1Or5yoNEzky395ZMPFUckyVYILKGvlWFuGnbAWTzEc5vFUtHcFPKqfPZt0r/ocnWyKevg==", + "requires": { + "@tiptap/core": "2.2.4", + "@tiptap/extension-blockquote": "2.2.4", + "@tiptap/extension-bullet-list": "2.2.4", + "@tiptap/extension-code": "2.2.4", + "@tiptap/extension-code-block": "2.2.4", + "@tiptap/extension-document": "2.2.4", + "@tiptap/extension-gapcursor": "2.2.4", + "@tiptap/extension-hard-break": "2.2.4", + "@tiptap/extension-heading": "2.2.4", + "@tiptap/extension-history": "2.2.4", + "@tiptap/extension-horizontal-rule": "2.2.4", + "@tiptap/extension-list-item": "2.2.4", + "@tiptap/extension-ordered-list": "2.2.4", + "@tiptap/extension-paragraph": "2.2.4", + "@tiptap/extension-placeholder": "2.2.4", + "@tiptap/extension-strike": "2.2.4", + "@tiptap/extension-text": "2.2.4", + "@tiptap/pm": "2.2.4", + "@tiptap/react": "2.2.4", + "@tiptap/suggestion": "2.2.4", + "hast-util-to-html": "^9.0.0", + "linkifyjs": "^4.1.3", + "mdast-util-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", - "rehype-format": "^4.0.1", - "rehype-parse": "^8.0.4", - "rehype-raw": "^6.1.1", - "rehype-remark": "^9.1.2", - "rehype-rewrite": "^3.0.6", - "rehype-sanitize": "^5.0.1", - "rehype-stringify": "^9.0.3", - "remark-breaks": "^3.0.3", - "remark-directive": "^2.0.1", + "rehype-external-links": "^3.0.0", + "rehype-format": "^5.0.0", + "rehype-parse": "^9.0.0", + "rehype-raw": "^7.0.0", + "rehype-remark": "^10.0.0", + "rehype-rewrite": "^4.0.2", + "rehype-sanitize": "^6.0.0", + "rehype-stringify": "^10.0.0", + "remark-breaks": "^4.0.0", + "remark-directive": "^3.0.0", "remark-directive-rehype": "^0.4.2", - "remark-parse": "^10.0.2", - "remark-rehype": "^10.1.0", - "remark-stringify": "^10.0.3", - "unified": "^10.1.2", - "zeed-dom": "^0.10.5" - }, - "dependencies": { - "micromark-util-chunked": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.0.tgz", - "integrity": "sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg==", - "requires": { - "micromark-util-symbol": "^2.0.0" - } - }, - "micromark-util-combine-extensions": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.0.tgz", - "integrity": "sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ==", - "requires": { - "micromark-util-chunked": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "micromark-util-symbol": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", - "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==" - }, - "micromark-util-types": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.0.tgz", - "integrity": "sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==" - } + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.4", + "zeed-dom": "^0.12.10" } }, "@matters/passport-likecoin": { @@ -27403,49 +27120,9 @@ "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" }, "@remirror/core-constants": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-2.0.1.tgz", - "integrity": "sha512-ZR4aihtnnT9lMbhh5DEbsriJRlukRXmLZe7HmM+6ufJNNUDoazc75UX26xbgQlNUqgAqMcUdGFAnPc1JwgAdLQ==", - "requires": { - "@babel/runtime": "^7.21.0" - } - }, - "@remirror/core-helpers": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@remirror/core-helpers/-/core-helpers-2.0.3.tgz", - "integrity": "sha512-LqIPF4stGG69l9qu/FFicv9d9B+YaItzgDMC5A0CEvDQfKkGD3BfabLmfpnuWbsc06oKGdTduilgWcALLZoYLg==", - "requires": { - "@babel/runtime": "^7.21.0", - "@linaria/core": "4.2.9", - "@remirror/core-constants": "^2.0.1", - "@remirror/types": "^1.0.1", - "@types/object.omit": "^3.0.0", - "@types/object.pick": "^1.3.2", - "@types/throttle-debounce": "^2.1.0", - "case-anything": "^2.1.10", - "dash-get": "^1.0.2", - "deepmerge": "^4.3.1", - "fast-deep-equal": "^3.1.3", - "make-error": "^1.3.6", - "object.omit": "^3.0.0", - "object.pick": "^1.3.0", - "throttle-debounce": "^3.0.1" - } - }, - "@remirror/types": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@remirror/types/-/types-1.0.1.tgz", - "integrity": "sha512-VlZQxwGnt1jtQ18D6JqdIF+uFZo525WEqrfp9BOc3COPpK4+AWCgdnAWL+ho6imWcoINlGjR/+3b6y5C1vBVEA==", - "requires": { - "type-fest": "^2.19.0" - }, - "dependencies": { - "type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==" - } - } + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-2.0.2.tgz", + "integrity": "sha512-dyHY+sMF0ihPus3O27ODd4+agdHMEmuRdyiZJ2CCWjPV5UFmn17ZbElvk6WOGVE4rdCJKZQCrPV2BcikOMLUGQ==" }, "@repeaterjs/repeater": { "version": "3.0.4", @@ -27685,144 +27362,144 @@ } }, "@tiptap/core": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.1.0-rc.9.tgz", - "integrity": "sha512-wY4kq3V4cge/c37K5Fh9vGdFJsq/SISrWCfL3A+m1BlQLXYViVyEx5IV3setgcTdh3q+cTUapBRRTDpmUpj4KQ==" + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.2.4.tgz", + "integrity": "sha512-cRrI8IlLIhCE1hacBQzXIC8dsRvGq6a4lYWQK/BaHuZg21CG7szp3Vd8Ix+ra1f5v0xPOT+Hy+QFNQooRMKMCw==" }, "@tiptap/extension-blockquote": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-2.1.0-rc.9.tgz", - "integrity": "sha512-gQPOY8ZYlA2NwKCv5s4kByON1x7ayXZVGHo7+7xHTKJyTd4J+s3Suv7+IJZJFbkkqRjNlUPTnFcB0ZnUQTvsWg==" + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-2.2.4.tgz", + "integrity": "sha512-FrfPnn0VgVrUwWLwja1afX99JGLp6PE9ThVcmri+tLwUZQvTTVcCvHoCdOakav3/nge1+aV4iE3tQdyq1tWI9Q==" }, "@tiptap/extension-bubble-menu": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.1.0-rc.9.tgz", - "integrity": "sha512-XJ0CO5XVoX/8YXl3RSDy+FtRfzKXVqB48/gBij6a2+vVenJLtSNvJeHVB/1pLJGJoiYOGj2UrbS1MDhEsaqsKQ==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.2.4.tgz", + "integrity": "sha512-Nx1fS9jcFlhxaTDYlnayz2UulhK6CMaePc36+7PQIVI+u20RhgTCRNr25zKNemvsiM0RPZZVUjlHkxC0l5as1Q==", "requires": { "tippy.js": "^6.3.7" } }, "@tiptap/extension-bullet-list": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.1.0-rc.9.tgz", - "integrity": "sha512-HtrCBiOmR26639zLNa4uL5fjv+cyFcfaR4hTJvkKVxjqIZ1bfr6zufFxe4awfzNDPl6iGyLlSu5+rBk5WrVAIg==" + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.2.4.tgz", + "integrity": "sha512-z/MPmW8bhRougMuorl6MAQBXeK4rhlP+jBWlNwT+CT8h5IkXqPnDbM1sZeagp2nYfVV6Yc4RWpzimqHHtGnYTA==" }, "@tiptap/extension-code": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.1.0-rc.9.tgz", - "integrity": "sha512-ZSyFeCfn1O1sZQPu9/76MdSlNuX28uOtjbA+8POZElEAf3UXKVpYXX/yy+3kt6x6qT7ei50FbueBS7ocs31vHQ==" + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.2.4.tgz", + "integrity": "sha512-JB4SJ2mUU/9qXFUf+K5K9szvovnN9AIcCb0f0UlcVBuddKHSqCl3wO3QJgYt44BfQTLMNuyzr+zVqfFd6BNt/g==" }, "@tiptap/extension-code-block": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-2.1.0-rc.9.tgz", - "integrity": "sha512-lkyGGUYEQnBDq/ceGDNBq69gNfROdA/WrvnoxNhEUVRwx0/hv8Enm+8mVbD98cm/VUbwyEb94wsea6aJoltXGA==" + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-2.2.4.tgz", + "integrity": "sha512-h6WV9TmaBEZmvqe1ezMR83DhCPUap6P2mSR5pwVk0WVq6rvZjfgU0iF3EetBJOeDgPlz7cNe2NMDfVb1nGTM/g==" }, "@tiptap/extension-document": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.1.0-rc.9.tgz", - "integrity": "sha512-vvAB2QvFsxBAkquLX0ijRLaYN0oZ8tXuEGEHZ70IDwiNjsAa1oHuXJUp+l/fBHSWvuG791/g02WIqGNjtdAOEw==" + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.2.4.tgz", + "integrity": "sha512-z+05xGK0OFoXV1GL+/8bzcZuWMdMA3+EKwk5c+iziG60VZcvGTF7jBRsZidlu9Oaj0cDwWHCeeo6L9SgSh6i2A==" }, "@tiptap/extension-floating-menu": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-2.1.0-rc.9.tgz", - "integrity": "sha512-+B813ZcYTH1G1TJnOJZgPCG/CENqUIke/9rThbw29vWiEO5jpag30bQjw9AZ08s5dFFV9xOz0rcx7ub4834X6g==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-2.2.4.tgz", + "integrity": "sha512-U25l7PEzOmlAPugNRl8t8lqyhQZS6W/+3f92+FdwW9qXju3i62iX/3OGCC3Gv+vybmQ4fbZmMjvl+VDfenNi3A==", "requires": { "tippy.js": "^6.3.7" } }, "@tiptap/extension-gapcursor": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.1.0-rc.9.tgz", - "integrity": "sha512-hwmoX6Aig3gK0APmzrYQR4YF1JTJ7vXVpVcTPta5rGHbWbEpepJZC3PyYzg1ay8tJAaZi5aTJE8tvL1oxquPhg==" + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.2.4.tgz", + "integrity": "sha512-Y6htT/RDSqkQ1UwG2Ia+rNVRvxrKPOs3RbqKHPaWr3vbFWwhHyKhMCvi/FqfI3d5pViVHOZQ7jhb5hT/a0BmNw==" }, "@tiptap/extension-hard-break": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.1.0-rc.9.tgz", - "integrity": "sha512-9eWs6JfSp8SmKiiVmHwzbJaJudIcw3sF3svPb7Nv0NPLZOGeFdAyk9SC0J9U5xo3SmVJPStGt22uCJMofXwltw==" + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.2.4.tgz", + "integrity": "sha512-FPvS57GcqHIeLbPKGJa3gnH30Xw+YB1PXXnAWG2MpnMtc2Vtj1l5xaYYBZB+ADdXLAlU0YMbKhFLQO4+pg1Isg==" }, "@tiptap/extension-heading": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-2.1.0-rc.9.tgz", - "integrity": "sha512-c7m9GbzikwfL02mV4sDbzqfeOczKt/XYGt7xmacJrXmScGAl7nrdeiaWIXRD8M3u6PNn+TwFtURwPTZLKCD8cQ==" + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-2.2.4.tgz", + "integrity": "sha512-gkq7Ns2FcrOCRq7Q+VRYt5saMt2R9g4REAtWy/jEevJ5UV5vA2AiGnYDmxwAkHutoYU0sAUkjqx37wE0wpamNw==" }, "@tiptap/extension-history": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.1.0-rc.9.tgz", - "integrity": "sha512-38TLb7Q6d2EFudzcD4MamRldbp0PfBhzrySU1RA+fseuiXKKLHj7828tcDiesEjUOlW5AO2YGgmO7ljGr0lo9A==" + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.2.4.tgz", + "integrity": "sha512-FDM32XYF5NU4mzh+fJ8w2CyUqv0l2Nl15sd6fOhQkVxSj8t57z+DUXc9ZR3zkH+1RAagYJo/2Gu3e99KpMr0tg==" }, "@tiptap/extension-horizontal-rule": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.1.0-rc.9.tgz", - "integrity": "sha512-WjnEW0yGL2uqiD7iYxsObFjVspWhDvr4/iVk+TpeFPhCvX8ZeXdy6j05NEa9QI/esdJ0t3e3g9XZkAMh7U1Ccg==" + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.2.4.tgz", + "integrity": "sha512-iCRHjFQQHApWg3R4fkKkJQhWEOdu1Fdc4YEAukdOXPSg3fg36IwjvsMXjt9SYBtVZ+iio3rORCZGXyMvgCH9uw==" }, "@tiptap/extension-list-item": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.1.0-rc.9.tgz", - "integrity": "sha512-vICjBG122blY92JXmjkg0SpTaWNxsybNDwXTxvMOMHMamStHb3bwbuhT0eYBFbLRkyNghYppi2CEjiNCYPHfNA==" + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.2.4.tgz", + "integrity": "sha512-lPLKGKsHpM9ClUa8n7GEUn8pG6HCYU0vFruIy3l2t6jZdHkrgBnYtVGMZ13K8UDnj/hlAlccxku0D0P4mA1Vrg==" }, "@tiptap/extension-ordered-list": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.1.0-rc.9.tgz", - "integrity": "sha512-lpSBeGvlRxUwqoEUtjbZBt2XfV6aSIPBcXapYNNbz6aZrjzBuq9PnHZ6cRp7r13b/5uJRuZY6zPsdWlwo0DjRg==" + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.2.4.tgz", + "integrity": "sha512-TpFy140O9Af1JciXt+xwqYUXxcJ6YG8zi/B5UDJujp+FH5sCmlYYBBnWxiFMhVaj6yEmA2eafu1qUkic/1X5Aw==" }, "@tiptap/extension-paragraph": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.1.0-rc.9.tgz", - "integrity": "sha512-I6owNBPw5slpy4PjBxuxb3/OBVNUgYHk7ncNTnz97HWbzpvu7hedb6k82ioVY5w+X6gHlSBnixJ6lEjwWwA4Mw==" + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.2.4.tgz", + "integrity": "sha512-m1KwyvTNJxsq7StbspbcOhxO4Wk4YpElDbqOouWi+H4c8azdpI5Pn96ZqhFeE9bSyjByg6OcB/wqoJsLbeFWdQ==" }, "@tiptap/extension-placeholder": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-2.1.0-rc.9.tgz", - "integrity": "sha512-/CyjlpJpzmD0Q7zhmK8m71KAq6s5O7P+x8HMrGLkvR3igtuRLMxqrVPPivw7RqMJuha0OqRhAnhnOu+O8d/uSg==" + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-2.2.4.tgz", + "integrity": "sha512-UL4Fn9T33SoS7vdI3NnSxBJVeGUIgCIutgXZZ5J8CkcRoDIeS78z492z+6J+qGctHwTd0xUL5NzNJI82HfiTdg==" }, "@tiptap/extension-strike": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-2.1.0-rc.9.tgz", - "integrity": "sha512-BzsciaqtvgP3RXVGtFmzH/SFzBFz9HVLPEwPrl3w8h0XDBbDYBISvuENkEvsVkv6f7Gp+PrHpzWuZiAArgIvug==" + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-2.2.4.tgz", + "integrity": "sha512-/a2EwQgA+PpG17V2tVRspcrIY0SN3blwcgM7lxdW4aucGkqSKnf7+91dkhQEwCZ//o8kv9mBCyRoCUcGy6S5Xg==" }, "@tiptap/extension-text": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.1.0-rc.9.tgz", - "integrity": "sha512-Tgnnq9M/9bLhI/4NMGnxTp4Li/gLc4h0wNHeE/uIyfToKsle2MfolRFz86BfVFDMFB2fFPVEVBHaDgq2at1e8A==" + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.2.4.tgz", + "integrity": "sha512-NlKHMPnRJXB+0AGtDlU0P2Pg+SdesA2lMMd7JzDUgJgL7pX2jOb8eUqSeOjFKuSzFSqYfH6C3o6mQiNhuQMv+g==" }, "@tiptap/pm": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.1.0-rc.9.tgz", - "integrity": "sha512-ORtAcmEQ2MKmzVgi8Et2iRpNcaIJnbbiiKSD2PsCJAU2pt2t5vC3+417Gce8wS4Vxo15RmtiT6ib481roAT0OA==", - "requires": { - "prosemirror-changeset": "^2.2.0", - "prosemirror-collab": "^1.3.0", - "prosemirror-commands": "^1.3.1", - "prosemirror-dropcursor": "^1.5.0", - "prosemirror-gapcursor": "^1.3.1", - "prosemirror-history": "^1.3.0", - "prosemirror-inputrules": "^1.2.0", - "prosemirror-keymap": "^1.2.0", - "prosemirror-markdown": "^1.10.1", - "prosemirror-menu": "^1.2.1", - "prosemirror-model": "^1.18.1", - "prosemirror-schema-basic": "^1.2.0", - "prosemirror-schema-list": "^1.2.2", - "prosemirror-state": "^1.4.1", - "prosemirror-tables": "^1.3.0", - "prosemirror-trailing-node": "^2.0.2", - "prosemirror-transform": "^1.7.0", - "prosemirror-view": "^1.28.2" + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.2.4.tgz", + "integrity": "sha512-Po0klR165zgtinhVp1nwMubjyKx6gAY9kH3IzcniYLCkqhPgiqnAcCr61TBpp4hfK8YURBS4ihvCB1dyfCyY8A==", + "requires": { + "prosemirror-changeset": "^2.2.1", + "prosemirror-collab": "^1.3.1", + "prosemirror-commands": "^1.5.2", + "prosemirror-dropcursor": "^1.8.1", + "prosemirror-gapcursor": "^1.3.2", + "prosemirror-history": "^1.3.2", + "prosemirror-inputrules": "^1.3.0", + "prosemirror-keymap": "^1.2.2", + "prosemirror-markdown": "^1.12.0", + "prosemirror-menu": "^1.2.4", + "prosemirror-model": "^1.19.4", + "prosemirror-schema-basic": "^1.2.2", + "prosemirror-schema-list": "^1.3.0", + "prosemirror-state": "^1.4.3", + "prosemirror-tables": "^1.3.5", + "prosemirror-trailing-node": "^2.0.7", + "prosemirror-transform": "^1.8.0", + "prosemirror-view": "^1.32.7" } }, "@tiptap/react": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-2.1.0-rc.9.tgz", - "integrity": "sha512-VyThuIA/bhxxvcSUitgL4gBbflOZrtKCEeLqjjtd/wrxKkGEKIMlxzpVBTG3zMsqSc7MIiu3RdetMcOw1pNquQ==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-2.2.4.tgz", + "integrity": "sha512-HkYmMZWcETPZn3KpzdDg/ns2TKeFh54TvtCEInA4ljYtWGLoZc/A+KaiEtMIgVs+Mo1XwrhuoNGjL9c0OK2HJw==", "requires": { - "@tiptap/extension-bubble-menu": "^2.1.0-rc.9", - "@tiptap/extension-floating-menu": "^2.1.0-rc.9" + "@tiptap/extension-bubble-menu": "^2.2.4", + "@tiptap/extension-floating-menu": "^2.2.4" } }, "@tiptap/suggestion": { - "version": "2.1.0-rc.9", - "resolved": "https://registry.npmjs.org/@tiptap/suggestion/-/suggestion-2.1.0-rc.9.tgz", - "integrity": "sha512-GN0ooEMzkUe/+iid9wpksZMRCcgDz8vImfZ7PACsu9hkhhgNd2oDYBxljWH4o89nXnOib/OCeIh88b32H+Xy2A==" + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tiptap/suggestion/-/suggestion-2.2.4.tgz", + "integrity": "sha512-g6HHsKM6K3asW+ZlwMYyLCRqCRaswoliZOQofY4iZt5ru5HNTSzm3YW4XSyW5RGXJIuc319yyrOFgtJ3Fyu5rQ==" }, "@tootallnate/once": { "version": "1.1.2", @@ -27993,11 +27670,6 @@ "@types/send": "*" } }, - "@types/extend": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/extend/-/extend-3.0.1.tgz", - "integrity": "sha512-R1g/VyKFFI2HLC1QGAeTtCBWCo6n75l41OnsVYNbmKG+kempOESaodf6BeJyUM3Q0rKa/NQcTHbB2+66lNnxLw==" - }, "@types/glob": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz", @@ -28060,6 +27732,16 @@ "integrity": "sha512-e+2rjEwK6KDaNOm5Aa9wNGgyS9oSZU/4pfSMMPYNOfjvFI0WVXm29+ITRFr6aKDvvKo7uU1jV68MW4ScsfDi7Q==", "dev": true }, + "@types/ioredis-mock": { + "version": "8.2.5", + "resolved": "https://registry.npmjs.org/@types/ioredis-mock/-/ioredis-mock-8.2.5.tgz", + "integrity": "sha512-cZyuwC9LGtg7s5G9/w6rpy3IOZ6F/hFR0pQlWYZESMo1xQUYbDpa6haqB4grTePjsGzcB/YLBFCjqRunK5wieg==", + "dev": true, + "requires": { + "@types/node": "*", + "ioredis": ">=5" + } + }, "@types/is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@types/is-stream/-/is-stream-1.1.0.tgz", @@ -28192,9 +27874,9 @@ } }, "@types/mdast": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz", - "integrity": "sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.3.tgz", + "integrity": "sha512-LsjtqsyF+d2/yFOYaN22dHZI1Cpwkrj+g06G8+qtUKlhovPW89YhqSnfKtMbkgmEtYpH2gydRNULd6y8mciAFg==", "requires": { "@types/unist": "*" } @@ -28284,16 +27966,6 @@ "@types/express": "*" } }, - "@types/object.omit": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/object.omit/-/object.omit-3.0.0.tgz", - "integrity": "sha512-I27IoPpH250TUzc9FzXd0P1BV/BMJuzqD3jOz98ehf9dQqGkxlq+hO1bIqZGWqCg5bVOy0g4AUVJtnxe0klDmw==" - }, - "@types/object.pick": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/object.pick/-/object.pick-1.3.2.tgz", - "integrity": "sha512-sn7L+qQ6RLPdXRoiaE7bZ/Ek+o4uICma/lBFPyJEKDTPTBP1W8u0c4baj3EiS4DiqLs+Hk+KUGvMVJtAw3ePJg==" - }, "@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -28301,11 +27973,6 @@ "dev": true, "optional": true }, - "@types/parse5": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-6.0.3.tgz", - "integrity": "sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==" - }, "@types/passport": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.12.tgz", @@ -28395,11 +28062,6 @@ "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "dev": true }, - "@types/throttle-debounce": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/throttle-debounce/-/throttle-debounce-2.1.0.tgz", - "integrity": "sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ==" - }, "@types/triple-beam": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.2.tgz", @@ -28446,16 +28108,6 @@ "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", "dev": true }, - "@types/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==", - "dev": true, - "optional": true, - "requires": { - "@types/node": "*" - } - }, "@typescript-eslint/eslint-plugin": { "version": "5.59.7", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.7.tgz", @@ -28606,6 +28258,11 @@ "eslint-visitor-keys": "^3.3.0" } }, + "@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" + }, "@whatwg-node/events": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/@whatwg-node/events/-/events-0.0.3.tgz", @@ -28762,6 +28419,7 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, "requires": { "color-convert": "^1.9.0" } @@ -28798,18 +28456,18 @@ "dev": true }, "are-we-there-yet": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", + "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", "requires": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" }, "dependencies": { "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -29083,22 +28741,6 @@ } } }, - "babel-merge": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/babel-merge/-/babel-merge-3.0.0.tgz", - "integrity": "sha512-eBOBtHnzt9xvnjpYNI5HmaPp/b2vMveE5XggzqHnQeHJ8mFIBrBv6WZEVIj5jJ2uwTItkqKo9gWzEEcBxEq0yw==", - "requires": { - "deepmerge": "^2.2.1", - "object.omit": "^3.0.0" - }, - "dependencies": { - "deepmerge": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", - "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==" - } - } - }, "babel-plugin-istanbul": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", @@ -29371,6 +29013,7 @@ "version": "4.21.9", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz", "integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==", + "dev": true, "requires": { "caniuse-lite": "^1.0.30001503", "electron-to-chromium": "^1.4.431", @@ -29406,12 +29049,6 @@ "isarray": "^1.0.0" } }, - "buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true - }, "buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -29512,7 +29149,8 @@ "caniuse-lite": { "version": "1.0.30001517", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001517.tgz", - "integrity": "sha512-Vdhm5S11DaFVLlyiKu4hiUTkpZu+y1KA/rZZqVQfOD5YdDT/eQKlkt7NaE0WGOFgX32diqt9MiP9CAiFeRklaA==" + "integrity": "sha512-Vdhm5S11DaFVLlyiKu4hiUTkpZu+y1KA/rZZqVQfOD5YdDT/eQKlkt7NaE0WGOFgX32diqt9MiP9CAiFeRklaA==", + "dev": true }, "capital-case": { "version": "1.0.4", @@ -29525,11 +29163,6 @@ "upper-case-first": "^2.0.2" } }, - "case-anything": { - "version": "2.1.13", - "resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.13.tgz", - "integrity": "sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==" - }, "catharsis": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", @@ -29552,6 +29185,7 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -29640,16 +29274,6 @@ "htmlparser2": "^8.0.1", "parse5": "^7.0.0", "parse5-htmlparser2-tree-adapter": "^7.0.0" - }, - "dependencies": { - "parse5": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", - "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", - "requires": { - "entities": "^4.4.0" - } - } } }, "cheerio-select": { @@ -29894,7 +29518,7 @@ "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==" }, "codecov": { "version": "3.8.3", @@ -30024,12 +29648,6 @@ "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", "dev": true }, - "commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", - "dev": true - }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -30094,9 +29712,9 @@ "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, "cors": { "version": "2.8.5", @@ -30201,9 +29819,9 @@ } }, "css-selector-parser": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-1.4.1.tgz", - "integrity": "sha512-HYPSb7y/Z7BNDCOrakL4raGO2zltZkbeXyAd6Tg9obzix6QhzxCotdBl6VT0Dv4vZfJGVz3WL/xaEI9Ly3ul0g==" + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-3.0.5.tgz", + "integrity": "sha512-3itoDFbKUNx1eKmVpYMFyqKX04Ww9osZ+dLgrk6GEv6KMVeXUhUnp4I5X+evw+u3ZxVU6RFXSSRxlTeMh8bA+g==" }, "css-what": { "version": "6.1.0", @@ -30255,11 +29873,6 @@ } } }, - "dash-get": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/dash-get/-/dash-get-1.0.2.tgz", - "integrity": "sha512-4FbVrHDwfOASx7uQVxeiCTo7ggSdYZbqs8lH+WU6ViypPlDbe9y6IP5VVUDQBv9DcnyaiPT5XT0UWHgJ64zLeQ==" - }, "dataloader": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.2.2.tgz", @@ -30401,7 +30014,7 @@ "detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==" }, "detect-newline": { "version": "3.1.0", @@ -30410,9 +30023,9 @@ "dev": true }, "devlop": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.0.0.tgz", - "integrity": "sha512-DNY7Ok32YUNiFjTw9sNVqUET5c2/cqbOdDxnsI6MkfQOvMcAULqPVqABm/An9IGVRP4ulHEvpo3/w2Potw3cfQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", "requires": { "dequal": "^2.0.0" } @@ -30564,7 +30177,8 @@ "electron-to-chromium": { "version": "1.4.477", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.477.tgz", - "integrity": "sha512-shUVy6Eawp33dFBFIoYbIwLHrX0IZ857AlH9ug2o4rvbWmpaCUdBpQ5Zw39HRrfzAFm4APJE9V+E2A/WB0YqJw==" + "integrity": "sha512-shUVy6Eawp33dFBFIoYbIwLHrX0IZ857AlH9ug2o4rvbWmpaCUdBpQ5Zw39HRrfzAFm4APJE9V+E2A/WB0YqJw==", + "dev": true }, "emittery": { "version": "0.13.1", @@ -31453,29 +31067,6 @@ "integrity": "sha512-FuoE1qtbJ4bBVvv94CC7s0oTnKUGvQs+Rjf1L2SJFfS+HTVVjhPFtehPdQ0JiGPqVNfSSZvL5yzHHQq2Z4WNhQ==", "dev": true }, - "extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "requires": { - "@types/yauzl": "^2.9.1", - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "dependencies": { - "get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - } - } - }, "fast-decode-uri-component": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", @@ -31591,20 +31182,33 @@ "integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==", "dev": true }, - "fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "requires": { - "pend": "~1.2.0" - } - }, "fecha": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" }, + "fengari": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/fengari/-/fengari-0.1.4.tgz", + "integrity": "sha512-6ujqUuiIYmcgkGz8MGAdERU57EIluGGPSUgGPTsco657EHa+srq0S3/YUl/r9kx1+D+d4rGfYObd+m8K22gB1g==", + "requires": { + "readline-sync": "^1.4.9", + "sprintf-js": "^1.1.1", + "tmp": "^0.0.33" + }, + "dependencies": { + "sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" + } + } + }, + "fengari-interop": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/fengari-interop/-/fengari-interop-0.1.3.tgz", + "integrity": "sha512-EtZ+oTu3kEwVJnoymFPBVLIbQcCoy9uWCVnMA6h3M/RqHkUBsLYp29+RRHf9rKr6GwjubWREU1O7RretFIXjHw==" + }, "figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -31666,17 +31270,6 @@ } } }, - "find-cache-dir": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", - "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", - "dev": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - } - }, "find-node-modules": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/find-node-modules/-/find-node-modules-2.1.2.tgz", @@ -31687,12 +31280,6 @@ "merge": "^2.1.0" } }, - "find-package-json": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/find-package-json/-/find-package-json-1.2.0.tgz", - "integrity": "sha512-+SOGcLGYDJHtyqHd87ysBhmaeQ95oWspDKnMXBrnQ9Eq4OkLNqejgoaD8xVWu6GPa0B6roa6KinCMEMcVeqONw==", - "dev": true - }, "find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", @@ -31754,9 +31341,9 @@ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" }, "follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", + "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==" }, "for-each": { "version": "0.3.3", @@ -31886,7 +31473,7 @@ "gauge": { "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "integrity": "sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==", "requires": { "aproba": "^1.0.3", "console-control-strings": "^1.0.0", @@ -31901,12 +31488,12 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==" }, "is-fullwidth-code-point": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", "requires": { "number-is-nan": "^1.0.0" } @@ -31914,7 +31501,7 @@ "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -31924,7 +31511,7 @@ "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", "requires": { "ansi-regex": "^2.0.0" } @@ -31961,7 +31548,8 @@ "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==" + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true }, "get-caller-file": { "version": "2.0.5", @@ -32084,7 +31672,8 @@ "globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true }, "globalthis": { "version": "1.0.3", @@ -32207,9 +31796,9 @@ "dev": true }, "graphql": { - "version": "16.6.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.6.0.tgz", - "integrity": "sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==" + "version": "16.8.1", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz", + "integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==" }, "graphql-config": { "version": "5.0.2", @@ -32478,7 +32067,8 @@ "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true }, "has-property-descriptors": { "version": "1.0.0", @@ -32513,49 +32103,149 @@ "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" }, "hast-util-embedded": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-embedded/-/hast-util-embedded-3.0.0.tgz", + "integrity": "sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA==", + "requires": { + "@types/hast": "^3.0.0", + "hast-util-is-element": "^3.0.0" + }, + "dependencies": { + "@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "requires": { + "@types/unist": "*" + } + } + } + }, + "hast-util-from-html": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/hast-util-embedded/-/hast-util-embedded-2.0.1.tgz", - "integrity": "sha512-QUdSOP1/o+/TxXtpPFXR2mUg2P+ySrmlX7QjwHZCXqMFyYk7YmcGSvqRW+4XgXAoHifdE1t2PwFaQK33TqVjSw==", + "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.1.tgz", + "integrity": "sha512-RXQBLMl9kjKVNkJTIO6bZyb2n+cUH8LFaSSzo82jiLT6Tfc+Pt7VQCS+/h3YwG4jaNE2TA2sdJisGWR+aJrp0g==", "requires": { - "hast-util-is-element": "^2.0.0" + "@types/hast": "^3.0.0", + "devlop": "^1.1.0", + "hast-util-from-parse5": "^8.0.0", + "parse5": "^7.0.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "dependencies": { + "@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "requires": { + "@types/unist": "*" + } + } } }, "hast-util-from-parse5": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-7.1.2.tgz", - "integrity": "sha512-Nz7FfPBuljzsN3tCQ4kCBKqdNhQE2l0Tn+X1ubgKBPRoiDIu1mL08Cfw4k7q71+Duyaw7DXDN+VTAp4Vh3oCOw==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.1.tgz", + "integrity": "sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ==", "requires": { - "@types/hast": "^2.0.0", - "@types/unist": "^2.0.0", - "hastscript": "^7.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^8.0.0", "property-information": "^6.0.0", - "vfile": "^5.0.0", - "vfile-location": "^4.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" + }, + "dependencies": { + "@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "requires": { + "@types/unist": "*" + } + }, + "@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "requires": { + "@types/hast": "^3.0.0" + } + }, + "hastscript": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-8.0.0.tgz", + "integrity": "sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==", + "requires": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0" + } + } } }, "hast-util-has-property": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/hast-util-has-property/-/hast-util-has-property-2.0.1.tgz", - "integrity": "sha512-X2+RwZIMTMKpXUzlotatPzWj8bspCymtXH3cfG3iQKV+wPF53Vgaqxi/eLqGck0wKq1kS9nvoB1wchbCPEL8sg==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-has-property/-/hast-util-has-property-3.0.0.tgz", + "integrity": "sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA==", + "requires": { + "@types/hast": "^3.0.0" + }, + "dependencies": { + "@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "requires": { + "@types/unist": "*" + } + } + } }, "hast-util-is-body-ok-link": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hast-util-is-body-ok-link/-/hast-util-is-body-ok-link-2.0.0.tgz", - "integrity": "sha512-S58hCexyKdD31vMsErvgLfflW6vYWo/ixRLPJTtkOvLld24vyI8vmYmkgLA5LG3la2ME7nm7dLGdm48gfLRBfw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-body-ok-link/-/hast-util-is-body-ok-link-3.0.0.tgz", + "integrity": "sha512-VFHY5bo2nY8HiV6nir2ynmEB1XkxzuUffhEGeVx7orbu/B1KaGyeGgMZldvMVx5xWrDlLLG/kQ6YkJAMkBEx0w==", "requires": { - "@types/hast": "^2.0.0", - "hast-util-has-property": "^2.0.0", - "hast-util-is-element": "^2.0.0" + "@types/hast": "^3.0.0" + }, + "dependencies": { + "@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "requires": { + "@types/unist": "*" + } + } } }, "hast-util-is-element": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-2.1.3.tgz", - "integrity": "sha512-O1bKah6mhgEq2WtVMk+Ta5K7pPMqsBBlmzysLdcwKVrqzZQ0CHqUPiIVspNhAG1rvxpvJjtGee17XfauZYKqVA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", "requires": { - "@types/hast": "^2.0.0", - "@types/unist": "^2.0.0" + "@types/hast": "^3.0.0" + }, + "dependencies": { + "@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "requires": { + "@types/unist": "*" + } + } } }, "hast-util-parse-selector": { @@ -32567,141 +32257,270 @@ } }, "hast-util-phrasing": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hast-util-phrasing/-/hast-util-phrasing-2.0.2.tgz", - "integrity": "sha512-yGkCfPkkfCyiLfK6KEl/orMDr/zgCnq/NaO9HfULx6/Zga5fso5eqQA5Ov/JZVqACygvw9shRYWgXNcG2ilo7w==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-phrasing/-/hast-util-phrasing-3.0.1.tgz", + "integrity": "sha512-6h60VfI3uBQUxHqTyMymMZnEbNl1XmEGtOxxKYL7stY2o601COo62AWAYBQR9lZbYXYSBoxag8UpPRXK+9fqSQ==", "requires": { - "@types/hast": "^2.0.0", - "hast-util-embedded": "^2.0.0", - "hast-util-has-property": "^2.0.0", - "hast-util-is-body-ok-link": "^2.0.0", - "hast-util-is-element": "^2.0.0" + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-has-property": "^3.0.0", + "hast-util-is-body-ok-link": "^3.0.0", + "hast-util-is-element": "^3.0.0" + }, + "dependencies": { + "@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "requires": { + "@types/unist": "*" + } + } } }, "hast-util-raw": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-7.2.3.tgz", - "integrity": "sha512-RujVQfVsOrxzPOPSzZFiwofMArbQke6DJjnFfceiEbFh7S05CbPt0cYN+A5YeD3pso0JQk6O1aHBnx9+Pm2uqg==", - "requires": { - "@types/hast": "^2.0.0", - "@types/parse5": "^6.0.0", - "hast-util-from-parse5": "^7.0.0", - "hast-util-to-parse5": "^7.0.0", - "html-void-elements": "^2.0.0", - "parse5": "^6.0.0", - "unist-util-position": "^4.0.0", - "unist-util-visit": "^4.0.0", - "vfile": "^5.0.0", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.0.2.tgz", + "integrity": "sha512-PldBy71wO9Uq1kyaMch9AHIghtQvIwxBUkv823pKmkTM3oV1JxtsTNYdevMxvUHqcnOAuO65JKU2+0NOxc2ksA==", + "requires": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" + }, + "dependencies": { + "@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "requires": { + "@types/unist": "*" + } + }, + "@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + } } }, "hast-util-sanitize": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hast-util-sanitize/-/hast-util-sanitize-4.1.0.tgz", - "integrity": "sha512-Hd9tU0ltknMGRDv+d6Ro/4XKzBqQnP/EZrpiTbpFYfXv/uOhWeKc+2uajcbEvAEH98VZd7eII2PiXm13RihnLw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/hast-util-sanitize/-/hast-util-sanitize-5.0.1.tgz", + "integrity": "sha512-IGrgWLuip4O2nq5CugXy4GI2V8kx4sFVy5Hd4vF7AR2gxS0N9s7nEAVUyeMtZKZvzrxVsHt73XdTsno1tClIkQ==", "requires": { - "@types/hast": "^2.0.0" + "@types/hast": "^3.0.0", + "@ungap/structured-clone": "^1.2.0", + "unist-util-position": "^5.0.0" + }, + "dependencies": { + "@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "requires": { + "@types/unist": "*" + } + } } }, "hast-util-select": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/hast-util-select/-/hast-util-select-5.0.5.tgz", - "integrity": "sha512-QQhWMhgTFRhCaQdgTKzZ5g31GLQ9qRb1hZtDPMqQaOhpLBziWcshUS0uCR5IJ0U1jrK/mxg35fmcq+Dp/Cy2Aw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/hast-util-select/-/hast-util-select-6.0.2.tgz", + "integrity": "sha512-hT/SD/d/Meu+iobvgkffo1QecV8WeKWxwsNMzcTJsKw1cKTQKSR/7ArJeURLNJF9HDjp9nVoORyNNJxrvBye8Q==", "requires": { - "@types/hast": "^2.0.0", - "@types/unist": "^2.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", "bcp-47-match": "^2.0.0", "comma-separated-tokens": "^2.0.0", - "css-selector-parser": "^1.0.0", + "css-selector-parser": "^3.0.0", + "devlop": "^1.0.0", "direction": "^2.0.0", - "hast-util-has-property": "^2.0.0", - "hast-util-to-string": "^2.0.0", - "hast-util-whitespace": "^2.0.0", + "hast-util-has-property": "^3.0.0", + "hast-util-to-string": "^3.0.0", + "hast-util-whitespace": "^3.0.0", "not": "^0.1.0", "nth-check": "^2.0.0", "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0", - "unist-util-visit": "^4.0.0", + "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" + }, + "dependencies": { + "@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "requires": { + "@types/unist": "*" + } + }, + "@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + } } }, "hast-util-to-html": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-8.0.4.tgz", - "integrity": "sha512-4tpQTUOr9BMjtYyNlt0P50mH7xj0Ks2xpo8M943Vykljf99HW6EzulIoJP1N3eKOSScEHzyzi9dm7/cn0RfGwA==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.0.tgz", + "integrity": "sha512-IVGhNgg7vANuUA2XKrT6sOIIPgaYZnmLx3l/CCOAK0PtgfoHrZwX7jCSYyFxHTrGmC6S9q8aQQekjp4JPZF+cw==", "requires": { - "@types/hast": "^2.0.0", - "@types/unist": "^2.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", - "hast-util-raw": "^7.0.0", - "hast-util-whitespace": "^2.0.0", - "html-void-elements": "^2.0.0", + "hast-util-raw": "^9.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" + }, + "dependencies": { + "@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "requires": { + "@types/unist": "*" + } + }, + "@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + } } }, "hast-util-to-mdast": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/hast-util-to-mdast/-/hast-util-to-mdast-8.4.1.tgz", - "integrity": "sha512-tfmBLASuCgyhCzpkTXM5kU8xeuS5jkMZ17BYm2YftGT5wvgc7uHXTZ/X8WfNd6F5NV/IGmrLsuahZ+jXQir4zQ==", - "requires": { - "@types/extend": "^3.0.0", - "@types/hast": "^2.0.0", - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "extend": "^3.0.0", - "hast-util-has-property": "^2.0.0", - "hast-util-is-element": "^2.0.0", - "hast-util-phrasing": "^2.0.0", - "hast-util-to-text": "^3.0.0", - "mdast-util-phrasing": "^3.0.0", - "mdast-util-to-string": "^3.0.0", - "rehype-minify-whitespace": "^5.0.0", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/hast-util-to-mdast/-/hast-util-to-mdast-10.1.0.tgz", + "integrity": "sha512-DsL/SvCK9V7+vfc6SLQ+vKIyBDXTk2KLSbfBYkH4zeF/uR1yBajHRhkzuaUSGOB1WJSTieJBdHwxlC+HLKvZZw==", + "requires": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-phrasing": "^3.0.0", + "hast-util-to-html": "^9.0.0", + "hast-util-to-text": "^4.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "mdast-util-to-string": "^4.0.0", + "rehype-minify-whitespace": "^6.0.0", "trim-trailing-lines": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit": "^4.0.0" + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0" + }, + "dependencies": { + "@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "requires": { + "@types/unist": "*" + } + } } }, "hast-util-to-parse5": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-7.1.0.tgz", - "integrity": "sha512-YNRgAJkH2Jky5ySkIqFXTQiaqcAtJyVE+D5lkN6CdtOqrnkLfGYYrEcKuHOJZlp+MwjSwuD3fZuawI+sic/RBw==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz", + "integrity": "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==", "requires": { - "@types/hast": "^2.0.0", + "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" + }, + "dependencies": { + "@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "requires": { + "@types/unist": "*" + } + } } }, "hast-util-to-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-2.0.0.tgz", - "integrity": "sha512-02AQ3vLhuH3FisaMM+i/9sm4OXGSq1UhOOCpTLLQtHdL3tZt7qil69r8M8iDkZYyC0HCFylcYoP+8IO7ddta1A==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.0.tgz", + "integrity": "sha512-OGkAxX1Ua3cbcW6EJ5pT/tslVb90uViVkcJ4ZZIMW/R33DX/AkcJcRrPebPwJkHYwlDHXz4aIwvAAaAdtrACFA==", "requires": { - "@types/hast": "^2.0.0" + "@types/hast": "^3.0.0" + }, + "dependencies": { + "@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "requires": { + "@types/unist": "*" + } + } } }, "hast-util-to-text": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-3.1.2.tgz", - "integrity": "sha512-tcllLfp23dJJ+ju5wCCZHVpzsQQ43+moJbqVX3jNWPB7z/KFC4FyZD6R7y94cHL6MQ33YtMZL8Z0aIXXI4XFTw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.0.tgz", + "integrity": "sha512-EWiE1FSArNBPUo1cKWtzqgnuRQwEeQbQtnFJRYV1hb1BWDgrAlBU0ExptvZMM/KSA82cDpm2sFGf3Dmc5Mza3w==", "requires": { - "@types/hast": "^2.0.0", - "@types/unist": "^2.0.0", - "hast-util-is-element": "^2.0.0", - "unist-util-find-after": "^4.0.0" + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.0.0" + }, + "dependencies": { + "@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "requires": { + "@types/unist": "*" + } + }, + "@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + } } }, "hast-util-whitespace": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz", - "integrity": "sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "requires": { + "@types/hast": "^3.0.0" + }, + "dependencies": { + "@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "requires": { + "@types/unist": "*" + } + } + } }, "hastscript": { "version": "7.2.0", @@ -32757,14 +32576,14 @@ "integrity": "sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg==" }, "html-void-elements": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz", - "integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==" }, "html-whitespace-sensitive-tag-names": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/html-whitespace-sensitive-tag-names/-/html-whitespace-sensitive-tag-names-2.0.0.tgz", - "integrity": "sha512-SQdIvTTtnHAx72xGUIUudvVOCjeWvV1U7rvSFnNGxTGRw3ZC7RES4Gw6dm1nMYD60TXvm6zjk/bWqgNc5pjQaw==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-whitespace-sensitive-tag-names/-/html-whitespace-sensitive-tag-names-3.0.0.tgz", + "integrity": "sha512-KlClZ3/Qy5UgvpvVvDomGhnQhNWH5INE8GwvSIQ9CWt1K0zbbXrl7eN5bWaafOZgtmO3jMPwUqmrmEwinhPq1w==" }, "htmlparser2": { "version": "8.0.2", @@ -33104,6 +32923,18 @@ "standard-as-callback": "^2.1.0" } }, + "ioredis-mock": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/ioredis-mock/-/ioredis-mock-8.9.0.tgz", + "integrity": "sha512-yIglcCkI1lvhwJVoMsR51fotZVsPsSk07ecTCgRTRlicG0Vq3lke6aAaHklyjmRNRsdYAgswqC2A0bPtQK4LSw==", + "requires": { + "@ioredis/as-callback": "^3.0.0", + "@ioredis/commands": "^1.2.0", + "fengari": "^0.1.4", + "fengari-interop": "^0.1.3", + "semver": "^7.5.4" + } + }, "ip-regex": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", @@ -33259,6 +33090,11 @@ "is-windows": "^1.0.1" } }, + "is-absolute-url": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-4.0.1.tgz", + "integrity": "sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==" + }, "is-alphabetical": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", @@ -33327,11 +33163,6 @@ "has-tostringtag": "^1.0.0" } }, - "is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==" - }, "is-builtin-module": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", @@ -33378,14 +33209,6 @@ "resolved": "https://registry.npmjs.org/is-electron/-/is-electron-2.2.0.tgz", "integrity": "sha512-SpMppC2XR3YdxSzczXReBjqs2zGscWQpBIKqwXYBFic0ERaxNVgwLCHwOLZeESfdJQjX0RDvrJ1lBXX2ij+G1Q==" }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "^2.0.4" - } - }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -33493,14 +33316,6 @@ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==" }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "requires": { - "isobject": "^3.0.1" - } - }, "is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -33630,11 +33445,6 @@ "resolved": "https://registry.npmjs.org/iso-url/-/iso-url-1.2.1.tgz", "integrity": "sha512-9JPDgCN4B7QPkLtYAAOrEuAWvP9rWvR5offAr0/SeF046wIkglqH3VXgYYP6NcsKslH80UIVgmPqNe3j7tG2ng==" }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==" - }, "isomorphic-fetch": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", @@ -35050,7 +34860,8 @@ "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true }, "js-yaml": { "version": "3.14.1", @@ -35143,7 +34954,8 @@ "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true }, "json-bigint": { "version": "1.0.0", @@ -35204,7 +35016,8 @@ "json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true }, "jsonify": { "version": "0.0.1", @@ -35371,17 +35184,24 @@ "dev": true }, "linkify-it": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz", - "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", "requires": { - "uc.micro": "^1.0.1" + "uc.micro": "^2.0.0" + }, + "dependencies": { + "uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==" + } } }, "linkifyjs": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.1.1.tgz", - "integrity": "sha512-zFN/CTVmbcVef+WaDXT63dNzzkfRBKT1j464NJQkV7iSgJU0sLBus9W0HBwnXK13/hf168pbrx/V/bjEHOXNHA==" + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.1.3.tgz", + "integrity": "sha512-auMesunaJ8yfkHvK4gfg1K0SaKX/6Wn9g2Aac/NwX+l5VdmFZzo/hdPGxEOETj+ryRa4/fiOPjeeKURSAJx1sg==" }, "listr2": { "version": "4.0.5", @@ -35501,15 +35321,6 @@ "p-locate": "^4.1.0" } }, - "lockfile": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/lockfile/-/lockfile-1.0.4.tgz", - "integrity": "sha512-cvbTwETRfsFh4nHsL1eGWapU1XFi5Ot9E85sWAwia7Y7EgB7vfqcZhTKZ+l7hCGxSPoushMv5GKhT5PdLv03WA==", - "dev": true, - "requires": { - "signal-exit": "^3.0.2" - } - }, "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -35541,12 +35352,6 @@ "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" }, - "lodash.defaultsdeep": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz", - "integrity": "sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==", - "dev": true - }, "lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", @@ -35849,7 +35654,8 @@ "make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true }, "makeerror": { "version": "1.0.12", @@ -35867,15 +35673,16 @@ "dev": true }, "markdown-it": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.1.tgz", - "integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.0.0.tgz", + "integrity": "sha512-seFjF0FIcPt4P9U39Bq1JYblX0KZCjDLFFQPHpL5AzHpqPEKtosxmdq/LTVZnjfH7tjt9BxStm+wXcDBNuYmzw==", "requires": { "argparse": "^2.0.1", - "entities": "~3.0.1", - "linkify-it": "^4.0.1", - "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.0.0" }, "dependencies": { "argparse": { @@ -35883,10 +35690,15 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, - "entities": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", - "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==" + "mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==" + }, + "uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==" } } }, @@ -35900,39 +35712,37 @@ "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==" }, - "mdast-util-definitions": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz", - "integrity": "sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA==", - "requires": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "unist-util-visit": "^4.0.0" - } - }, "mdast-util-directive": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-2.2.4.tgz", - "integrity": "sha512-sK3ojFP+jpj1n7Zo5ZKvoxP1MvLyzVG63+gm40Z/qI00avzdPCYxt7RBMgofwAva9gBjbDBWVRB/i+UD+fUCzQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.0.0.tgz", + "integrity": "sha512-JUpYOqKI4mM3sZcNxmF/ox04XYFFkNwr0CFlrQIkCwbvH0xzMCqkMqAde9wRd80VAhaUrwFwKm2nxretdT1h7Q==", "requires": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "mdast-util-from-markdown": "^1.3.0", - "mdast-util-to-markdown": "^1.5.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", - "unist-util-visit-parents": "^5.1.3" + "unist-util-visit-parents": "^6.0.0" + }, + "dependencies": { + "@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + } } }, "mdast-util-find-and-replace": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-2.2.2.tgz", - "integrity": "sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.1.tgz", + "integrity": "sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==", "requires": { - "@types/mdast": "^3.0.0", + "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.0.0" + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" }, "dependencies": { "escape-string-regexp": { @@ -35943,87 +35753,113 @@ } }, "mdast-util-from-markdown": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.0.tgz", - "integrity": "sha512-HN3W1gRIuN/ZW295c7zi7g9lVBllMgZE40RxCX37wrTPWXCWtpvOZdfnuK+1WNpvZje6XuJeI3Wnb4TJEUem+g==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.0.tgz", + "integrity": "sha512-n7MTOr/z+8NAX/wmhhDji8O3bRvPTV/U0oTCaZJkjhPSKTPhS3xufVhKGF8s1pJ7Ox4QgoIU7KHseh09S+9rTA==", "requires": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", - "mdast-util-to-string": "^3.1.0", - "micromark": "^3.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-decode-string": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "unist-util-stringify-position": "^3.0.0", - "uvu": "^0.5.0" + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "dependencies": { + "@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + } } }, "mdast-util-gfm-strikethrough": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-1.0.3.tgz", - "integrity": "sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", "requires": { - "@types/mdast": "^3.0.0", - "mdast-util-to-markdown": "^1.3.0" + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" } }, "mdast-util-newline-to-break": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-newline-to-break/-/mdast-util-newline-to-break-1.0.0.tgz", - "integrity": "sha512-491LcYv3gbGhhCrLoeALncQmega2xPh+m3gbsIhVsOX4sw85+ShLFPvPyibxc1Swx/6GtzxgVodq+cGa/47ULg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-newline-to-break/-/mdast-util-newline-to-break-2.0.0.tgz", + "integrity": "sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog==", "requires": { - "@types/mdast": "^3.0.0", - "mdast-util-find-and-replace": "^2.0.0" + "@types/mdast": "^4.0.0", + "mdast-util-find-and-replace": "^3.0.0" } }, "mdast-util-phrasing": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz", - "integrity": "sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", "requires": { - "@types/mdast": "^3.0.0", - "unist-util-is": "^5.0.0" + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" } }, "mdast-util-to-hast": { - "version": "12.3.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-12.3.0.tgz", - "integrity": "sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==", + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.1.0.tgz", + "integrity": "sha512-/e2l/6+OdGp/FB+ctrJ9Avz71AN/GRH3oi/3KAx/kMnoUsD6q0woXlDT8lLEeViVKE7oZxE7RXzvO3T8kF2/sA==", "requires": { - "@types/hast": "^2.0.0", - "@types/mdast": "^3.0.0", - "mdast-util-definitions": "^5.0.0", - "micromark-util-sanitize-uri": "^1.1.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", - "unist-util-generated": "^2.0.0", - "unist-util-position": "^4.0.0", - "unist-util-visit": "^4.0.0" + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "dependencies": { + "@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "requires": { + "@types/unist": "*" + } + } } }, "mdast-util-to-markdown": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz", - "integrity": "sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz", + "integrity": "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==", "requires": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", - "mdast-util-phrasing": "^3.0.0", - "mdast-util-to-string": "^3.0.0", - "micromark-util-decode-string": "^1.0.0", - "unist-util-visit": "^4.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" + }, + "dependencies": { + "@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + } } }, "mdast-util-to-string": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.1.1.tgz", - "integrity": "sha512-tGvhT94e+cVnQt8JWE9/b3cUQZWS732TJxXHktvP+BYo62PpYD53Ls/6cC60rW21dW+txxiM4zMdc6abASvZKA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", "requires": { - "@types/mdast": "^3.0.0" + "@types/mdast": "^4.0.0" } }, "mdurl": { @@ -36093,64 +35929,64 @@ "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" }, "micromark": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.1.0.tgz", - "integrity": "sha512-6Mj0yHLdUZjHnOPgr5xfWIMqMWS12zDN6iws9SLuSz76W8jTtAv24MN4/CL7gJrl5vtxGInkkqDv/JIoRsQOvA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.0.tgz", + "integrity": "sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==", "requires": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", - "micromark-core-commonmark": "^1.0.1", - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-chunked": "^1.0.0", - "micromark-util-combine-extensions": "^1.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-encode": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-sanitize-uri": "^1.0.0", - "micromark-util-subtokenize": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.1", - "uvu": "^0.5.0" + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, "micromark-core-commonmark": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.0.6.tgz", - "integrity": "sha512-K+PkJTxqjFfSNkfAhp4GB+cZPfQd6dxtTXnf+RjZOV7T4EEXnvgzOcnp+eSTmpGk9d1S9sL6/lqrgSNn/s0HZA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.0.tgz", + "integrity": "sha512-jThOz/pVmAYUtkroV3D5c1osFXAMv9e0ypGDOIZuCeAe91/sD6BoE2Sjzt30yuXtwOYUmySOhMas/PVyh02itA==", "requires": { "decode-named-character-reference": "^1.0.0", - "micromark-factory-destination": "^1.0.0", - "micromark-factory-label": "^1.0.0", - "micromark-factory-space": "^1.0.0", - "micromark-factory-title": "^1.0.0", - "micromark-factory-whitespace": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-chunked": "^1.0.0", - "micromark-util-classify-character": "^1.0.0", - "micromark-util-html-tag-name": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-subtokenize": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.1", - "uvu": "^0.5.0" + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, "micromark-extension-directive": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-2.1.2.tgz", - "integrity": "sha512-brqLEztt14/73snVXYsq9Cv6ng67O+Sy69ZuM0s8ZhN/GFI9rnyXyj0Y0DaCwi648vCImv7/U1H5TzR7wMv5jw==", - "requires": { - "micromark-factory-space": "^1.0.0", - "micromark-factory-whitespace": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "parse-entities": "^4.0.0", - "uvu": "^0.5.0" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-3.0.0.tgz", + "integrity": "sha512-61OI07qpQrERc+0wEysLHMvoiO3s2R56x5u7glHq2Yqq6EHbH4dW25G9GfDdGCDYqA21KE6DWgNSzxSwHc2hSg==", + "requires": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "parse-entities": "^4.0.0" } }, "micromark-extension-gfm-strikethrough": { @@ -36164,219 +36000,171 @@ "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" - }, - "dependencies": { - "micromark-util-character": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.0.1.tgz", - "integrity": "sha512-3wgnrmEAJ4T+mGXAUfMvMAbxU9RDG43XmGce4j6CwPtVxB3vfwXSZ6KhFwDzZ3mZHhmPimMAXg71veiBGzeAZw==", - "requires": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "micromark-util-chunked": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.0.tgz", - "integrity": "sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg==", - "requires": { - "micromark-util-symbol": "^2.0.0" - } - }, - "micromark-util-classify-character": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.0.tgz", - "integrity": "sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw==", - "requires": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "micromark-util-resolve-all": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.0.tgz", - "integrity": "sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA==", - "requires": { - "micromark-util-types": "^2.0.0" - } - }, - "micromark-util-symbol": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", - "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==" - }, - "micromark-util-types": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.0.tgz", - "integrity": "sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==" - } } }, "micromark-factory-destination": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.0.0.tgz", - "integrity": "sha512-eUBA7Rs1/xtTVun9TmV3gjfPz2wEwgK5R5xcbIM5ZYAtvGF6JkyaDsj0agx8urXnO31tEO6Ug83iVH3tdedLnw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz", + "integrity": "sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==", "requires": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, "micromark-factory-label": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.0.2.tgz", - "integrity": "sha512-CTIwxlOnU7dEshXDQ+dsr2n+yxpP0+fn271pu0bwDIS8uqfFcumXpj5mLn3hSC8iw2MUr6Gx8EcKng1dD7i6hg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.0.tgz", + "integrity": "sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw==", "requires": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, "micromark-factory-space": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.0.0.tgz", - "integrity": "sha512-qUmqs4kj9a5yBnk3JMLyjtWYN6Mzfcx8uJfi5XAveBniDevmZasdGBba5b4QsvRcAkmvGo5ACmSUmyGiKTLZew==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz", + "integrity": "sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==", "requires": { - "micromark-util-character": "^1.0.0", - "micromark-util-types": "^1.0.0" + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, "micromark-factory-title": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.0.2.tgz", - "integrity": "sha512-zily+Nr4yFqgMGRKLpTVsNl5L4PMu485fGFDOQJQBl2NFpjGte1e86zC0da93wf97jrc4+2G2GQudFMHn3IX+A==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.0.tgz", + "integrity": "sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A==", "requires": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, "micromark-factory-whitespace": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.0.0.tgz", - "integrity": "sha512-Qx7uEyahU1lt1RnsECBiuEbfr9INjQTGa6Err+gF3g0Tx4YEviPbqqGKNv/NrBaE7dVHdn1bVZKM/n5I/Bak7A==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.0.tgz", + "integrity": "sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA==", "requires": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, "micromark-util-character": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.1.0.tgz", - "integrity": "sha512-agJ5B3unGNJ9rJvADMJ5ZiYjBRyDpzKAOk01Kpi1TKhlT1APx3XZk6eN7RtSz1erbWHC2L8T3xLZ81wdtGRZzg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", + "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", "requires": { - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, "micromark-util-chunked": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.0.0.tgz", - "integrity": "sha512-5e8xTis5tEZKgesfbQMKRCyzvffRRUX+lK/y+DvsMFdabAicPkkZV6gO+FEWi9RfuKKoxxPwNL+dFF0SMImc1g==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.0.tgz", + "integrity": "sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg==", "requires": { - "micromark-util-symbol": "^1.0.0" + "micromark-util-symbol": "^2.0.0" } }, "micromark-util-classify-character": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.0.0.tgz", - "integrity": "sha512-F8oW2KKrQRb3vS5ud5HIqBVkCqQi224Nm55o5wYLzY/9PwHGXC01tr3d7+TqHHz6zrKQ72Okwtvm/xQm6OVNZA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.0.tgz", + "integrity": "sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw==", "requires": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, "micromark-util-combine-extensions": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", - "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.0.tgz", + "integrity": "sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ==", "requires": { - "micromark-util-chunked": "^1.0.0", - "micromark-util-types": "^1.0.0" + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, "micromark-util-decode-numeric-character-reference": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.0.0.tgz", - "integrity": "sha512-OzO9AI5VUtrTD7KSdagf4MWgHMtET17Ua1fIpXTpuhclCqD8egFWo85GxSGvxgkGS74bEahvtM0WP0HjvV0e4w==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.1.tgz", + "integrity": "sha512-bmkNc7z8Wn6kgjZmVHOX3SowGmVdhYS7yBpMnuMnPzDq/6xwVA604DuOXMZTO1lvq01g+Adfa0pE2UKGlxL1XQ==", "requires": { - "micromark-util-symbol": "^1.0.0" + "micromark-util-symbol": "^2.0.0" } }, "micromark-util-decode-string": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.0.2.tgz", - "integrity": "sha512-DLT5Ho02qr6QWVNYbRZ3RYOSSWWFuH3tJexd3dgN1odEuPNxCngTCXJum7+ViRAd9BbdxCvMToPOD/IvVhzG6Q==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.0.tgz", + "integrity": "sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA==", "requires": { "decode-named-character-reference": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-symbol": "^1.0.0" + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" } }, "micromark-util-encode": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.0.1.tgz", - "integrity": "sha512-U2s5YdnAYexjKDel31SVMPbfi+eF8y1U4pfiRW/Y8EFVCy/vgxk/2wWTxzcqE71LHtCuCzlBDRU2a5CQ5j+mQA==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz", + "integrity": "sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==" }, "micromark-util-html-tag-name": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.1.0.tgz", - "integrity": "sha512-BKlClMmYROy9UiV03SwNmckkjn8QHVaWkqoAqzivabvdGcwNGMMMH/5szAnywmsTBUzDsU57/mFi0sp4BQO6dA==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.0.tgz", + "integrity": "sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw==" }, "micromark-util-normalize-identifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.0.0.tgz", - "integrity": "sha512-yg+zrL14bBTFrQ7n35CmByWUTFsgst5JhA4gJYoty4Dqzj4Z4Fr/DHekSS5aLfH9bdlfnSvKAWsAgJhIbogyBg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.0.tgz", + "integrity": "sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w==", "requires": { - "micromark-util-symbol": "^1.0.0" + "micromark-util-symbol": "^2.0.0" } }, "micromark-util-resolve-all": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.0.0.tgz", - "integrity": "sha512-CB/AGk98u50k42kvgaMM94wzBqozSzDDaonKU7P7jwQIuH2RU0TeBqGYJz2WY1UdihhjweivStrJ2JdkdEmcfw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.0.tgz", + "integrity": "sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA==", "requires": { - "micromark-util-types": "^1.0.0" + "micromark-util-types": "^2.0.0" } }, "micromark-util-sanitize-uri": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.1.0.tgz", - "integrity": "sha512-RoxtuSCX6sUNtxhbmsEFQfWzs8VN7cTctmBPvYivo98xb/kDEoTCtJQX5wyzIYEmk/lvNFTat4hL8oW0KndFpg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz", + "integrity": "sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==", "requires": { - "micromark-util-character": "^1.0.0", - "micromark-util-encode": "^1.0.0", - "micromark-util-symbol": "^1.0.0" + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" } }, "micromark-util-subtokenize": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.0.2.tgz", - "integrity": "sha512-d90uqCnXp/cy4G881Ub4psE57Sf8YD0pim9QdjCRNjfas2M1u6Lbt+XZK9gnHL2XFhnozZiEdCa9CNfXSfQ6xA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.0.tgz", + "integrity": "sha512-vc93L1t+gpR3p8jxeVdaYlbV2jTYteDje19rNSS/H5dlhxUYll5Fy6vJ2cDwP8RnsXi818yGty1ayP55y3W6fg==", "requires": { - "micromark-util-chunked": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, "micromark-util-symbol": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.0.1.tgz", - "integrity": "sha512-oKDEMK2u5qqAptasDAwWDXq0tG9AssVwAx3E9bBF3t/shRIGsWIRG+cGafs2p/SnDSOecnt6hZPCE2o6lHfFmQ==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", + "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==" }, "micromark-util-types": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.0.2.tgz", - "integrity": "sha512-DCfg/T8fcrhrRKTPjRrw/5LLvdGV7BHySf/1LOZx7TzWZdYRjogNtyNq885z3nNallwr3QUKARjqvHqX1/7t+w==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.0.tgz", + "integrity": "sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==" }, "micromatch": { "version": "4.0.5", @@ -36461,20 +36249,15 @@ "resolved": "https://registry.npmjs.org/module-alias/-/module-alias-2.2.2.tgz", "integrity": "sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q==" }, - "mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==" - }, "ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "msgpackr": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.9.2.tgz", - "integrity": "sha512-xtDgI3Xv0AAiZWLRGDchyzBwU6aq0rwJ+W+5Y4CZhEWtkl/hJtFFLc+3JtGTw7nz1yquxs7nL8q/yA2aqpflIQ==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.10.1.tgz", + "integrity": "sha512-r5VRLv9qouXuLiIBrLpl2d5ZvPt8svdQTl5/vMvE4nzDMyEX4sgW5yWhuBBj5UmgwOTWj8CIdSXn5sAfsHAWIQ==", "requires": { "msgpackr-extract": "^3.0.2" } @@ -36537,14 +36320,14 @@ "dev": true }, "nan": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", - "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==" + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz", + "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==" }, "nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==" + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==" }, "native-fetch": { "version": "3.0.0", @@ -36582,9 +36365,9 @@ } }, "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", + "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==" } } }, @@ -36769,7 +36552,8 @@ "node-releases": { "version": "2.0.13", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==" + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true }, "nodemon": { "version": "2.0.22", @@ -36973,7 +36757,7 @@ "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==" }, "number-precision": { "version": "1.6.0", @@ -37097,22 +36881,6 @@ "es-abstract": "^1.20.4" } }, - "object.omit": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-3.0.0.tgz", - "integrity": "sha512-EO+BCv6LJfu+gBIF3ggLicFebFLN5zqzz/WWJlMFfkMyGth+oBkhxzDl0wx2W4GkLzuQs/FsSkXZb2IMWQqmBQ==", - "requires": { - "is-extendable": "^1.0.0" - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", - "requires": { - "isobject": "^3.0.1" - } - }, "object.values": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", @@ -37489,9 +37257,12 @@ "dev": true }, "parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "requires": { + "entities": "^4.4.0" + } }, "parse5-htmlparser2-tree-adapter": { "version": "7.0.0", @@ -37500,16 +37271,6 @@ "requires": { "domhandler": "^5.0.2", "parse5": "^7.0.0" - }, - "dependencies": { - "parse5": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", - "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", - "requires": { - "entities": "^4.4.0" - } - } } }, "parseurl": { @@ -37641,12 +37402,6 @@ "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=" }, - "pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true - }, "pg": { "version": "8.11.0", "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.0.tgz", @@ -37718,7 +37473,8 @@ "picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true }, "picomatch": { "version": "2.3.1", @@ -37946,9 +37702,9 @@ } }, "prosemirror-inputrules": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.2.1.tgz", - "integrity": "sha512-3LrWJX1+ULRh5SZvbIQlwZafOXqp1XuV21MGBu/i5xsztd+9VD15x6OtN6mdqSFI7/8Y77gYUbQ6vwwJ4mr6QQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.4.0.tgz", + "integrity": "sha512-6ygpPRuTJ2lcOXs9JkefieMst63wVJBgHZGl5QOytN7oSZs3Co/BYbc3Yx9zm9H37Bxw8kVzCnDsihsVsL4yEg==", "requires": { "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.0.0" @@ -37964,18 +37720,18 @@ } }, "prosemirror-markdown": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.11.0.tgz", - "integrity": "sha512-yP9mZqPRstjZhhf3yykCQNE3AijxARrHe4e7esV9A+gp4cnGOH4QvrKYPpXLHspNWyvJJ+0URH+iIvV5qP1I2Q==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.12.0.tgz", + "integrity": "sha512-6F5HS8Z0HDYiS2VQDZzfZP6A0s/I0gbkJy8NCzzDMtcsz3qrfqyroMMeoSjAmOhDITyon11NbXSzztfKi+frSQ==", "requires": { - "markdown-it": "^13.0.1", + "markdown-it": "^14.0.0", "prosemirror-model": "^1.0.0" } }, "prosemirror-menu": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.2.2.tgz", - "integrity": "sha512-437HIWTq4F9cTX+kPfqZWWm+luJm95Aut/mLUy+9OMrOml0bmWDS26ceC6SNfb2/S94et1sZ186vLO7pDHzxSw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.2.4.tgz", + "integrity": "sha512-S/bXlc0ODQup6aiBbWVsX/eM+xJgCTAfMq/nLqaO5ID/am4wS0tTCIkzwytmao7ypEtjj39i7YbJjAgO20mIqA==", "requires": { "crelt": "^1.0.0", "prosemirror-commands": "^1.0.0", @@ -37984,9 +37740,9 @@ } }, "prosemirror-model": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.19.2.tgz", - "integrity": "sha512-RXl0Waiss4YtJAUY3NzKH0xkJmsZupCIccqcIFoLTIKFlKNbIvFDRl27/kQy1FP8iUAxrjRRfIVvOebnnXJgqQ==", + "version": "1.19.4", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.19.4.tgz", + "integrity": "sha512-RPmVXxUfOhyFdayHawjuZCxiROsm9L4FCUA6pWI+l7n2yCBsWy9VpdE1hpDHUS8Vad661YLY9AzqfjLhAKQ4iQ==", "requires": { "orderedmap": "^2.0.0" } @@ -38020,9 +37776,9 @@ } }, "prosemirror-tables": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.3.4.tgz", - "integrity": "sha512-z6uLSQ1BLC3rgbGwZmpfb+xkdvD7W/UOsURDfognZFYaTtc0gsk7u/t71Yijp2eLflVpffMk6X0u0+u+MMDvIw==", + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.3.7.tgz", + "integrity": "sha512-oEwX1wrziuxMtwFvdDWSFHVUWrFJWt929kVVfHvtTi8yvw+5ppxjXZkMG/fuTdFo+3DXyIPSKfid+Be1npKXDA==", "requires": { "prosemirror-keymap": "^1.1.2", "prosemirror-model": "^1.8.1", @@ -38032,13 +37788,11 @@ } }, "prosemirror-trailing-node": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-2.0.4.tgz", - "integrity": "sha512-0Yl9w7IdHkaCdqR+NE3FOucePME4OmiGcybnF1iasarEILP5U8+4xTnl53yafULjmwcg1SrSG65Hg7Zk2H2v3g==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-2.0.8.tgz", + "integrity": "sha512-ujRYhSuhQb1Jsarh1IHqb2KoSnRiD7wAMDGucP35DN7j5af6X7B18PfdPIrbwsPTqIAj0fyOvxbuPsWhNvylmA==", "requires": { - "@babel/runtime": "^7.21.0", - "@remirror/core-constants": "^2.0.1", - "@remirror/core-helpers": "^2.0.2", + "@remirror/core-constants": "^2.0.2", "escape-string-regexp": "^4.0.0" }, "dependencies": { @@ -38050,17 +37804,17 @@ } }, "prosemirror-transform": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.7.3.tgz", - "integrity": "sha512-qDapyx5lqYfxVeUWEw0xTGgeP2S8346QtE7DxkalsXlX89lpzkY6GZfulgfHyk1n4tf74sZ7CcXgcaCcGjsUtA==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.8.0.tgz", + "integrity": "sha512-BaSBsIMv52F1BVVMvOmp1yzD3u65uC3HTzCBQV1WDPqJRQ2LuHKcyfn0jwqodo8sR9vVzMzZyI+Dal5W9E6a9A==", "requires": { "prosemirror-model": "^1.0.0" } }, "prosemirror-view": { - "version": "1.31.5", - "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.31.5.tgz", - "integrity": "sha512-tobRCDeCp61elR1d97XE/JTL9FDIfswZpWeNs7GKJjAJvWyMGHWYFCq29850p6bbG2bckP+i9n1vT56RifosbA==", + "version": "1.33.1", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.33.1.tgz", + "integrity": "sha512-62qkYgSJIkwIMMCpuGuPzc52DiK1Iod6TWoIMxP4ja6BTD4yO8kCUL64PZ/WhH/dJ9fW0CDO39FhH1EMyhUFEg==", "requires": { "prosemirror-model": "^1.16.0", "prosemirror-state": "^1.0.0", @@ -38081,9 +37835,9 @@ "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" }, "protobufjs": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.3.tgz", - "integrity": "sha512-TtpvOqwB5Gdz/PQmOjgsrGH1nHjAQVCN7JG4A6r1sXRWESL5rNMAiRcBQlCAdKxZcAbstExQePYG8xof/JVRgg==", + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.6.tgz", + "integrity": "sha512-dgJaEDDL6x8ASUZ1YqWciTRrdOuYNzoOf27oHNfdyvKqHr5i0FV7FSLU+aIeFjyFgVxrpTOtQUi0BLLBymZaBw==", "requires": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", @@ -38102,9 +37856,9 @@ } }, "protobufjs": { - "version": "6.11.3", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.3.tgz", - "integrity": "sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==", + "version": "6.11.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz", + "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==", "requires": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", @@ -38298,21 +38052,16 @@ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", "dev": true }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, + "punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==" + }, "pure-rand": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.2.tgz", @@ -38479,6 +38228,11 @@ "picomatch": "^2.2.1" } }, + "readline-sync": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/readline-sync/-/readline-sync-1.4.10.tgz", + "integrity": "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==" + }, "receptacle": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/receptacle/-/receptacle-1.3.2.tgz", @@ -38500,68 +38254,6 @@ "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", "integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=" }, - "redis-memory-server": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/redis-memory-server/-/redis-memory-server-0.6.0.tgz", - "integrity": "sha512-jo80uIfGY2goCY7rXXIOxs7rD/hsTtyWKUE1IA1wraugPxpkz21vEjckdkAvEZ+eawlkn0b+53Fv0N1CNfIOVw==", - "dev": true, - "requires": { - "camelcase": "^6.0.0", - "cross-spawn": "^7.0.3", - "debug": "^4.2.0", - "extract-zip": "^2.0.1", - "find-cache-dir": "^3.3.1", - "find-package-json": "^1.2.0", - "get-port": "^5.1.1", - "https-proxy-agent": "^5.0.0", - "lockfile": "^1.0.4", - "lodash.defaultsdeep": "^4.6.1", - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2", - "semver": "^7.3.2", - "tar": "^6.1.0", - "tmp": "^0.2.1", - "uuid": "8.3.0" - }, - "dependencies": { - "camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true - }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", - "dev": true, - "requires": { - "rimraf": "^3.0.0" - } - }, - "uuid": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz", - "integrity": "sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ==", - "dev": true - } - } - }, "redis-parser": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", @@ -38573,7 +38265,8 @@ "regenerator-runtime": { "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true }, "regexp-tree": { "version": "0.1.27", @@ -38609,95 +38302,185 @@ } } }, + "rehype-external-links": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rehype-external-links/-/rehype-external-links-3.0.0.tgz", + "integrity": "sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw==", + "requires": { + "@types/hast": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-is-element": "^3.0.0", + "is-absolute-url": "^4.0.0", + "space-separated-tokens": "^2.0.0", + "unist-util-visit": "^5.0.0" + }, + "dependencies": { + "@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "requires": { + "@types/unist": "*" + } + } + } + }, "rehype-format": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/rehype-format/-/rehype-format-4.0.1.tgz", - "integrity": "sha512-HA92WeqFri00yiClrz54IIpM9no2DH9Mgy5aFmInNODoAYn+hN42a6oqJTIie2nj0HwFyV7JvOYx5YHBphN8mw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/rehype-format/-/rehype-format-5.0.0.tgz", + "integrity": "sha512-kM4II8krCHmUhxrlvzFSptvaWh280Fr7UGNJU5DCMuvmAwGCNmGfi9CvFAQK6JDjsNoRMWQStglK3zKJH685Wg==", "requires": { - "@types/hast": "^2.0.0", - "hast-util-embedded": "^2.0.0", - "hast-util-is-element": "^2.0.0", - "hast-util-phrasing": "^2.0.0", - "hast-util-whitespace": "^2.0.0", - "html-whitespace-sensitive-tag-names": "^2.0.0", - "rehype-minify-whitespace": "^5.0.0", - "unified": "^10.0.0", - "unist-util-visit-parents": "^5.0.0" + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "hast-util-phrasing": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "html-whitespace-sensitive-tag-names": "^3.0.0", + "rehype-minify-whitespace": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "dependencies": { + "@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "requires": { + "@types/unist": "*" + } + } } }, "rehype-minify-whitespace": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/rehype-minify-whitespace/-/rehype-minify-whitespace-5.0.1.tgz", - "integrity": "sha512-PPp4lWJiBPlePI/dv1BeYktbwkfgXkrK59MUa+tYbMPgleod+4DvFK2PLU0O0O60/xuhHfiR9GUIUlXTU8sRIQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/rehype-minify-whitespace/-/rehype-minify-whitespace-6.0.0.tgz", + "integrity": "sha512-i9It4YHR0Sf3GsnlR5jFUKXRr9oayvEk9GKQUkwZv6hs70OH9q3OCZrq9PpLvIGKt3W+JxBOxCidNVpH/6rWdA==", "requires": { - "@types/hast": "^2.0.0", - "hast-util-embedded": "^2.0.0", - "hast-util-is-element": "^2.0.0", - "hast-util-whitespace": "^2.0.0", - "unified": "^10.0.0", - "unist-util-is": "^5.0.0" + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "dependencies": { + "@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "requires": { + "@types/unist": "*" + } + } } }, "rehype-parse": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-8.0.4.tgz", - "integrity": "sha512-MJJKONunHjoTh4kc3dsM1v3C9kGrrxvA3U8PxZlP2SjH8RNUSrb+lF7Y0KVaUDnGH2QZ5vAn7ulkiajM9ifuqg==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.0.tgz", + "integrity": "sha512-WG7nfvmWWkCR++KEkZevZb/uw41E8TsH4DsY9UxsTbIXCVGbAs4S+r8FrQ+OtH5EEQAs+5UxKC42VinkmpA1Yw==", "requires": { - "@types/hast": "^2.0.0", - "hast-util-from-parse5": "^7.0.0", - "parse5": "^6.0.0", - "unified": "^10.0.0" + "@types/hast": "^3.0.0", + "hast-util-from-html": "^2.0.0", + "unified": "^11.0.0" + }, + "dependencies": { + "@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "requires": { + "@types/unist": "*" + } + } } }, "rehype-raw": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-6.1.1.tgz", - "integrity": "sha512-d6AKtisSRtDRX4aSPsJGTfnzrX2ZkHQLE5kiUuGOeEoLpbEulFF4hj0mLPbsa+7vmguDKOVVEQdHKDSwoaIDsQ==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", "requires": { - "@types/hast": "^2.0.0", - "hast-util-raw": "^7.2.0", - "unified": "^10.0.0" + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "dependencies": { + "@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "requires": { + "@types/unist": "*" + } + } } }, "rehype-remark": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/rehype-remark/-/rehype-remark-9.1.2.tgz", - "integrity": "sha512-c0fG3/CrJ95zAQ07xqHSkdpZybwdsY7X5dNWvgL2XqLKZuqmG3+vk6kP/4miCnp+R+x/0uKKRSpfXb9aGR8Z5w==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/rehype-remark/-/rehype-remark-10.0.0.tgz", + "integrity": "sha512-+aDXY/icqMFOafJQomVjxe3BAP7aR3lIsQ3GV6VIwpbCD2nvNFOXjGvotMe5p0Ny+Gt6L13DhEf/FjOOpTuUbQ==", "requires": { - "@types/hast": "^2.0.0", - "@types/mdast": "^3.0.0", - "hast-util-to-mdast": "^8.3.0", - "unified": "^10.0.0" + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "hast-util-to-mdast": "^10.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "dependencies": { + "@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "requires": { + "@types/unist": "*" + } + } } }, "rehype-rewrite": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/rehype-rewrite/-/rehype-rewrite-3.0.6.tgz", - "integrity": "sha512-REDTNCvsKcAazy8IQWzKp66AhSUDSOIKssSCqNqCcT9sN7JCwAAm3mWGTUdUzq80ABuy8d0D6RBwbnewu1aY1g==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/rehype-rewrite/-/rehype-rewrite-4.0.2.tgz", + "integrity": "sha512-rjLJ3z6fIV11phwCqHp/KRo8xuUCO8o9bFJCNw5o6O2wlLk6g8r323aRswdGBQwfXPFYeSuZdAjp4tzo6RGqEg==", "requires": { - "hast-util-select": "~5.0.1", - "unified": "~10.1.1", - "unist-util-visit": "~4.1.0" + "hast-util-select": "^6.0.0", + "unified": "^11.0.3", + "unist-util-visit": "^5.0.0" } }, "rehype-sanitize": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/rehype-sanitize/-/rehype-sanitize-5.0.1.tgz", - "integrity": "sha512-da/jIOjq8eYt/1r9GN6GwxIR3gde7OZ+WV8pheu1tL8K0D9KxM2AyMh+UEfke+FfdM3PvGHeYJU0Td5OWa7L5A==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/rehype-sanitize/-/rehype-sanitize-6.0.0.tgz", + "integrity": "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==", "requires": { - "@types/hast": "^2.0.0", - "hast-util-sanitize": "^4.0.0", - "unified": "^10.0.0" + "@types/hast": "^3.0.0", + "hast-util-sanitize": "^5.0.0" + }, + "dependencies": { + "@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "requires": { + "@types/unist": "*" + } + } } }, "rehype-stringify": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-9.0.3.tgz", - "integrity": "sha512-kWiZ1bgyWlgOxpqD5HnxShKAdXtb2IUljn3hQAhySeak6IOQPPt6DeGnsIh4ixm7yKJWzm8TXFuC/lPfcWHJqw==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.0.tgz", + "integrity": "sha512-1TX1i048LooI9QoecrXy7nGFFbFSufxVRAfc6Y9YMRAi56l+oB0zP51mLSV312uRuvVLPV1opSlJmslozR1XHQ==", "requires": { - "@types/hast": "^2.0.0", - "hast-util-to-html": "^8.0.0", - "unified": "^10.0.0" + "@types/hast": "^3.0.0", + "hast-util-to-html": "^9.0.0", + "unified": "^11.0.0" + }, + "dependencies": { + "@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "requires": { + "@types/unist": "*" + } + } } }, "relay-runtime": { @@ -38712,24 +38495,24 @@ } }, "remark-breaks": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/remark-breaks/-/remark-breaks-3.0.3.tgz", - "integrity": "sha512-C7VkvcUp1TPUc2eAYzsPdaUh8Xj4FSbQnYA5A9f80diApLZscTDeG7efiWP65W8hV2sEy3JuGVU0i6qr5D8Hug==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-breaks/-/remark-breaks-4.0.0.tgz", + "integrity": "sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==", "requires": { - "@types/mdast": "^3.0.0", - "mdast-util-newline-to-break": "^1.0.0", - "unified": "^10.0.0" + "@types/mdast": "^4.0.0", + "mdast-util-newline-to-break": "^2.0.0", + "unified": "^11.0.0" } }, "remark-directive": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/remark-directive/-/remark-directive-2.0.1.tgz", - "integrity": "sha512-oosbsUAkU/qmUE78anLaJePnPis4ihsE7Agp0T/oqTzvTea8pOiaYEtfInU/+xMOVTS9PN5AhGOiaIVe4GD8gw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/remark-directive/-/remark-directive-3.0.0.tgz", + "integrity": "sha512-l1UyWJ6Eg1VPU7Hm/9tt0zKtReJQNOA4+iDMAxTyZNWnJnFlbS/7zhiel/rogTLQ2vMYwDzSJa4BiVNqGlqIMA==", "requires": { - "@types/mdast": "^3.0.0", - "mdast-util-directive": "^2.0.0", - "micromark-extension-directive": "^2.0.0", - "unified": "^10.0.0" + "@types/mdast": "^4.0.0", + "mdast-util-directive": "^3.0.0", + "micromark-extension-directive": "^3.0.0", + "unified": "^11.0.0" } }, "remark-directive-rehype": { @@ -38742,34 +38525,46 @@ } }, "remark-parse": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.2.tgz", - "integrity": "sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", "requires": { - "@types/mdast": "^3.0.0", - "mdast-util-from-markdown": "^1.0.0", - "unified": "^10.0.0" + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" } }, "remark-rehype": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-10.1.0.tgz", - "integrity": "sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.0.tgz", + "integrity": "sha512-z3tJrAs2kIs1AqIIy6pzHmAHlF1hWQ+OdY4/hv+Wxe35EhyLKcajL33iUEn3ScxtFox9nUvRufR/Zre8Q08H/g==", "requires": { - "@types/hast": "^2.0.0", - "@types/mdast": "^3.0.0", - "mdast-util-to-hast": "^12.1.0", - "unified": "^10.0.0" + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "dependencies": { + "@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "requires": { + "@types/unist": "*" + } + } } }, "remark-stringify": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-10.0.3.tgz", - "integrity": "sha512-koyOzCMYoUHudypbj4XpnAKFbkddRMYZHwghnxd7ue5210WzGw6kOBwauJTRUMq16jsovXx8dYNvSSWP89kZ3A==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", "requires": { - "@types/mdast": "^3.0.0", - "mdast-util-to-markdown": "^1.0.0", - "unified": "^10.0.0" + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" } }, "remedial": { @@ -39003,14 +38798,6 @@ "tslib": "^2.1.0" } }, - "sade": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", - "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", - "requires": { - "mri": "^1.1.0" - } - }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -39660,6 +39447,7 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, "requires": { "has-flag": "^3.0.0" } @@ -39747,11 +39535,6 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, - "throttle-debounce": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz", - "integrity": "sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==" - }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -39792,7 +39575,6 @@ "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, "requires": { "os-tmpdir": "~1.0.2" } @@ -39806,7 +39588,8 @@ "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==" + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true }, "to-regex-range": { "version": "5.0.1", @@ -39858,9 +39641,9 @@ "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" }, "trough": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/trough/-/trough-2.1.0.tgz", - "integrity": "sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==" }, "ts-jest": { "version": "29.1.0", @@ -40075,19 +39858,24 @@ "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" }, "unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.4.tgz", + "integrity": "sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ==", "requires": { - "@types/unist": "^2.0.0", + "@types/unist": "^3.0.0", "bail": "^2.0.0", + "devlop": "^1.0.0", "extend": "^3.0.0", - "is-buffer": "^2.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", - "vfile": "^5.0.0" + "vfile": "^6.0.0" }, "dependencies": { + "@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, "is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -40096,25 +39884,34 @@ } }, "unist-util-find-after": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-4.0.1.tgz", - "integrity": "sha512-QO/PuPMm2ERxC6vFXEPtmAutOopy5PknD+Oq64gGwxKtk4xwo9Z97t9Av1obPmGU0IyTa6EKYUfTrK2QJS3Ozw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", "requires": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "dependencies": { + "@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + } } }, - "unist-util-generated": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.1.tgz", - "integrity": "sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==" - }, "unist-util-is": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", - "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", "requires": { - "@types/unist": "^2.0.0" + "@types/unist": "^3.0.0" + }, + "dependencies": { + "@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + } } }, "unist-util-map": { @@ -40126,38 +39923,66 @@ } }, "unist-util-position": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", - "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", "requires": { - "@types/unist": "^2.0.0" + "@types/unist": "^3.0.0" + }, + "dependencies": { + "@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + } } }, "unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", "requires": { - "@types/unist": "^2.0.0" + "@types/unist": "^3.0.0" + }, + "dependencies": { + "@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + } } }, "unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", "requires": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "dependencies": { + "@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + } } }, "unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", "requires": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "dependencies": { + "@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + } } }, "unixify": { @@ -40189,6 +40014,7 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "dev": true, "requires": { "escalade": "^3.1.1", "picocolors": "^1.0.0" @@ -40287,29 +40113,6 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" }, - "uvu": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz", - "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==", - "requires": { - "dequal": "^2.0.0", - "diff": "^5.0.0", - "kleur": "^4.0.3", - "sade": "^1.7.3" - }, - "dependencies": { - "diff": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", - "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==" - }, - "kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==" - } - } - }, "v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -40388,32 +40191,52 @@ "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, "vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz", + "integrity": "sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==", "requires": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "dependencies": { + "@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + } } }, "vfile-location": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-4.1.0.tgz", - "integrity": "sha512-YF23YMyASIIJXpktBa4vIGLJ5Gs88UB/XePgqPmTa7cDA+JeO3yclbpheQYCHjVHBn/yePzrXuygIL+xbvRYHw==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.2.tgz", + "integrity": "sha512-NXPYyxyBSH7zB5U6+3uDdd6Nybz6o6/od9rk8bp9H8GR3L+cm/fC0uUTbqBmUTnMCUDslAGBOIKNfvvb+gGlDg==", "requires": { - "@types/unist": "^2.0.0", - "vfile": "^5.0.0" + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "dependencies": { + "@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + } } }, "vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", "requires": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "dependencies": { + "@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + } } }, "viem": { @@ -40846,16 +40669,6 @@ "decamelize": "^1.2.0" } }, - "yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "requires": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, "yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", @@ -40869,9 +40682,9 @@ "dev": true }, "zeed-dom": { - "version": "0.10.5", - "resolved": "https://registry.npmjs.org/zeed-dom/-/zeed-dom-0.10.5.tgz", - "integrity": "sha512-blCwZ4ACAsbGh7tNy8eG+2Ri1Mj9SJmxcYmjw0ijC5b4Oyfm/F2RDFTuFXIiiJaOq3xEcPBhgQpI1zIIfS4v4Q==", + "version": "0.12.10", + "resolved": "https://registry.npmjs.org/zeed-dom/-/zeed-dom-0.12.10.tgz", + "integrity": "sha512-bFao9LxLVC8BOfLS9OGv/JHVDQ+JrR+opn1ZAvcFceGdpAviUurGxE5RQaLH+fGJyOmmpv71OIr92QdcSwLPBg==", "requires": { "css-what": "^6.1.0" } diff --git a/package.json b/package.json index e8fc6a379..575dcf593 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { "name": "matters-server", - "version": "4.29.0", + "version": "5.0.0", "description": "Matters Server", "author": "Matters ", "main": "build/index.js", "engines": { - "node": ">=16.14 <17.0" + "node": ">=16.14 <19.0" }, "license": "Apache-2.0", "scripts": { @@ -36,7 +36,6 @@ "gen": "npm run gen:schema && npm run gen:types", "schema:push:dev": "rover graph publish matters-stage@current --schema ./schema.graphql", "schema:push:prod": "rover graph publish matters-production@current --schema ./schema.graphql", - "search:init": "ts-node --files src/common/utils/initSearchIndices.ts", "prepare": "husky install" }, "dependencies": { @@ -50,8 +49,8 @@ "@graphql-tools/utils": "^10.0.0", "@keyv/redis": "^2.6.1", "@matters/apollo-response-cache": "^2.0.0-alpha.0", - "@matters/ipns-site-generator": "^0.1.3", - "@matters/matters-editor": "^0.2.2", + "@matters/ipns-site-generator": "^0.1.6", + "@matters/matters-editor": "^0.2.4", "@matters/passport-likecoin": "^1.0.0", "@matters/slugify": "^0.7.3", "@sendgrid/helpers": "^7.7.0", @@ -75,7 +74,7 @@ "fastest-levenshtein": "^1.0.16", "form-data": "^4.0.0", "get-stream": "^6.0.1", - "graphql": "^16.6.0", + "graphql": "^16.8.1", "graphql-constraint-directive": "^5.1.1", "graphql-middleware": "^6.1.34", "graphql-playground-middleware-express": "^1.7.23", @@ -85,6 +84,7 @@ "graphql-upload": "^13.0.0", "helmet": "^7.0.0", "ioredis": "^5.3.2", + "ioredis-mock": "^8.9.0", "ipfs-http-client": "^56.0.3", "js-base64": "^3.7.5", "jsonwebtoken": "^9.0.0", @@ -94,7 +94,7 @@ "meilisearch": "^0.32.3", "mime-types": "^2.1.35", "module-alias": "^2.2.2", - "nanoid": "^3.3.6", + "nanoid": "^3.3.7", "node-fetch": "^2.6.11", "number-precision": "^1.6.0", "oauth-1.0a": "^2.2.6", @@ -124,6 +124,7 @@ "@types/cors": "^2.8.13", "@types/debug": "^4.1.7", "@types/graphql-upload": "^8.0.12", + "@types/ioredis-mock": "^8.2.5", "@types/jest": "^29.5.1", "@types/jsonwebtoken": "^9.0.2", "@types/lodash": "^4.14.194", @@ -158,7 +159,6 @@ "nodemon": "^2.0.22", "npm-run-all": "^4.1.5", "prettier": "^2.8.8", - "redis-memory-server": "^0.6.0", "rimraf": "^5.0.1", "ts-jest": "^29.1.0", "ts-node": "^10.9.1" diff --git a/schema.graphql b/schema.graphql index b7d63a46c..c1be83c35 100644 --- a/schema.graphql +++ b/schema.graphql @@ -49,22 +49,6 @@ type Mutation { """Read an article.""" readArticle(input: ReadArticleInput!): Article! - """ - Create a Topic when no id is given, update fields when id is given. Throw error if no id & no title. - """ - putTopic(input: PutTopicInput!): Topic! - - """ - Create a Chapter when no id is given, update fields when id is given. Throw error if no id & no title, or no id & no topic. - """ - putChapter(input: PutChapterInput!): Chapter! - - """Delete topics""" - deleteTopics(input: DeleteTopicsInput!): Boolean! - - """Sort topics""" - sortTopics(input: SortTopicsInput!): [Topic!]! - """Follow or unfollow tag.""" toggleFollowTag(input: ToggleItemInput!): Tag! @@ -156,6 +140,9 @@ type Mutation { """Delete blocked search keywords from search_history db""" deleteBlockedSearchKeywords(input: KeywordsInput!): Boolean + + """Submit inappropriate content report""" + submitReport(input: SubmitReportInput!): Report! setBoost(input: SetBoostInput!): Node! putRemark(input: PutRemarkInput!): String putSkippedListItem(input: PutSkippedListItemInput!): [SkippedListItem!] @@ -164,6 +151,7 @@ type Mutation { putAnnouncement(input: PutAnnouncementInput!): Announcement! deleteAnnouncements(input: DeleteAnnouncementsInput!): Boolean! putRestrictedUsers(input: PutRestrictedUsersInput!): [User!]! + putIcymiTopic(input: PutIcymiTopicInput!): IcymiTopic """Send verification code for email.""" sendVerificationCode(input: SendVerificationCodeInput!): Boolean @@ -355,6 +343,9 @@ type Article implements Node & PinnableWork { """Media hash, composed of cid encoding, of this article.""" mediaHash: String! + """Short hash for shorter url addressing""" + shortHash: String! + """Content (HTML) of this article.""" content: String! @@ -451,6 +442,9 @@ type Article implements Node & PinnableWork { """License Type""" license: ArticleLicenseType! + """whether current viewer has donated to this article""" + donated: Boolean! + """creator message asking for support""" requestForDonation: String @@ -462,6 +456,9 @@ type Article implements Node & PinnableWork { """whether readers can comment""" canComment: Boolean! + + """history versions""" + versions(input: ArticleVersionsInput!): ArticleVersionsConnection! oss: ArticleOSS! remark: String @@ -490,65 +487,32 @@ type Article implements Node & PinnableWork { responses(input: ResponsesInput!): ResponseConnection! } -""" -This type contains metadata, content and related data of Chapter type, which is a container for Article type. A Chapter belong to a Topic. -""" -type Chapter implements Node { - """Unique id of this chapter.""" - id: ID! - - """Title of this chapter.""" - title: String! - - """Description of this chapter.""" - description: String - - """Number articles included in this chapter.""" - articleCount: Int! +input ArticleVersionsInput { + after: String + first: Int +} - """Articles included in this Chapter""" - articles: [Article!] +type ArticleVersionsConnection implements Connection { + totalCount: Int! + pageInfo: PageInfo! + edges: [ArticleVersionEdge]! +} - """The topic that this Chapter belongs to.""" - topic: Topic! +type ArticleVersionEdge { + node: ArticleVersion! + cursor: String! } -""" -This type contains metadata, content and related data of a topic, which is a container for Article and Chapter types. -""" -type Topic implements Node { - """Unique id of this topic.""" +type ArticleVersion implements Node { id: ID! - - """Title of this topic.""" + dataHash: String + mediaHash: String title: String! - - """Cover of this topic.""" - cover: String - - """Description of this topic.""" + summary: String! + contents: ArticleContents! + translation(input: TranslationArgs): ArticleTranslation + createdAt: DateTime! description: String - - """Number of chapters included in this topic.""" - chapterCount: Int! - - """Number articles included in this topic.""" - articleCount: Int! - - """List of chapters included in this topic.""" - chapters: [Chapter!] - - """List of articles included in this topic.""" - articles: [Article!] - - """Author of this topic.""" - author: User! - - """Whether this topic is public or not.""" - public: Boolean! - - """Latest published article on this topic""" - latestArticle: Article } """This type contains content, count and related data of an article tag.""" @@ -655,17 +619,6 @@ type ArticleEdge { node: Article! } -type TopicConnection implements Connection { - totalCount: Int! - pageInfo: PageInfo! - edges: [TopicEdge!] -} - -type TopicEdge { - cursor: String! - node: Topic! -} - type TagConnection implements Connection { totalCount: Int! pageInfo: PageInfo! @@ -694,7 +647,8 @@ type ArticleDonation { } input ArticleInput { - mediaHash: String! + mediaHash: String + shortHash: String } input PublishArticleInput { @@ -711,6 +665,7 @@ input EditArticleInput { """deprecated, use pinned instead""" sticky: Boolean pinned: Boolean + title: String summary: String tags: [String!] content: String @@ -723,6 +678,9 @@ input EditArticleInput { requestForDonation: String replyToDonator: String + """revision description""" + description: String + """whether publish to ISCN""" iscnPublish: Boolean @@ -741,32 +699,6 @@ input ReadArticleInput { id: ID! } -input PutTopicInput { - id: ID - title: String - description: String - cover: ID - public: Boolean - articles: [ID!] - chapters: [ID!] -} - -input PutChapterInput { - id: ID - title: String - description: String - topic: ID - articles: [ID!] -} - -input DeleteTopicsInput { - ids: [ID!]! -} - -input SortTopicsInput { - ids: [ID!]! -} - input ToggleRecommendInput { id: ID! enabled: Boolean! @@ -1018,11 +950,13 @@ type User implements Node { """Articles authored by current user.""" articles(input: UserArticlesInput!): ArticleConnection! - """Topics created by current user.""" - topics(input: TopicInput!): TopicConnection! - """collections authored by current user.""" collections(input: ConnectionArgs!): CollectionConnection! + + """user latest articles or collections""" + latestWorks: [PinnableWork!]! + + """user pinned articles or collections""" pinnedWorks: [PinnableWork!]! """Tags by by usage order of current user.""" @@ -1937,6 +1871,8 @@ type OSS { seedingUsers(input: ConnectionArgs!): UserConnection! badgedUsers(input: BadgedUsersInput!): UserConnection! restrictedUsers(input: ConnectionArgs!): UserConnection! + reports(input: ConnectionArgs!): ReportConnection! + icymiTopics(input: ConnectionArgs!): IcymiTopicConnection! } """This type contains type, link and related data of an asset.""" @@ -2011,6 +1947,25 @@ type UserRestriction { createdAt: DateTime! } +type Report implements Node { + id: ID! + reporter: User! + target: Response! + reason: ReportReason! + createdAt: DateTime! +} + +type ReportConnection implements Connection { + totalCount: Int! + pageInfo: PageInfo! + edges: [ReportEdge!] +} + +type ReportEdge { + cursor: String! + node: Report! +} + input NodeInput { id: ID! } @@ -2157,6 +2112,11 @@ input PutRestrictedUsersInput { restrictions: [UserRestrictionType!]! } +input SubmitReportInput { + targetId: ID! + reason: ReportReason! +} + enum SearchTypes { Article User @@ -2209,7 +2169,6 @@ enum AssetType { circleCover collectionCover announcementCover - topicCover } enum EntityType { @@ -2219,7 +2178,6 @@ enum EntityType { user circle announcement - topic collection } @@ -2269,6 +2227,51 @@ enum UserRestrictionType { articleNewest } +enum ReportReason { + tort + illegal_advertising + discrimination_insult_hatred + pornography_involving_minors + other +} + +type IcymiTopic implements Node { + id: ID! + title: String! + articles: [Article!]! + pinAmount: Int! + note: String + state: IcymiTopicState! + publishedAt: DateTime + archivedAt: DateTime +} + +enum IcymiTopicState { + published + editing + archived +} + +type IcymiTopicConnection implements Connection { + totalCount: Int! + pageInfo: PageInfo! + edges: [IcymiTopicEdge!]! +} + +type IcymiTopicEdge { + cursor: String! + node: IcymiTopic! +} + +input PutIcymiTopicInput { + id: ID + title: String + articles: [ID!] + pinAmount: Int + note: String + state: IcymiTopicState +} + enum CacheControlScope { PUBLIC PRIVATE @@ -2290,6 +2293,9 @@ type Recommendation { """'In case you missed it' recommendation.""" icymi(input: ConnectionArgs!): ArticleConnection! + """'In case you missed it' topic.""" + icymiTopic: IcymiTopic + """Global tag list, sort by activities in recent 14 days.""" tags(input: RecommendInput!): TagConnection! @@ -2317,12 +2323,6 @@ input RecommendInput { type: AuthorsType } -input TopicInput { - after: String - first: Int - filter: FilterInput -} - input FilterInput { """index of list, min: 0, max: 49""" random: Int @@ -2330,9 +2330,6 @@ input FilterInput { """Used in RecommendInput""" followed: Boolean - """Used in User.topics""" - public: Boolean - """Used in User Articles filter, by tags or by time range, or both""" tagIds: [ID!] inRangeStart: DateTime diff --git a/src/common/enums/action.ts b/src/common/enums/action.ts index 1bb631760..29bc901d7 100644 --- a/src/common/enums/action.ts +++ b/src/common/enums/action.ts @@ -6,6 +6,10 @@ export enum USER_ACTION { downVote = 'down_vote', } +export enum ARTICLE_ACTION { + subscribe = 'subscribe', +} + export enum TAG_ACTION { follow = 'follow', pin = 'pin', diff --git a/src/common/enums/file.ts b/src/common/enums/file.ts index cead3c7a0..247a2c942 100644 --- a/src/common/enums/file.ts +++ b/src/common/enums/file.ts @@ -5,7 +5,6 @@ export const COVER_ASSET_TYPE = { circleCover: 'circleCover', collectionCover: 'collectionCover', announcementCover: 'announcementCover', - topicCover: 'topicCover', } as const export const AVATAR_ASSET_TYPE = { diff --git a/src/common/enums/index.ts b/src/common/enums/index.ts index 0692380df..312acf4c1 100644 --- a/src/common/enums/index.ts +++ b/src/common/enums/index.ts @@ -31,7 +31,7 @@ export * from './appreciation' export * from './metrics' export * from './badges' -export const GRAPHQL_COST_LIMIT = 10e3 +export const GRAPHQL_COST_LIMIT = 25e3 export const GRAPHQL_INPUT_LENGTH_LIMIT = 100 export const BCRYPT_ROUNDS = 12 @@ -89,12 +89,6 @@ export const PUBLISH_STATE = { published: 'published', } as const -export enum PIN_STATE { - pinned = 'pinned', - pinning = 'pinning', - failed = 'failed', -} - export const CIRCLE_STATE = { active: 'active', archived: 'archived', @@ -159,6 +153,7 @@ export const OAUTH_CALLBACK_ERROR_CODE = { export enum NODE_TYPES { Article = 'Article', + ArticleVersion = 'ArticleVersion', Comment = 'Comment', Draft = 'Draft', User = 'User', @@ -166,10 +161,10 @@ export enum NODE_TYPES { Appreciation = 'Appreciation', Transaction = 'Transaction', Circle = 'Circle', - Topic = 'Topic', - Chapter = 'Chapter', Collection = 'Collection', + Report = 'Report', + IcymiTopic = 'IcymiTopic', SkippedListItem = 'SkippedListItem', Price = 'Price', Invitation = 'Invitation', @@ -208,7 +203,15 @@ export const SKIPPED_LIST_ITEM_TYPES: Record = { AGENT_HASH: 'agent_hash', EMAIL: 'email', DOMAIN: 'domain', -} +} as const + +export const MATTERS_CHOICE_TOPIC_STATE = { + published: 'published', + editing: 'editing', + archived: 'archived', +} as const + +export const MATTERS_CHOICE_TOPIC_VALID_PIN_AMOUNTS = [3, 6] export const LOCAL_STRIPE = { host: 'localhost', @@ -230,12 +233,15 @@ export enum ActivityType { UserAddArticleTagActivity = 'UserAddArticleTagActivity', } -export const MAX_ARTICE_TITLE_LENGTH = 100 -export const MAX_ARTICE_SUMMARY_LENGTH = 200 +export const MAX_ARTICLE_TITLE_LENGTH = 100 +export const MAX_ARTICLE_SUMMARY_LENGTH = 200 export const MAX_ARTICLE_CONTENT_LENGTH = 50e3 export const MAX_ARTICLES_PER_CONNECTION_LIMIT = 3 export const MAX_ARTICLE_CONTENT_REVISION_LENGTH = 50 +export const MAX_ARTICLE_COMMENT_LENGTH = 1200 +export const MAX_COMMENT_EMPTY_PARAGRAPHS = 1 + export const MAX_TAGS_PER_ARTICLE_LIMIT = 3 export const TAGS_RECOMMENDED_LIMIT = 100 @@ -243,3 +249,5 @@ export const MAX_TAG_CONTENT_LENGTH = 50 export const MAX_TAG_DESCRIPTION_LENGTH = 200 export const MAX_PINNED_WORKS_LIMIT = 3 + +export const LATEST_WORKS_NUM = 4 diff --git a/src/common/enums/payment.ts b/src/common/enums/payment.ts index ff985cfb6..36479fa80 100644 --- a/src/common/enums/payment.ts +++ b/src/common/enums/payment.ts @@ -1,4 +1,4 @@ -import { optimism, optimismSepolia, polygon, polygonMumbai } from 'viem/chains' +import { optimism, optimismSepolia, polygon } from 'viem/chains' import { environment, isProd } from 'common/environment' import { GQLChain } from 'definitions' @@ -51,19 +51,21 @@ export const BLOCKCHAIN: { [key in GQLChain]: GQLChain } = { export const BLOCKCHAIN_CHAINNAME: { [chainId: string]: GQLChain } = { [polygon.id]: BLOCKCHAIN.Polygon, - [polygonMumbai.id]: BLOCKCHAIN.Polygon, [optimism.id]: BLOCKCHAIN.Optimism, [optimismSepolia.id]: BLOCKCHAIN.Optimism, } as const export const BLOCKCHAIN_CHAINID = { - [BLOCKCHAIN.Polygon]: isProd ? polygon.id + '' : polygonMumbai.id + '', + // Temporarily pause support for the Polygon testnet + // since Polygon Mumbai has migrated to Polygon Amoy + // and the `Curation` contract on Mumbai is being deprecated. + // For `Logbook` contract, we can migrate it to Polygon Amoy if needed. + [BLOCKCHAIN.Polygon]: polygon.id + '', [BLOCKCHAIN.Optimism]: isProd ? optimism.id + '' : optimismSepolia.id + '', } as const export const BLOCKCHAIN_RPC: { [chainId: string]: string } = { [polygon.id]: `https://polygon-mainnet.g.alchemy.com/v2/${environment.alchemyApiKey}`, - [polygonMumbai.id]: `https://polygon-mumbai.g.alchemy.com/v2/${environment.alchemyApiKey}`, [optimism.id]: `https://opt-mainnet.g.alchemy.com/v2/${environment.alchemyApiKey}`, [optimismSepolia.id]: `https://opt-sepolia.g.alchemy.com/v2/${environment.alchemyApiKey}`, } diff --git a/src/common/enums/queue.ts b/src/common/enums/queue.ts index 990439af9..57992ed74 100644 --- a/src/common/enums/queue.ts +++ b/src/common/enums/queue.ts @@ -10,7 +10,6 @@ export const QUEUE_JOB = { // Publication publishArticle: 'publishArticle', publishPendingDrafts: 'publishPendingDrafts', - verifyIPFSPinHashes: 'verifyIPFSPinHashes', // refresh IPNS Feed refreshIPNSFeed: 'refreshIPNSFeed', @@ -47,7 +46,6 @@ export const QUEUE_NAME = { appreciation: 'appreciation', revision: 'revision', asset: 'asset', - ipfs: 'ipfs', } export const QUEUE_CONCURRENCY = { diff --git a/src/common/enums/sqs.ts b/src/common/enums/sqs.ts index 39d229607..2bd17e100 100644 --- a/src/common/enums/sqs.ts +++ b/src/common/enums/sqs.ts @@ -1,7 +1,6 @@ import { environment } from 'common/environment' export const QUEUE_URL = { - ipfsArticles: environment?.awsIpfsArticlesQueueUrl, archiveUser: environment?.awsArchiveUserQueueUrl, // likecoin likecoinLike: environment?.awsLikecoinLikeUrl, diff --git a/src/common/enums/table.ts b/src/common/enums/table.ts index d59928e29..b246a2eb6 100644 --- a/src/common/enums/table.ts +++ b/src/common/enums/table.ts @@ -11,6 +11,7 @@ export enum VIEW { export enum MATERIALIZED_VIEW { tag_count_materialized = 'tag_count_materialized', + tags_lasts_view_materialized = 'mat_views.tags_lasts_view_materialized', user_reader_materialized = 'user_reader_materialized', featured_comment_materialized = 'featured_comment_materialized', curation_tag_materialized = 'curation_tag_materialized', diff --git a/src/common/environment.ts b/src/common/environment.ts index 940dff507..894f4c158 100644 --- a/src/common/environment.ts +++ b/src/common/environment.ts @@ -41,8 +41,6 @@ export const environment = { awsAccessKey: process.env.MATTERS_AWS_ACCESS_KEY, awsS3Endpoint: process.env.MATTERS_AWS_S3_ENDPOINT, awsS3Bucket: process.env.MATTERS_AWS_S3_BUCKET || '', - awsIpfsArticlesQueueUrl: - process.env.MATTERS_AWS_IPFS_ARTICLES_QUEUE_URL || '', awsMailQueueUrl: process.env.MATTERS_AWS_MAIL_QUEUE_URL || '', awsExpressMailQueueUrl: process.env.MATTERS_AWS_EXPRESS_MAIL_QUEUE_URL || '', awsArchiveUserQueueUrl: process.env.MATTERS_AWS_ARCHIVE_USER_QUEUE_URL || '', @@ -51,7 +49,6 @@ export const environment = { process.env.MATTERS_AWS_LIKECOIN_SEND_PV_QUEUE_URL || '', awsLikecoinUpdateCivicLikerCache: process.env.MATTERS_AWS_LIKECOIN_UPDATE_CIVIC_LIKER_CACHE_QUEUE_URL || '', - awsArticlesSnsTopic: process.env.MATTERS_AWS_ARTICLES_SNS_TOPIC || '', tsQiServerUrl: process.env.MATTERS_TSQI_SERVER_URL || '', awsCloudFrontEndpoint: process.env.MATTERS_AWS_CLOUD_FRONT_ENDPOINT, cloudflareAccountId: process.env.MATTERS_CLOUDFLARE_ACCOUNT_ID, @@ -59,9 +56,6 @@ export const environment = { cloudflareApiToken: process.env.MATTERS_CLOUDFLARE_API_TOKEN, cloudflareTurnstileSecretKey: process.env.MATTERS_CLOUDFLARE_TURNSTILE_SECRET_KEY, - verifyCaptchaTokenThresholds: JSON.parse( - process.env.MATTERS_VERIFY_CAPTCHA_TOKENS_THRESHOLDS || '[0.5, 1.0]' - ), pgHost: process.env.MATTERS_PG_HOST, pgUser: process.env.MATTERS_PG_USER, pgPassword: process.env.MATTERS_PG_PASSWORD, @@ -102,7 +96,6 @@ export const environment = { sentryDsn: process.env.MATTERS_SENTRY_DSN, gcpProjectId: process.env.MATTERS_GCP_PROJECT_ID, translateCertPath: process.env.MATTERS_TRANSLATE_CREDENTIAL_PATH, - recaptchaSecret: process.env.MATTERS_RECAPTCHA_KEY, OICDPrivateKey, likecoinOAuthClientName: process.env.MATTERS_LIKECOIN_OAUTH_CLIENT_NAME || '', likecoinMigrationApiURL: process.env.MATTERS_LIKECOIN_MIGRATION_API_URL || '', diff --git a/src/common/errors.ts b/src/common/errors.ts index 078d58be0..9ee3c7667 100644 --- a/src/common/errors.ts +++ b/src/common/errors.ts @@ -32,6 +32,12 @@ export class UserInputError extends GraphQLError { } } +export class InvalidCursorError extends GraphQLError { + public constructor(message: string) { + super(message, { extensions: { code: 'INVALID_CURSOR_ERROR' } }) + } +} + export class ActionLimitExceededError extends GraphQLError { public constructor(message: string) { super(message, { extensions: { code: 'ACTION_LIMIT_EXCEEDED' } }) diff --git a/src/common/utils/__test__/content.test.ts b/src/common/utils/__test__/content.test.ts index adfc69cdb..c4d69f859 100644 --- a/src/common/utils/__test__/content.test.ts +++ b/src/common/utils/__test__/content.test.ts @@ -32,7 +32,7 @@ test('countWords', async () => { }, { data: '

Hello космонавты. Сегодня на орбите Crescent. Сразу скажу они вроде как ни чего еще не обещали за тестирование их платформы, но мы как криптаны поможем проверить устойчивость их платформы и заодно впишем свой кошелек, кто его знает.

Полумесяц

Crescent предоставит подключенную функциональность DeFi для экосистемы Cosmos, чтобы повысить эффективность использования капитала и эффективно управлять рисками. Он фокусируется на трех основных функциях; сrescent DEX, повышение crescent, производные crescent.

Не буду подробно останавливаться на этом а перейду сразу к описанию тестнета, думаю и так все понятно, но если у вас есть желание и время почитать то вот вам ссылка где подробно все указано.

Для начала переходим по ссылке на саму платформу.

Connect Wallet
Подтверждаем для добавления всех тестовый сетей.

После подключаем кошелек Keplr и подписываем разрешения добавления тестовых сетей (bombay-12, cosmoshub-testnet, mooncat-1-1)

Faucet

Переходим в верхнем правом углу в Faucet и берем с крана тестовых токенов.

Terra Faucet

Хочу сразу сказать что вам придется скорее всего подождать чтобы получить монеты CRE самой площадки и монет ATOM. Я получил свои токены CRE только через день грубо говоря, поэтому придется скорее подождать чтобы оплатить те же транзакции в токенах CRE. Можете сегодня например пару раз потыкать для получения тестовых токенов и после того как придут уже делать дальнейшие действия.

Переходим в Portfolio и перекидываем свои токены LUNA из сети bombay-12 в сеть mooncat1-1
Deposit
Проверяем баланс
Переходим в раздел Farm
Добавляем ликвидность в LP токенах cCRE-CRE
Пишем сколько хотим добавить и затем DEPOSIT
Тут можем наблюдать такой знак, это означает что наши токены не работают. Нажимаем на Manage
Нажимаем в появившемся окне Farm, далее сколько процентов хотим внести и Farm. Подтверждаем на кошельке
Все наши монеты работают.
Переходим в раздел Staking, пишем сколько монет CRE хотим застейкать и нажимаем на STAKE, подтверждаем на кошельке. Вместо застейканных CRE нам дают монет bCRE
Можем полученные от стейкинга монеты bCRE застейкать к паре LUNA-bCRE
Deposit, сумма обеих монет и нажимаем на DEPOSIT
Тут опять же чтобы наши монеты начали работать нажимаем на Manage и проделываем выше перечисленные действия.
Все наши вложения работают.

На этом пока все, платформа протестирована, токены работают а дадут ли нам что нибудь за эти простые действия решать команде но пальчики протестировать и привязать лишний раз кошелек не помешало.

Всем мира!

', - count: 376, + count: 373, }, ] diff --git a/src/common/utils/__test__/counter.test.ts b/src/common/utils/__test__/counter.test.ts index e4de9702d..8a955b372 100644 --- a/src/common/utils/__test__/counter.test.ts +++ b/src/common/utils/__test__/counter.test.ts @@ -1,13 +1,9 @@ -import { Redis } from 'ioredis' -import { RedisMemoryServer } from 'redis-memory-server' +import Redis from 'ioredis-mock' import { RatelimitCounter } from 'common/utils' test('increment', async () => { - const redisServer = new RedisMemoryServer() - const redisPort = await redisServer.getPort() - const redisHost = await redisServer.getHost() - const redis = new Redis(redisPort, redisHost) + const redis = new Redis() const counter = new RatelimitCounter(redis) const key = 'test:increment' @@ -17,7 +13,4 @@ test('increment', async () => { expect(value2).toBe(2) const value = await counter.get(key) expect(value).toBe(2) - - redis.disconnect() - await redisServer.stop() }) diff --git a/src/common/utils/connections.ts b/src/common/utils/connections.ts index d54de2e9a..e17e13620 100644 --- a/src/common/utils/connections.ts +++ b/src/common/utils/connections.ts @@ -2,7 +2,6 @@ import { connectionFromArraySlice } from 'graphql-relay' import { Base64 } from 'js-base64' import { DEFAULT_TAKE_PER_PAGE } from 'common/enums' -import { Item } from 'definitions' export type ConnectionCursor = string @@ -13,11 +12,6 @@ export interface ConnectionArguments { last?: number } -export interface ConnectionHelpers { - offset: number - totalCount: number -} - export interface Connection { totalCount: number edges: Array> @@ -38,9 +32,6 @@ export interface PageInfo { const PREFIX = 'arrayconnection' -export const cursorToOffset = (cursor: ConnectionCursor | undefined): number => - cursor ? parseInt(Base64.decode(cursor).split(':')[1], 10) : -1 - export const cursorToIndex = (cursor: ConnectionCursor | undefined): number => cursor ? parseInt(Base64.decode(cursor).split(':')[1], 10) : -1 @@ -90,22 +81,22 @@ export const connectionFromArray = ( } export const connectionFromPromisedArray = ( - dataPromise: Promise | T[], + dataPromise: Promise> | Array, args: ConnectionArguments, totalCount?: number ): Promise> => Promise.resolve(dataPromise).then((data) => - connectionFromArray(data, args, totalCount) + connectionFromArray(loadManyFilterError(data), args, totalCount) ) -export const loadManyFilterError = (items: Array) => - items.filter((item: Item | Error) => { +export const loadManyFilterError = (items: Array) => + items.filter((item) => { if (item instanceof Error) { return false } return true - }) as Item[] + }) as T[] /** * Convert GQL curosr to query keys. For example, the GQL cursor @@ -134,52 +125,41 @@ export const keysToCursor = ( /** * Construct a GQL connection using query keys mechanism. Query keys are - * composed of `offset` and `idCursor`. `offset` is for managing connection - * like `merge`, and `idCursor` is for SQL querying. - * + * composed of `offset` and `idCursor`. + * `offset` is for managing connection like `merge`, + * and `idCursor` is for SQL querying. + * (for detail explain see https://github.com/thematters/matters-server/pull/922#discussion_r409256544) */ export const connectionFromArrayWithKeys = ( data: T[], args: ConnectionArguments, - totalCount?: number + totalCount: number ): Connection => { - if (totalCount) { - const { after } = args - const keys = cursorToKeys(after) - - const edges = data.map((value, index) => ({ - cursor: keysToCursor( - index + keys.offset + 1, - (value as any).__cursor || value.id - ), - node: value, - })) - - const firstEdge = edges[0] - const lastEdge = edges[edges.length - 1] + const { after } = args + const keys = cursorToKeys(after) - return { - edges, - totalCount, - pageInfo: { - startCursor: firstEdge ? firstEdge.cursor : '', - endCursor: lastEdge ? lastEdge.cursor : '', - hasPreviousPage: after ? keys.offset >= 0 : false, - hasNextPage: lastEdge - ? cursorToKeys(lastEdge.cursor).offset + 1 < totalCount - : false, - }, - } - } + const edges = data.map((value, index) => ({ + cursor: keysToCursor( + index + keys.offset + 1, + (value as any).__cursor || value.id + ), + node: value, + })) - const connections = connectionFromArraySlice(data, args, { - sliceStart: 0, - arrayLength: data.length, - }) + const firstEdge = edges[0] + const lastEdge = edges[edges.length - 1] return { - ...connections, - totalCount: data.length, + edges, + totalCount, + pageInfo: { + startCursor: firstEdge ? firstEdge.cursor : '', + endCursor: lastEdge ? lastEdge.cursor : '', + hasPreviousPage: after ? keys.offset >= 0 : false, + hasNextPage: lastEdge + ? cursorToKeys(lastEdge.cursor).offset + 1 < totalCount + : false, + }, } } diff --git a/src/common/utils/getViewer.ts b/src/common/utils/getViewer.ts index 305d536d0..fed218410 100644 --- a/src/common/utils/getViewer.ts +++ b/src/common/utils/getViewer.ts @@ -16,7 +16,7 @@ import { environment } from 'common/environment' import { ForbiddenByStateError, TokenInvalidError } from 'common/errors' import { getLogger } from 'common/logger' import { clearCookie, getLanguage } from 'common/utils' -import { OAuthService, SystemService, UserService } from 'connectors' +import { OAuthService, SystemService, AtomService } from 'connectors' const logger = getLogger('utils-auth') @@ -82,7 +82,7 @@ const getUser = async ( agentHash: string, connections: Connections ) => { - const userService = new UserService(connections) + const atomService = new AtomService(connections) const systemService = new SystemService(connections) try { @@ -90,7 +90,7 @@ const getUser = async ( const source = jwt.verify(token, environment.jwtSecret) as { id: string } - const user = await userService.dataloader.load(source.id) + const user = await atomService.userIdLoader.load(source.id) if (user.state === USER_STATE.archived) { if (agentHash) { diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index bce937cd6..824211d0d 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -24,6 +24,7 @@ export * from './IERC1271' export * from './genDisplayName' export * from './counter' export * from './verify' +export * from './nanoid' /** * Make a valid user name based on a given email address. It removes all special characters including _. @@ -39,6 +40,7 @@ export const makeUserName = (email: string): string => { return matched.join('').substring(0, 12).toLowerCase() } +// TOFIX: uuid extract logic is broken after editor upgrade export const extractAssetDataFromHtml = ( html: string, type?: 'image' | 'audio' diff --git a/src/common/utils/nanoid.ts b/src/common/utils/nanoid.ts new file mode 100644 index 000000000..284ce8463 --- /dev/null +++ b/src/common/utils/nanoid.ts @@ -0,0 +1,5 @@ +import { customAlphabet } from 'nanoid' + +const ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz' + +export const nanoid = customAlphabet(ALPHABET, 12) // ~35 years or 308M IDs needed, in order to have a 1% probability of one collision // https://zelark.github.io/nano-id-cc/ diff --git a/src/common/utils/payment.ts b/src/common/utils/payment.ts index a86fd39b7..fb06751dc 100644 --- a/src/common/utils/payment.ts +++ b/src/common/utils/payment.ts @@ -4,7 +4,8 @@ import { PAYMENT_CURRENCY, PAYMENT_PROVIDER } from 'common/enums' NP.enableBoundaryChecking(false) -export const numRound = (num: number, decPlaces = 2) => NP.round(num, decPlaces) +export const numRound = (num: number | string, decPlaces = 2) => + NP.round(num, decPlaces) export const numDivide = (num1: number, num2: number) => NP.divide(num1, num2) diff --git a/src/common/utils/text.ts b/src/common/utils/text.ts index 41b6fe941..b4f9a024e 100644 --- a/src/common/utils/text.ts +++ b/src/common/utils/text.ts @@ -1,3 +1,4 @@ +import crypto from 'crypto' import { distance } from 'fastest-levenshtein' import { OpenCC } from 'opencc' @@ -46,3 +47,6 @@ export const s2tConverter: OpenCC = new OpenCC('s2t.json') export const normalizeSearchKey = async (content: string): Promise => t2sConverter.convertPromise(stripSpaces(content.toLowerCase()) as string) + +export const genMD5 = (content: string) => + crypto.createHash('md5').update(content).digest('hex') diff --git a/src/common/utils/verify.ts b/src/common/utils/verify.ts index e1385bfbe..64748e0eb 100644 --- a/src/common/utils/verify.ts +++ b/src/common/utils/verify.ts @@ -1,18 +1,5 @@ -import { environment, isTest } from 'common/environment' -import { gcp, cfsvc } from 'connectors' - -// read MATTERS_VERIFY_CAPTCHA_TOKENS_THRESHOLDS="[0.5, 1.0]" as 2 numbers between [0,1) -// 0 to not verify token; 1 to always verify this token; -// check results of either one token pass - -const GCP_RECAPTCHA_THRESHOLD = - environment.verifyCaptchaTokenThresholds?.[0] || 1.0 -const CFSVC_TURNSTILE_THRESHOLD = - environment.verifyCaptchaTokenThresholds?.[1] || 1.0 - -// verify either gcp.recaptcha token or cfsvc.turnstile token, or both -// for a transition period, we may check both, and pass if any one pass siteverify -// after the transition period, can turn off the one no longer in use +import { isTest } from 'common/environment' +import { cfsvc } from 'connectors' // returns isHuman: boolean export async function verifyCaptchaToken(token: string, ip: string) { @@ -23,19 +10,8 @@ export async function verifyCaptchaToken(token: string, ip: string) { // suppose it's space concatenated multiple tokens const tokens = (token || '').split(' ') - return ( - ( - await Promise.allSettled( - [ - Math.random() < GCP_RECAPTCHA_THRESHOLD && - gcp.recaptcha({ token: tokens?.[0] ?? token, ip }), - Math.random() < CFSVC_TURNSTILE_THRESHOLD && - cfsvc.turnstileVerify({ - token: tokens?.[1] ?? tokens?.[0] ?? token, - ip, - }), - ] // .map((p) => p.catch((e) => false)) - ) - ).filter((r) => r.status === 'fulfilled' && r.value === true).length > 0 - ) // includes(true) + return cfsvc.turnstileVerify({ + token: tokens?.[1] ?? tokens?.[0] ?? token, + ip, + }) } diff --git a/src/connectors/__test__/articleService.test.ts b/src/connectors/__test__/articleService.test.ts index be9af6dfe..5a595cfc1 100644 --- a/src/connectors/__test__/articleService.test.ts +++ b/src/connectors/__test__/articleService.test.ts @@ -1,16 +1,21 @@ -import type { Connections } from 'definitions' +import type { Connections, Article } from 'definitions' -import { ArticleService, UserService } from 'connectors' +import { v4 } from 'uuid' -import { genConnections, closeConnections, createArticle } from './utils' +import { COMMENT_STATE, NODE_TYPES } from 'common/enums' +import { ArticleService, UserService, AtomService } from 'connectors' + +import { genConnections, closeConnections } from './utils' let articleId: string let connections: Connections let articleService: ArticleService +let atomService: AtomService beforeAll(async () => { connections = await genConnections() articleService = new ArticleService(connections) + atomService = new AtomService(connections) }, 50000) afterAll(async () => { @@ -19,27 +24,21 @@ afterAll(async () => { test('publish', async () => { // publish article to IPFS - const publishedDraft = await articleService.draftLoader.load('1') - const { mediaHash, contentHash: dataHash } = - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (await articleService.publishToIPFS(publishedDraft))! - const articlePublished = await articleService.createArticle({ - draftId: '1', + // const publishedDraft = await atomService.articleIdLoader.load('1') + // const { mediaHash, contentHash: dataHash } = + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + // (await articleService.publishToIPFS(publishedDraft))! + const [article] = await articleService.createArticle({ authorId: '1', title: 'test', - slug: 'test', cover: '1', - wordCount: 0, - summary: 'test-summary', content: '
test-html-string
', - dataHash, - mediaHash, }) - expect(mediaHash).toBeDefined() - expect(dataHash).toBeDefined() - expect(articlePublished.state).toBe('pending') + // expect(mediaHash).toBeDefined() + // expect(dataHash).toBeDefined() + expect(article.state).toBe('active') - articleId = articlePublished.id + articleId = article.id // publish to IPNS // await articleService.publishFeedToIPNS({ userName: 'test1' }) }) @@ -55,20 +54,20 @@ describe('findByAuthor', () => { expect(draftIds.length).toBeDefined() }) test('order by num of readers', async () => { - const draftIds = await articleService.findByAuthor('1', { + const articles = await articleService.findByAuthor('1', { orderBy: 'mostReaders', }) - expect(draftIds.length).toBeDefined() - expect(draftIds[0].draftId).not.toBe('1') + expect(articles.length).toBeDefined() + expect(articles[0].id).not.toBe('1') await connections.knex('article_ga4_data').insert({ articleId: '1', totalUsers: '1', dateRange: '[2023-10-24,2023-10-24]', }) - const draftIds2 = await articleService.findByAuthor('1', { + const articles2 = await articleService.findByAuthor('1', { orderBy: 'mostReaders', }) - expect(draftIds2[0].draftId).toBe('1') + expect(articles2[0].id).toBe('1') }) test('order by amount of appreciations', async () => { const draftIds = await articleService.findByAuthor('1', { @@ -130,7 +129,8 @@ test('findSubscriptions', async () => { }) describe('updatePinned', () => { - const getArticleFromDb = async (id: string) => articleService.baseFindById(id) + const getArticleFromDb = async (id: string) => + articleService.baseFindById(id) as Promise
test('invaild article id will throw error', async () => { await expect(articleService.updatePinned('999', '1', true)).rejects.toThrow( 'Cannot find article' @@ -184,11 +184,12 @@ describe('quicksearch', () => { expect(nodes.length).toBe(1) expect(totalCount).toBeGreaterThan(0) - // both case insensitive and chinese simplified/traditional insensitive - await createArticle( - { title: 'Uppercase', content: '', authorId: '1' }, - connections - ) + // both case insensitive and Chinese simplified/traditional insensitive + await articleService.createArticle({ + title: 'Uppercase', + content: '', + authorId: '1', + }) const { nodes: nodes2 } = await articleService.searchV3({ key: 'uppercase', take: 1, @@ -197,10 +198,11 @@ describe('quicksearch', () => { }) expect(nodes2.length).toBe(1) - await createArticle( - { title: '測試', content: '', authorId: '1' }, - connections - ) + await articleService.createArticle({ + title: '測試', + content: '', + authorId: '1', + }) const { nodes: nodes3 } = await articleService.searchV3({ key: '测试', take: 1, @@ -209,10 +211,11 @@ describe('quicksearch', () => { }) expect(nodes3.length).toBe(1) - await createArticle( - { title: '试测', content: '', authorId: '1' }, - connections - ) + await articleService.createArticle({ + title: '试测', + content: '', + authorId: '1', + }) const { nodes: nodes4 } = await articleService.searchV3({ key: '試測', take: 1, @@ -221,10 +224,11 @@ describe('quicksearch', () => { }) expect(nodes4.length).toBe(1) - await createArticle( - { title: '測测', content: '', authorId: '1' }, - connections - ) + await articleService.createArticle({ + title: '測测', + content: '', + authorId: '1', + }) const { nodes: nodes5 } = await articleService.searchV3({ key: '測测', take: 1, @@ -250,7 +254,6 @@ describe('quicksearch', () => { quicksearch: true, filter: { authorId: '2' }, }) - console.log(nodes) nodes.forEach((node) => { expect(node.authorId).toBe('2') }) @@ -273,4 +276,176 @@ test('latestArticles', async () => { oss: false, }) expect(articles.length).toBeGreaterThan(0) + expect(articles[0].id).toBeDefined() + expect(articles[0].authorId).toBeDefined() + expect(articles[0].state).toBeDefined() +}) + +describe('findResponses', () => { + const createComment = async ( + state?: keyof typeof COMMENT_STATE, + parentCommentId?: string + ) => { + return atomService.create({ + table: 'comment', + data: { + uuid: v4(), + content: 'test', + authorId: '1', + targetId: '1', + targetTypeId: '4', + type: 'article', + parentCommentId, + state: state ?? COMMENT_STATE.active, + }, + }) + } + test('do not return archived comment not having any not-archived child comments', async () => { + const res1 = await articleService.findResponses({ id: '1' }) + expect(res1.length).toBeGreaterThan(0) + + // active comment will be returned + await createComment() + const res2 = await articleService.findResponses({ id: '1' }) + expect(res2.length).toBe(res1.length + 1) + + // archived comment will not be returned + const archived = await createComment(COMMENT_STATE.archived) + const res3 = await articleService.findResponses({ id: '1' }) + expect(res3.length).toBe(res2.length) + + // archived comment w/o active/collapsed child comments will not be returned + await createComment(COMMENT_STATE.archived, archived.id) + await createComment(COMMENT_STATE.banned, archived.id) + const res4 = await articleService.findResponses({ id: '1' }) + expect(res4.length).toBe(res3.length) + + // archived comment w active/collapsed child comments will be returned + await createComment(COMMENT_STATE.active, archived.id) + const res5 = await articleService.findResponses({ id: '1' }) + expect(res5.length).toBe(res4.length + 1) + + // banned comment will not be returned + const banned = await createComment(COMMENT_STATE.archived) + const res6 = await articleService.findResponses({ id: '1' }) + expect(res6.length).toBe(res5.length) + + // banned comment w/o active/collapsed child comments will not be returned + await createComment(COMMENT_STATE.archived, banned.id) + await createComment(COMMENT_STATE.banned, banned.id) + const res7 = await articleService.findResponses({ id: '1' }) + expect(res7.length).toBe(res6.length) + + // banned comment w active/collapsed child comments will be returned + await createComment(COMMENT_STATE.collapsed, banned.id) + const res8 = await articleService.findResponses({ id: '1' }) + expect(res8.length).toBe(res7.length + 1) + }) + test('count is right', async () => { + const res = await articleService.findResponses({ id: '1' }) + expect(+res[0].totalCount).toBe(res.length) + + const res1 = await articleService.findResponses({ id: '1', first: 1 }) + expect(+res1[0].totalCount).toBe(res.length) + }) + + test('cursor works', async () => { + const res = await articleService.findResponses({ id: '1' }) + const res1 = await articleService.findResponses({ + id: '1', + after: { type: NODE_TYPES.Comment, id: res[0].entityId }, + }) + expect(res1.length).toBe(res.length - 1) + expect(+res1[0].totalCount).toBe(res.length) + }) +}) + +test('loadLatestArticleVersion', async () => { + const articleVersion = await articleService.loadLatestArticleVersion('1') + expect(articleVersion.articleId).toBe('1') +}) + +test('countArticleVersions', async () => { + const count = await articleService.countArticleVersions('1') + expect(count).toBe(1) + await articleService.createNewArticleVersion('1', '1', { content: 'test2' }) + const count2 = await articleService.countArticleVersions('1') + expect(count2).toBe(2) +}) + +describe('createNewArticleVersion', () => { + test('provide description or not', async () => { + const articleVersion = await articleService.createNewArticleVersion( + '1', + '1', + { canComment: false } + ) + expect(articleVersion.description).toBe(null) + + const articleVersion2 = await articleService.createNewArticleVersion( + '1', + '1', + { canComment: false }, + undefined + ) + expect(articleVersion2.description).toBe(null) + + const description = 'test desc' + const articleVersion3 = await articleService.createNewArticleVersion( + '1', + '1', + { canComment: false }, + description + ) + expect(articleVersion3.description).toBe(description) + }) +}) + +describe('findArticleVersions', () => { + test('return content change versions', async () => { + const [, count1] = await articleService.findArticleVersions('2') + expect(count1).toBeGreaterThan(0) + + const changedContent = 'text change' + await articleService.createNewArticleVersion('2', '2', { + content: changedContent, + }) + const [, count2] = await articleService.findArticleVersions('2') + expect(count2).toBe(count1 + 1) + + await articleService.createNewArticleVersion('2', '2', { + title: 'new title', + }) + const [, count3] = await articleService.findArticleVersions('2') + expect(count3).toBe(count2 + 1) + + await articleService.createNewArticleVersion('2', '2', { + summary: 'new summary', + }) + const [, count4] = await articleService.findArticleVersions('2') + expect(count4).toBe(count3 + 1) + + await articleService.createNewArticleVersion('2', '2', { cover: '1' }) + const [, count5] = await articleService.findArticleVersions('2') + expect(count5).toBe(count4 + 1) + + await articleService.createNewArticleVersion('2', '2', { + tags: ['new tags'], + }) + const [, count6] = await articleService.findArticleVersions('2') + expect(count6).toBe(count5 + 1) + + await articleService.createNewArticleVersion('2', '2', { + collection: ['1'], + }) + const [, count7] = await articleService.findArticleVersions('2') + expect(count7).toBe(count6 + 1) + + // create new version with no content change + await articleService.createNewArticleVersion('2', '2', { + sensitiveByAuthor: true, + }) + const [, count8] = await articleService.findArticleVersions('2') + expect(count8).toBe(count7) + }) }) diff --git a/src/connectors/__test__/atomService.test.ts b/src/connectors/__test__/atomService.test.ts new file mode 100644 index 000000000..169255baf --- /dev/null +++ b/src/connectors/__test__/atomService.test.ts @@ -0,0 +1,44 @@ +import type { Connections } from 'definitions' + +import { AtomService } from 'connectors' + +import { genConnections, closeConnections } from './utils' + +let connections: Connections +let atomService: AtomService + +beforeAll(async () => { + connections = await genConnections() + atomService = new AtomService(connections) +}, 30000) + +afterAll(async () => { + await closeConnections(connections) +}) + +test('find customer', async () => { + // only return cardLast4 + const customer = await atomService.findFirst({ + table: 'customer', + where: { + id: '1', + }, + }) + expect(customer).toBeDefined() + expect(customer?.cardLast4).toBeDefined() + expect(customer?.card_last_4).toBeUndefined() + + // cardLast4 can not be used as parameter + expect( + atomService.findFirst({ + table: 'customer', + where: { cardLast4: customer.cardLast4 }, + }) + ).rejects.toThrow() + + const customer2 = await atomService.findFirst({ + table: 'customer', + where: { card_last_4: customer.cardLast4 }, + }) + expect(customer2).toBeDefined() +}) diff --git a/src/connectors/__test__/collectionService.test.ts b/src/connectors/__test__/collectionService.test.ts index 320320129..cc5d1926c 100644 --- a/src/connectors/__test__/collectionService.test.ts +++ b/src/connectors/__test__/collectionService.test.ts @@ -1,15 +1,17 @@ import type { Connections } from 'definitions' -import { CollectionService } from 'connectors' +import { CollectionService, AtomService } from 'connectors' import { genConnections, closeConnections } from './utils' let collectionService: CollectionService +let atomService: AtomService let connections: Connections beforeAll(async () => { connections = await genConnections() collectionService = new CollectionService(connections) + atomService = new AtomService(connections) }, 30000) afterAll(async () => { @@ -159,7 +161,7 @@ test('deleteCollectionArticles', async () => { }) test('loadByIds', async () => { - const res = await collectionService.loadByIds([]) + const res = await atomService.collectionIdLoader.loadMany([]) expect(res.length).toBe(0) const { id: id1 } = await collectionService.createCollection({ @@ -170,7 +172,7 @@ test('loadByIds', async () => { authorId: '1', title: 'test', }) - const res2 = await collectionService.loadByIds([id1, id2]) + const res2 = await atomService.collectionIdLoader.loadMany([id1, id2]) expect(res2.length).toBe(2) // eslint-disable-next-line @typescript-eslint/no-explicit-any expect((res2[0] as any).id).toBe(id1) @@ -352,6 +354,46 @@ describe('findPinnedByAuthor', () => { }) }) +describe('findByAuthor', () => { + test('empty', async () => { + const records = await collectionService.findByAuthor('3') + expect(records.length).toBe(0) + }) + test('success', async () => { + await collectionService.createCollection({ + authorId: '3', + title: 'test', + }) + const records = await collectionService.findByAuthor('3') + expect(records.length).toBe(1) + + await collectionService.createCollection({ + authorId: '3', + title: 'test2', + }) + + const records2 = await collectionService.findByAuthor('3', { take: 1 }) + expect(records2.length).toBe(1) + }) + test('filter empty collections', async () => { + const records = await collectionService.findByAuthor('3', { take: 1 }, true) + expect(records.length).toBe(0) + + const collection = await collectionService.createCollection({ + authorId: '3', + title: 'test3', + }) + await collectionService.addArticles(collection.id, ['1']) + + const records2 = await collectionService.findByAuthor( + '3', + { take: 1 }, + true + ) + expect(records2.length).toBe(1) + }) +}) + test('updatePinned', async () => { const { id } = await collectionService.createCollection({ authorId: '1', diff --git a/src/connectors/__test__/commentService.test.ts b/src/connectors/__test__/commentService.test.ts new file mode 100644 index 000000000..141edffc7 --- /dev/null +++ b/src/connectors/__test__/commentService.test.ts @@ -0,0 +1,201 @@ +import { v4 as uuidv4 } from 'uuid' +import type { Connections } from 'definitions' + +import { CommentService, AtomService } from 'connectors' + +import { genConnections, closeConnections } from './utils' + +let connections: Connections +let atomService: AtomService +let commentService: CommentService + +beforeAll(async () => { + connections = await genConnections() + atomService = new AtomService(connections) + commentService = new CommentService(connections) +}, 50000) + +afterAll(async () => { + await closeConnections(connections) +}) + +describe('find subcomments by parent comment id', () => { + test('found nothing', async () => { + const [comments, count] = await commentService.findByParent({ id: '100' }) + expect(comments).toEqual([]) + expect(count).toBe(0) + }) + test('found', async () => { + const [comments, count] = await commentService.findByParent({ id: '1' }) + expect(comments.length).toBeGreaterThan(0) + expect(count).toBeGreaterThan(0) + }) +}) + +describe('find comments', () => { + test('filter archived/banned comments (except those which have active sub comments) by default', async () => { + const { id: targetTypeId } = await atomService.findFirst({ + table: 'entity_type', + where: { table: 'article' }, + }) + const [comments, count] = await commentService.find({ + where: { + type: 'article', + targetId: '1', + targetTypeId, + parentCommentId: null, + }, + }) + comments.forEach((comment) => { + expect(comment.type).toBe('article') + expect(comment.targetId).toBe('1') + expect(comment.targetTypeId).toBe(targetTypeId) + expect(comment.parentCommentId).toBeNull() + }) + expect(count).toBeGreaterThan(0) + + // archived/banned comments should be filtered + const archived = await atomService.create({ + table: 'comment', + data: { + type: 'article', + targetId: '1', + targetTypeId, + parentCommentId: null, + state: 'archived', + uuid: uuidv4(), + authorId: '1', + }, + }) + const banned = await atomService.create({ + table: 'comment', + data: { + type: 'article', + targetId: '1', + targetTypeId, + parentCommentId: null, + state: 'banned', + uuid: uuidv4(), + authorId: '1', + }, + }) + const [comments2, count2] = await commentService.find({ + where: { + type: 'article', + targetId: '1', + targetTypeId, + parentCommentId: null, + }, + }) + expect(comments2.map((c) => c.id)).not.toContain(archived.id) + expect(comments2.map((c) => c.id)).not.toContain(banned.id) + expect(count2).toBe(count) + + // archived/banned comments should be included if they have active sub comments + await atomService.create({ + table: 'comment', + data: { + type: 'article', + targetId: '1', + targetTypeId, + parentCommentId: archived.id, + state: 'archived', + uuid: uuidv4(), + authorId: '1', + }, + }) + await atomService.create({ + table: 'comment', + data: { + type: 'article', + targetId: '1', + targetTypeId, + parentCommentId: banned.id, + state: 'banned', + uuid: uuidv4(), + authorId: '1', + }, + }) + const [comments3, count3] = await commentService.find({ + where: { + type: 'article', + targetId: '1', + targetTypeId, + parentCommentId: null, + }, + }) + expect(comments3.map((c) => c.id)).not.toContain(archived.id) + expect(comments3.map((c) => c.id)).not.toContain(banned.id) + expect(count3).toBe(count) + + await atomService.create({ + table: 'comment', + data: { + type: 'article', + targetId: '1', + targetTypeId, + parentCommentId: archived.id, + state: 'active', + uuid: uuidv4(), + authorId: '1', + }, + }) + await atomService.create({ + table: 'comment', + data: { + type: 'article', + targetId: '1', + targetTypeId, + parentCommentId: banned.id, + state: 'collapsed', + uuid: uuidv4(), + authorId: '1', + }, + }) + const [comments4, count4] = await commentService.find({ + where: { + type: 'article', + targetId: '1', + targetTypeId, + parentCommentId: null, + }, + }) + expect(comments4.map((c) => c.id)).toContain(archived.id) + expect(comments4.map((c) => c.id)).toContain(banned.id) + expect(count4).toBe(count + 2) + + // when state is provided, filter by state + const [comments5, _] = await commentService.find({ + where: { + type: 'article', + targetId: '1', + targetTypeId, + parentCommentId: null, + state: 'archived', + }, + }) + expect(comments5.map((c) => c.id)).toContain(archived.id) + comments5.forEach((comment) => { + expect(comment.state).toBe('archived') + }) + }) + test('return all comments when parentCommentId not specified', async () => { + const { id: targetTypeId } = await atomService.findFirst({ + table: 'entity_type', + where: { table: 'article' }, + }) + const [comments, _] = await commentService.find({ + where: { + type: 'article', + targetId: '1', + targetTypeId, + state: 'archived', + }, + }) + const parentCommentIds = comments.map((c) => c.parentCommentId) + expect(parentCommentIds).toContain(null) + expect(parentCommentIds.filter((id) => id !== null).length).toBeGreaterThan( + 0 + ) + }) +}) diff --git a/src/connectors/__test__/curationContract.test.ts b/src/connectors/__test__/curationContract.test.ts index 4a9d38aaa..19b581c1c 100644 --- a/src/connectors/__test__/curationContract.test.ts +++ b/src/connectors/__test__/curationContract.test.ts @@ -1,14 +1,14 @@ -import { polygonMumbai } from 'viem/chains' +import { optimismSepolia } from 'viem/chains' -import { BLOCKCHAIN, BLOCKCHAIN_CHAINID } from 'common/enums' -import { contract } from 'common/environment' import { CurationContract } from 'connectors/blockchain' +import { contract } from 'common/environment' +import { BLOCKCHAIN, BLOCKCHAIN_CHAINID } from 'common/enums' describe('curationContract', () => { test('compute topics correctly', async () => { const curation = new CurationContract( - polygonMumbai.id.toString(), - contract.Polygon.curationAddress + optimismSepolia.id.toString(), + contract.Optimism.curationAddress ) expect(curation.erc20TokenCurationEventTopic[0]).toBe( '0xc2e41b3d49bbccbac6ceb142bad6119608adf4f1ee1ca5cc6fc332e0ca2fc602' @@ -20,8 +20,8 @@ describe('curationContract', () => { test.skip('fetchLogs correctly', async () => { jest.setTimeout(0) const curation = new CurationContract( - BLOCKCHAIN_CHAINID[BLOCKCHAIN.Polygon], - contract.Polygon.curationAddress + BLOCKCHAIN_CHAINID[BLOCKCHAIN.Optimism], + contract.Optimism.curationAddress ) const logs = await curation.fetchLogs(BigInt(28675517), BigInt(28797000)) console.log(logs) @@ -29,8 +29,8 @@ describe('curationContract', () => { test.skip('fetchTxReceipt correctly', async () => { jest.setTimeout(0) const curation = new CurationContract( - BLOCKCHAIN_CHAINID[BLOCKCHAIN.Polygon], - contract.Polygon.curationAddress + BLOCKCHAIN_CHAINID[BLOCKCHAIN.Optimism], + contract.Optimism.curationAddress ) const erc20Receipt = await curation.fetchTxReceipt( '0x1764d50fb01e04350248f6a4e30dff3839880f50af26de3e0b78657a46c4118f' diff --git a/src/connectors/__test__/notificationService.test.ts b/src/connectors/__test__/notificationService.test.ts index 7dd46e189..2a18bfbc4 100644 --- a/src/connectors/__test__/notificationService.test.ts +++ b/src/connectors/__test__/notificationService.test.ts @@ -1,18 +1,24 @@ import type { NotificationType, Connections } from 'definitions' -import { MONTH, NOTIFICATION_TYPES } from 'common/enums' -import { NotificationService, UserService } from 'connectors' +import { + MONTH, + NOTIFICATION_TYPES, + OFFICIAL_NOTICE_EXTEND_TYPE, +} from 'common/enums' +import { NotificationService, UserService, AtomService } from 'connectors' import { genConnections, closeConnections } from './utils' let connections: Connections let userService: UserService +let atomService: AtomService let notificationService: NotificationService const recipientId = '1' beforeAll(async () => { connections = await genConnections() userService = new UserService(connections) + atomService = new AtomService(connections) notificationService = new NotificationService(connections) }, 30000) @@ -126,6 +132,34 @@ const getBundleableUserNewFollowerNotice = async () => { }) return bundleables[0] } + +describe('create notice', () => { + test('article title in messages is not `undefined`', async () => { + const article = await atomService.findUnique({ + table: 'article', + where: { id: '1' }, + }) + + await notificationService.trigger({ + event: OFFICIAL_NOTICE_EXTEND_TYPE.article_banned, + entities: [{ type: 'target', entityTable: 'article', entity: article }], + recipientId: article.authorId, + }) + await notificationService.trigger({ + event: OFFICIAL_NOTICE_EXTEND_TYPE.article_reported, + entities: [{ type: 'target', entityTable: 'article', entity: article }], + recipientId: article.authorId, + }) + + const notices = await notificationService.notice.findByUser({ + userId: article.authorId, + }) + + expect(notices[0].message).not.toContain('undefined') + expect(notices[1].message).not.toContain('undefined') + }) +}) + describe('find notice', () => { test('find one notice', async () => { const notice = await notificationService.notice.dataloader.load('1') diff --git a/src/connectors/__test__/oauthService.test.ts b/src/connectors/__test__/oauthService.test.ts index 57f88c8b1..2304c6657 100644 --- a/src/connectors/__test__/oauthService.test.ts +++ b/src/connectors/__test__/oauthService.test.ts @@ -1,9 +1,9 @@ -import type { User, Connections } from 'definitions' +import type { Connections } from 'definitions' import _ from 'lodash' import { SCOPE_PREFIX } from 'common/enums' -import { OAuthService, UserService } from 'connectors' +import { OAuthService, AtomService } from 'connectors' import { genConnections, closeConnections } from './utils' @@ -22,9 +22,10 @@ afterAll(async () => { const getClient = () => { return oauthService.getClient('test-client-id') } + const getUser = () => { - const userService = new UserService(connections) - return userService.dataloader.load('1') as Promise + const atomService = new AtomService(connections) + return atomService.userIdLoader.load('1') } describe('client', () => { @@ -89,6 +90,7 @@ describe('token', () => { refreshToken: refreshToken || 'test-token', client, user, + scope: '', }, client, user diff --git a/src/connectors/__test__/paymentService.test.ts b/src/connectors/__test__/paymentService.test.ts index 30a5f0eea..7654e3782 100644 --- a/src/connectors/__test__/paymentService.test.ts +++ b/src/connectors/__test__/paymentService.test.ts @@ -1,4 +1,4 @@ -import type { Connections } from 'definitions' +import type { Connections, EmailableUser } from 'definitions' import { BLOCKCHAIN, @@ -55,7 +55,7 @@ describe('Transaction CRUD', () => { '0xd65dc6bf6dcc111237f9acfbfa6003ea4a4d88f2e071f4307d3af81ae876f7be' const txHashUppercase = '0xD65DC6BF6DCC111237F9ACFBFA6003EA4A4D88F2E071F4307D3AF81AE876F7BE' - const chainId = BLOCKCHAIN_CHAINID[BLOCKCHAIN.Polygon] + const chainId = BLOCKCHAIN_CHAINID[BLOCKCHAIN.Optimism] test('create Transaction', async () => { const txn = await paymentService.createTransaction({ @@ -84,6 +84,8 @@ describe('Transaction CRUD', () => { expect(txn.targetId).toEqual(targetId) expect(txn.targetType).toBeDefined() expect(txn.remark).toEqual(txn.remark) + expect(txn.articleVersionId).toBeDefined() + expect(txn.articleVersionId).not.toBeNull() }) test('get or create BlockchainTransaction', async () => { // create @@ -303,14 +305,14 @@ describe('notifyDonation', () => { .dynamic_template_data.tx.donationCount const articleService = new ArticleService(connections) - const sender = await userService.create({ + const sender = (await userService.create({ userName: 'sender', email: 'sender@example.com', - }) - const recipient = await userService.create({ + })) as EmailableUser + const recipient = (await userService.create({ userName: 'recipient', email: 'recipient@example.com', - }) + })) as EmailableUser const tx = await createDonationTx( { senderId: sender.id, diff --git a/src/connectors/__test__/recommendationService.test.ts b/src/connectors/__test__/recommendationService.test.ts new file mode 100644 index 000000000..8e22eb3db --- /dev/null +++ b/src/connectors/__test__/recommendationService.test.ts @@ -0,0 +1,247 @@ +import type { Connections } from 'definitions' + +import { + MATTERS_CHOICE_TOPIC_STATE, + MATTERS_CHOICE_TOPIC_VALID_PIN_AMOUNTS, +} from 'common/enums' +import { RecommendationService, AtomService } from 'connectors' + +import { genConnections, closeConnections } from './utils' + +let connections: Connections +let atomService: AtomService +let recommendationService: RecommendationService + +beforeAll(async () => { + connections = await genConnections() + atomService = new AtomService(connections) + recommendationService = new RecommendationService(connections) +}, 30000) + +afterAll(async () => { + await closeConnections(connections) +}) + +const title = 'test title' +const pinAmount = MATTERS_CHOICE_TOPIC_VALID_PIN_AMOUNTS[0] +const articleIds = ['1', '2', '3'] +const note = 'test note' + +describe('IcymiTopic', () => { + describe('createIcymiTopic', () => { + test('pin amount is checked', () => { + expect( + recommendationService.createIcymiTopic({ + title, + articleIds, + pinAmount: 42, + note, + }) + ).rejects.toThrowError('Invalid pin amount') + }) + test('articles are checked', () => { + expect( + recommendationService.createIcymiTopic({ + title, + articleIds: ['0'], + pinAmount, + note, + }) + ).rejects.toThrowError('Invalid article') + }) + test('create topic', async () => { + const topicNoNote = await recommendationService.createIcymiTopic({ + title, + articleIds, + pinAmount, + }) + expect(topicNoNote.title).toBe(title) + expect(topicNoNote.articles).toEqual(articleIds) + expect(topicNoNote.pinAmount).toBe(pinAmount) + expect(topicNoNote.note).toBe(null) + expect(topicNoNote.state).toBe(MATTERS_CHOICE_TOPIC_STATE.editing) + const topic = await recommendationService.createIcymiTopic({ + title, + articleIds, + pinAmount, + note, + }) + expect(topic.note).toBe(note) + }) + }) + describe('updateIcymiTopic', () => { + test('topic is checked', async () => { + expect( + recommendationService.updateIcymiTopic('0', { + title, + }) + ).rejects.toThrowError('Topic not found') + const topic = await recommendationService.createIcymiTopic({ + title, + articleIds, + pinAmount, + note, + }) + await atomService.update({ + table: 'matters_choice_topic', + where: { id: topic.id }, + data: { state: MATTERS_CHOICE_TOPIC_STATE.archived }, + }) + expect( + recommendationService.updateIcymiTopic(topic.id, { + title, + }) + ).rejects.toThrowError('Invalid topic state') + }) + }) + + describe('publishIcymiTopic', () => { + test('topic is checked', async () => { + expect(recommendationService.publishIcymiTopic('0')).rejects.toThrowError( + 'Topic not found' + ) + const topic = await recommendationService.createIcymiTopic({ + title, + articleIds: ['1', '2'], + pinAmount, + note, + }) + expect(topic.state).toBe(MATTERS_CHOICE_TOPIC_STATE.editing) + + // articles amount should more than or equal to pinAmount + expect( + recommendationService.publishIcymiTopic(topic.id) + ).rejects.toThrowError('Articles amount less than pinAmount') + + await recommendationService.updateIcymiTopic(topic.id, { + articleIds, + }) + const published = await recommendationService.publishIcymiTopic(topic.id) + expect(published.state).toBe(MATTERS_CHOICE_TOPIC_STATE.published) + + expect( + recommendationService.publishIcymiTopic(topic.id) + ).rejects.toThrowError('Invalid topic state') + }) + test('archive other published topics when published', async () => { + const topic1 = await recommendationService.createIcymiTopic({ + title, + articleIds, + pinAmount, + note, + }) + await recommendationService.publishIcymiTopic(topic1.id) + + const topic2 = await recommendationService.createIcymiTopic({ + title, + articleIds, + pinAmount, + note, + }) + const published = await recommendationService.publishIcymiTopic(topic2.id) + expect(published.state).toBe(MATTERS_CHOICE_TOPIC_STATE.published) + expect(published.publishedAt).not.toBeNull() + + const topic1AfterPublish = await atomService.findUnique({ + table: 'matters_choice_topic', + where: { id: topic1.id }, + }) + expect(topic1AfterPublish.state).toBe(MATTERS_CHOICE_TOPIC_STATE.archived) + }) + }) + describe('archiveIcymiTopic', () => { + test('delete editing topic', async () => { + const topic = await recommendationService.createIcymiTopic({ + title, + articleIds, + pinAmount, + note, + }) + expect(topic.state).toBe(MATTERS_CHOICE_TOPIC_STATE.editing) + await recommendationService.archiveIcymiTopic(topic.id) + const topicAfterArchive = await atomService.findUnique({ + table: 'matters_choice_topic', + where: { id: topic.id }, + }) + expect(topicAfterArchive).toBeUndefined() + }) + test('archive published topic', async () => { + const topic = await recommendationService.createIcymiTopic({ + title, + articleIds, + pinAmount, + note, + }) + await recommendationService.publishIcymiTopic(topic.id) + const archived = await recommendationService.archiveIcymiTopic(topic.id) + expect(archived?.state).toBe(MATTERS_CHOICE_TOPIC_STATE.archived) + }) + test('update articles in archived topic to icymi articles', async () => { + await atomService.deleteMany({ table: 'matters_choice' }) + const topic = await recommendationService.createIcymiTopic({ + title, + articleIds, + pinAmount, + }) + await recommendationService.publishIcymiTopic(topic.id) + await recommendationService.archiveIcymiTopic(topic.id) + const icymis = await atomService.findMany({ + table: 'matters_choice', + orderBy: [{ column: 'updatedAt', order: 'desc' }], + }) + expect(icymis.map(({ articleId }) => articleId)).toEqual(articleIds) + + const topic2 = await recommendationService.createIcymiTopic({ + title, + articleIds: [...articleIds].reverse(), + pinAmount, + }) + await recommendationService.publishIcymiTopic(topic2.id) + await recommendationService.archiveIcymiTopic(topic2.id) + const icymis2 = await atomService.findMany({ + table: 'matters_choice', + orderBy: [{ column: 'updatedAt', order: 'desc' }], + }) + expect(icymis2.map(({ articleId }) => articleId)).toEqual( + [...articleIds].reverse() + ) + }) + }) +}) + +describe('find icymi articles', () => { + beforeEach(async () => { + await atomService.deleteMany({ table: 'matters_choice' }) + }) + test('find nothing', async () => { + const [articles, totalCount] = + await recommendationService.findIcymiArticles({}) + expect(articles).toHaveLength(0) + expect(totalCount).toBe(0) + }) + test('find articles', async () => { + const topic = await recommendationService.createIcymiTopic({ + title, + articleIds, + pinAmount, + }) + await recommendationService.publishIcymiTopic(topic.id) + await recommendationService.archiveIcymiTopic(topic.id) + const [articles, totalCount] = + await recommendationService.findIcymiArticles({}) + expect(articles).toHaveLength(3) + expect(totalCount).toBe(3) + + const topic2 = await recommendationService.createIcymiTopic({ + title, + articleIds, + pinAmount, + }) + await recommendationService.publishIcymiTopic(topic2.id) + // articles in published topic are not included + const [articles2, totalCount2] = + await recommendationService.findIcymiArticles({}) + expect(articles2).toHaveLength(0) + expect(totalCount2).toBe(0) + }) +}) diff --git a/src/connectors/__test__/systemService.test.ts b/src/connectors/__test__/systemService.test.ts index 1625f6a97..ab900f31e 100644 --- a/src/connectors/__test__/systemService.test.ts +++ b/src/connectors/__test__/systemService.test.ts @@ -1,8 +1,9 @@ -import type { Connections } from 'definitions' +import type { Connections, Asset } from 'definitions' import { v4 } from 'uuid' -import { SystemService } from 'connectors' +import { NODE_TYPES, COMMENT_TYPE, COMMENT_STATE } from 'common/enums' +import { SystemService, AtomService } from 'connectors' import { genConnections, closeConnections } from './utils' @@ -18,10 +19,12 @@ const assetValidation = { let connections: Connections let systemService: SystemService +let atomService: AtomService beforeAll(async () => { connections = await genConnections() systemService = new SystemService(connections) + atomService = new AtomService(connections) }, 30000) afterAll(async () => { @@ -42,14 +45,139 @@ test('findAssetUrl', async () => { test('create and delete asset', async () => { const data = { uuid: v4(), - authorId: 1, + authorId: '1', type: 'cover', path: 'path/to/file.txt', } - const asset = await systemService.baseCreate(data, 'asset') + const asset = await systemService.baseCreate(data, 'asset') expect(asset).toEqual(expect.objectContaining(assetValidation)) await systemService.baseDelete(asset.id, 'asset') const result = await systemService.baseFindById(asset.id, 'asset') expect(result).toBeUndefined() }) + +test('copy asset map', async () => { + const data = { + uuid: v4(), + authorId: '1', + type: 'cover', + path: 'path/to/file.txt', + } + const draftEntityTypeId = '13' + const articleEntityTypeId = '4' + await systemService.createAssetAndAssetMap(data, draftEntityTypeId, '1') + const source = { + entityTypeId: draftEntityTypeId, + entityId: '1', + } + const target = { + entityTypeId: articleEntityTypeId, + entityId: '1', + } + // should not throw errors + await systemService.copyAssetMapEntities({ source, target }) +}) + +describe('report', () => { + test('submit report', async () => { + const report = await systemService.submitReport({ + targetType: NODE_TYPES.Article, + targetId: '1', + reporterId: '1', + reason: 'other', + }) + expect(report.id).toBeDefined() + expect(report.articleId).not.toBeNull() + expect(report.commentId).toBeNull() + }) + test('collapse comment if more than 3 different users report it', async () => { + const commentId = '1' + const comment = await atomService.findUnique({ + table: 'comment', + where: { id: commentId }, + }) + expect(comment.type).toBe(COMMENT_TYPE.article) + expect(comment.state).toBe(COMMENT_STATE.active) + + // only 2 reports, comment should not be collapsed + + await systemService.submitReport({ + targetType: NODE_TYPES.Comment, + targetId: commentId, + reporterId: '2', + reason: 'other', + }) + await systemService.submitReport({ + targetType: NODE_TYPES.Comment, + targetId: commentId, + reporterId: '3', + reason: 'other', + }) + + const commentAfter2Reports = await atomService.findUnique({ + table: 'comment', + where: { id: commentId }, + }) + expect(commentAfter2Reports.state).toBe(COMMENT_STATE.active) + + // only 3 reports from 2 different users, comment should not be collapsed + + await systemService.submitReport({ + targetType: NODE_TYPES.Comment, + targetId: commentId, + reporterId: '3', + reason: 'other', + }) + + const commentAfter3Reports = await atomService.findUnique({ + table: 'comment', + where: { id: commentId }, + }) + expect(commentAfter3Reports.state).toBe(COMMENT_STATE.active) + + // 4 reports from 3 different users, comment should be collapsed + + await systemService.submitReport({ + targetType: NODE_TYPES.Comment, + targetId: commentId, + reporterId: '4', + reason: 'other', + }) + + const commentAfter4Reports = await atomService.findUnique({ + table: 'comment', + where: { id: commentId }, + }) + expect(commentAfter4Reports.state).toBe(COMMENT_STATE.collapsed) + }) + + test('collapse comment if article author report it', async () => { + const commentId = '2' + const comment = await atomService.findUnique({ + table: 'comment', + where: { id: commentId }, + }) + expect(comment.type).toBe(COMMENT_TYPE.article) + expect(comment.state).toBe(COMMENT_STATE.active) + + const { authorId } = await atomService.findUnique({ + table: 'article', + where: { id: comment.targetId }, + }) + + await systemService.submitReport({ + targetType: NODE_TYPES.Comment, + targetId: commentId, + reporterId: authorId, + reason: 'other', + }) + + const commentAfterReport = await atomService.findUnique({ + table: 'comment', + where: { id: commentId }, + }) + + expect(commentAfterReport.state).toBe(COMMENT_STATE.collapsed) + }) +}) diff --git a/src/connectors/__test__/tagService.test.ts b/src/connectors/__test__/tagService.test.ts index f6a6c902e..5e79d0a9a 100644 --- a/src/connectors/__test__/tagService.test.ts +++ b/src/connectors/__test__/tagService.test.ts @@ -26,6 +26,11 @@ test('findArticleIds', async () => { expect(articleIds).toBeDefined() }) +test('findArticleCovers', async () => { + const covers = await tagService.findArticleCovers({ id: '2' }) + expect(covers).toBeDefined() +}) + test('create', async () => { const content = 'foo' const tag = await tagService.create( diff --git a/src/connectors/__test__/userService.test.ts b/src/connectors/__test__/userService.test.ts index 2158d6f2b..699322a69 100644 --- a/src/connectors/__test__/userService.test.ts +++ b/src/connectors/__test__/userService.test.ts @@ -23,7 +23,7 @@ beforeAll(async () => { atomService = new AtomService(connections) userService = new UserService(connections) paymentService = new PaymentService(connections) -}, 50000) +}, 30000) afterAll(async () => { await closeConnections(connections) diff --git a/src/connectors/__test__/utils.ts b/src/connectors/__test__/utils.ts index f5383a985..929693585 100644 --- a/src/connectors/__test__/utils.ts +++ b/src/connectors/__test__/utils.ts @@ -3,9 +3,7 @@ import type { Connections } from 'definitions' // @ts-ignore import initDatabase from '@root/db/initDatabase' -import { Redis } from 'ioredis' -import { RedisMemoryServer } from 'redis-memory-server' -import { v4 } from 'uuid' +import Redis from 'ioredis-mock' import { PAYMENT_CURRENCY, @@ -13,10 +11,7 @@ import { TRANSACTION_PURPOSE, TRANSACTION_STATE, TRANSACTION_TARGET_TYPE, - PUBLISH_STATE, - ARTICLE_STATE, } from 'common/enums' -import { ArticleService, DraftService } from 'connectors' export const genConnections = async (): Promise => { const randomString = Buffer.from(Math.random().toString()) @@ -25,67 +20,18 @@ export const genConnections = async (): Promise => { const database = 'test_matters_' + randomString const knexClient = await initDatabase(database) - const redisServer = new RedisMemoryServer() - const redisPort = await redisServer.getPort() - const redisHost = await redisServer.getHost() - const redis = new Redis(redisPort, redisHost, { - maxRetriesPerRequest: null, - enableReadyCheck: false, - }) + const redis = new Redis() return { knex: knexClient, knexRO: knexClient, knexSearch: knexClient, redis, - __redisServer: redisServer, - } as any as Connections + } } export const closeConnections = async (connections: Connections) => { - connections.redis.disconnect() await connections.knex.destroy() - // @ts-ignore - await connections.__redisServer.stop() -} - -export const createArticle = async ( - { - title, - content, - authorId, - }: { title: string; content: string; authorId: string }, - connections: Connections -) => { - const articleService = new ArticleService(connections) - const draftService = new DraftService(connections) - - const randomString = Math.random().toString() - const dataHash = `test-data-hash-${randomString}` - const mediaHash = `test-media-hash-${randomString}` - - const draft = await draftService.baseCreate({ - uuid: v4(), - title, - content, - authorId, - publishState: PUBLISH_STATE.published, - dataHash, - mediaHash, - }) - const article = await articleService.createArticle({ - draftId: draft.id, - authorId, - title, - slug: title, - cover: '1', - wordCount: content.length, - summary: 'test-summary', - content, - dataHash, - mediaHash, - }) - return articleService.baseUpdate(article.id, { state: ARTICLE_STATE.active }) } export const createDonationTx = async ( diff --git a/src/connectors/alchemy/index.ts b/src/connectors/alchemy/index.ts index 940b8d76e..52479b4bc 100644 --- a/src/connectors/alchemy/index.ts +++ b/src/connectors/alchemy/index.ts @@ -7,7 +7,6 @@ export enum AlchemyNetwork { Mainnet = 'eth-mainnet.g', Rinkeby = 'eth-rinkeby.g', PolygonMainnet = 'polygon-mainnet.g', - PolygonMumbai = 'polygon-mumbai.g', } export class Alchemy { diff --git a/src/connectors/articleService.ts b/src/connectors/articleService.ts index d834325fb..44291e537 100644 --- a/src/connectors/articleService.ts +++ b/src/connectors/articleService.ts @@ -1,52 +1,76 @@ import type { GQLSearchExclude, GQLSearchFilter, - Item, - Article, Draft, + Article, + ArticleVersion, + ArticleBoost, Connections, + User, } from 'definitions' +import { invalidateFQC } from '@matters/apollo-response-cache' import { ArticlePageContext, makeArticlePage, + makeSummary, } from '@matters/ipns-site-generator' +import { html2md } from '@matters/matters-editor/transformers' import DataLoader from 'dataloader' import { Knex } from 'knex' +import { difference, isEqual, uniq } from 'lodash' import { v4 } from 'uuid' import { APPRECIATION_PURPOSE, ARTICLE_ACCESS_TYPE, + ARTICLE_LICENSE_TYPE, ARTICLE_APPRECIATE_LIMIT, ARTICLE_STATE, CIRCLE_STATE, COMMENT_TYPE, COMMENT_STATE, + DB_NOTICE_TYPE, MINUTE, - QUEUE_URL, TRANSACTION_PURPOSE, TRANSACTION_STATE, TRANSACTION_TARGET_TYPE, MAX_PINNED_WORKS_LIMIT, + MAX_ARTICLES_PER_CONNECTION_LIMIT, USER_ACTION, USER_STATE, + NODE_TYPES, } from 'common/enums' import { environment } from 'common/environment' import { ArticleNotFoundError, ServerError, + NetworkError, ForbiddenError, ActionLimitExceededError, + ActionFailedError, + InvalidCursorError, + EntityNotFoundError, + ArticleCollectionReachLimitError, } from 'common/errors' import { getLogger } from 'common/logger' -import { s2tConverter, t2sConverter, normalizeSearchKey } from 'common/utils' import { - AtomService, + countWords, + s2tConverter, + t2sConverter, + nanoid, + normalizeSearchKey, + genMD5, +} from 'common/utils' +import { BaseService, ipfsServers, SystemService, UserService, + TagService, + NotificationService, + PaymentService, + GCP, } from 'connectors' const logger = getLogger('service-article') @@ -54,80 +78,334 @@ const logger = getLogger('service-article') const SEARCH_TITLE_RANK_THRESHOLD = 0.001 const SEARCH_DEFAULT_TEXT_RANK_THRESHOLD = 0.0001 -export class ArticleService extends BaseService { +export class ArticleService extends BaseService
{ private ipfsServers: typeof ipfsServers - public dataloader: DataLoader - public draftLoader: DataLoader + public latestArticleVersionLoader: DataLoader public constructor(connections: Connections) { super('article', connections) this.ipfsServers = ipfsServers - this.dataloader = new DataLoader(async (ids: readonly string[]) => { - const result = await this.baseFindByIds(ids) - - if (result.findIndex((item: any) => !item) >= 0) { - throw new ArticleNotFoundError('Cannot find article') - } - - return result - }) - - // load drafts by aritcle ids - this.draftLoader = new DataLoader(async (ids: readonly string[]) => { - const items = await this.baseFindByIds(ids) - - if (items.findIndex((item: any) => !item) >= 0) { - throw new ArticleNotFoundError('Cannot find article') - } + const batchFn = async ( + keys: readonly string[] + ): Promise => { + const table = 'article_version_newest' + const records = await this.knexRO(table).whereIn( + 'article_id', + keys + ) - const draftIds = items.map((item: any) => item.draftId) - const result = await this.baseFindByIds(draftIds, 'draft') - if (result.findIndex((item: any) => !item) >= 0) { - throw new ArticleNotFoundError("Cannot find article's linked draft") + if (records.findIndex((item: unknown) => !item) >= 0) { + throw new EntityNotFoundError(`Cannot find entity from ${table}`) } + // fix order based on keys + return keys.map( + (key) => records.find((r) => r.articleId === key) as ArticleVersion + ) + } - return result - }) + this.latestArticleVersionLoader = new DataLoader(batchFn) } - public loadById = async (id: string): Promise
=> - this.dataloader.load(id) as Promise
- public loadByIds = async (ids: string[]): Promise => - this.dataloader.loadMany(ids) as Promise - - public loadDraftsByArticles = async (ids: string[]): Promise => - this.draftLoader.loadMany(ids) as Promise - /** - * Create a pending article with linked draft + * Create article from draft */ public createArticle = async ({ - draftId, + id: draftId, authorId, title, - slug, - wordCount, summary, content, + contentMd, cover, - dataHash, - mediaHash, - }: Record) => - this.baseCreate({ - uuid: v4(), - state: ARTICLE_STATE.pending, - draftId, - authorId, - title, - slug, - wordCount, - summary, - content, - cover, - dataHash, - mediaHash, + tags, + collection, + circleId, + access, + license, + requestForDonation, + replyToDonator, + canComment, + sensitiveByAuthor, + }: Partial & { + authorId: string + title: string + content: string + }): Promise<[Article, ArticleVersion]> => { + const wordCount = countWords(content) + const _summary = summary || makeSummary(content) + const summaryCustomized = !!summary + + // get contentId and contentMdId + const { id: contentId } = await this.getOrCreateArticleContent(content) + let _contentMd + try { + _contentMd = contentMd || html2md(content) + } catch (e) { + logger.warn('draft %s failed to convert HTML to Markdown', draftId) + } + let contentMdId + if (_contentMd) { + const { id: _contentMdId } = await this.getOrCreateArticleContent( + _contentMd + ) + contentMdId = _contentMdId + } + + // create article and article version + const trx = await this.knex.transaction() + try { + const [article] = await trx
('article') + .insert({ + authorId, + state: ARTICLE_STATE.active, + shortHash: nanoid(), // retry handling at higher level of a very low probability of collision, or increase the nanoid length when it comes to higher probability; + }) + .returning('*') + const [articleVersion] = await trx('article_version') + .insert({ + articleId: article.id, + title, + summary: _summary, + summaryCustomized, + contentId, + contentMdId, + cover, + tags: tags ?? [], + connections: collection ?? [], + wordCount, + circleId, + access: access ?? ARTICLE_ACCESS_TYPE.public, + license: license ?? ARTICLE_LICENSE_TYPE.cc_by_nc_nd_4, + requestForDonation, + replyToDonator, + canComment: canComment ?? true, + sensitiveByAuthor: sensitiveByAuthor ?? false, + }) + .returning('*') + + // copy asset_map from draft to article if there is a draft + if (draftId) { + const systemService = new SystemService(this.connections) + const [draftEntity, articleEntity] = await Promise.all([ + systemService.baseFindEntityTypeId('draft'), + systemService.baseFindEntityTypeId('article'), + ]) + await systemService.copyAssetMapEntities({ + source: { entityTypeId: draftEntity.id, entityId: draftId }, + target: { entityTypeId: articleEntity.id, entityId: article.id }, + }) + } + await trx.commit() + return [article, articleVersion] + } catch (e) { + await trx.rollback() + throw e + } + } + + public getOrCreateArticleContent = async (content: string) => { + const contentHash = genMD5(content) + const result = await this.models.findUnique({ + table: 'article_content', + where: { hash: contentHash }, + }) + if (result) { + return result + } else { + return this.models.create({ + table: 'article_content', + data: { hash: contentHash, content }, + }) + } + } + + public loadLatestArticleVersion = async (articleId: string) => { + const version = await this.latestArticleVersionLoader.load(articleId) + return version + ? version + : await this.knexRO('draft') + .where({ articleId }) + .orderBy('id', 'desc') + .first() + } + + public loadLatestArticleContent = async (articleId: string) => { + const { contentId } = await this.latestArticleVersionLoader.load(articleId) + const { content } = await this.models.articleContentIdLoader.load(contentId) + return content + } + + public loadLatestArticleContentMd = async (articleId: string) => { + const { contentMdId } = await this.latestArticleVersionLoader.load( + articleId + ) + if (!contentMdId) { + return '' + } + const { content: contentMd } = + await this.models.articleContentIdLoader.load(contentMdId) + return contentMd + } + + public createNewArticleVersion = async ( + articleId: string, + actorId: string, + newData: Partial, + description?: string + ) => { + if (Object.keys(newData).length === 0) { + throw new ActionFailedError('newData is empty') + } + const lastData = await this.latestArticleVersionLoader.load(articleId) + let data = { ...lastData } as Partial + delete data.id + delete data.description + delete data.createdAt + delete data.updatedAt + + if (newData.content) { + const { id: contentId } = await this.getOrCreateArticleContent( + newData.content + ) + data = { ...data, contentId, wordCount: countWords(newData.content) } + let _contentMd + try { + _contentMd = newData.contentMd || html2md(newData.content) + } catch (e) { + logger.warn( + 'failed to convert HTML to Markdown for new article version of article %s', + articleId + ) + } + if (_contentMd) { + const { id: _contentMdId } = await this.getOrCreateArticleContent( + _contentMd + ) + data = { ...data, contentMdId: _contentMdId } + delete newData.content + } + } else { + data = { + ...data, + contentId: lastData.contentId, + contentMdId: lastData.contentMdId, + wordCount: lastData.wordCount, + dataHash: lastData.dataHash, + mediaHash: lastData.mediaHash, + iscnId: lastData.iscnId, + } + } + if (newData.summary || newData.summary === null || newData.summary === '') { + data = { + ...data, + summary: newData.summary ?? '', + summaryCustomized: newData.summary ? true : false, + } + delete newData.summary + } else { + data = { + ...data, + summary: lastData.summary, + summaryCustomized: lastData.summaryCustomized, + } + } + if (newData.collection || newData.collection === null) { + data = { ...data, connections: newData.collection ?? [] } + await this.updateArticleConnections({ + articleId, + connections: newData.collection ?? [], + }) + delete newData.collection + } + + if (newData.circleId) { + const _data = { articleId, circleId: newData.circleId } + await this.models.upsert({ + table: 'article_circle', + where: _data, + create: { ..._data, access: newData.access || data.access }, + update: { ..._data, access: newData.access || data.access }, + }) + } + if (newData.circleId === null) { + await this.models.deleteMany({ + table: 'article_circle', + where: { articleId }, + }) + } + + if (newData.tags || newData.tags === null) { + const tagService = new TagService(this.connections) + await tagService.updateArticleTags({ + articleId, + actorId, + tags: newData.tags ?? [], + }) + data = { ...data, tags: newData.tags ?? [] } + delete newData.tags + } + + const articleVersion = await this.models.create({ + table: 'article_version', + data: { ...data, ...newData, description } as Partial, }) + this.latestArticleVersionLoader.clear(articleId) + return articleVersion + } + + public countArticleVersions = async (articleId: string) => + this.models.count({ table: 'article_version', where: { articleId } }) + + public findArticleVersions = async ( + articleId: string, + { take, skip }: { take?: number; skip?: number } = {}, + all = false + ) => { + const revisionCols = + '(title, summary, content_id, content_md_id, tags, connections, cover)' + const records = await this.knexRO('article_version') + .select('*', this.knexRO.raw('COUNT(1) OVER() ::int AS total_count')) + .from( + this.knexRO('article_version') + .where({ articleId }) + .modify((builder) => { + if (!all) { + builder.select( + '*', + this.knexRO.raw( + `LAG(${revisionCols}, 1) OVER(order by id) AS pre_cols` + ) + ) + } else { + builder.select('*') + } + }) + .orderBy('id', 'desc') + .as('t') + ) + .modify((builder) => { + if (!all) { + builder + .where( + this.knexRO.raw(revisionCols), + '!=', + this.knexRO.ref('pre_cols') + ) + .orWhere((whereBuilder) => { + // first version + whereBuilder.whereNull('pre_cols') + }) + } + if (take !== undefined && Number.isFinite(take)) { + builder.limit(take) + } + if (skip !== undefined && Number.isFinite(skip)) { + builder.offset(skip) + } + }) + return [records, +(records[0]?.totalCount ?? 0)] + } /** * Update article's pin status and return article @@ -156,7 +434,10 @@ export class ArticleService extends BaseService { `Can only pin up to ${MAX_PINNED_WORKS_LIMIT} articles/collections` ) } - await this.baseUpdate(articleId, { pinned, pinnedAt: this.knex.fn.now() }) + await this.baseUpdate(articleId, { + pinned, + pinnedAt: this.knex.fn.now() as unknown as Date, + }) return { ...article, pinned } } @@ -168,25 +449,19 @@ export class ArticleService extends BaseService { /** * Publish draft data to IPFS */ - public publishToIPFS = async (draft: any) => { - const userService = new UserService(this.connections) + public publishToIPFS = async ( + article: Article, + articleVersion: ArticleVersion, + content: string + ) => { const systemService = new SystemService(this.connections) - const atomService = new AtomService(this.connections) // prepare metadata - const { - title, - content, - summary, - cover, - tags, - circleId, - access, - authorId, - articleId, - updatedAt: publishedAt, - } = draft - const author = await userService.loadById(authorId) + const { id, title, summary, cover, tags, circleId, access, createdAt } = + articleVersion + const author = (await this.models.userIdLoader.load( + article.authorId + )) as User const { // avatar, displayName, @@ -203,25 +478,28 @@ export class ArticleService extends BaseService { ] = await Promise.all([ // avatar && (await systemService.findAssetUrl(avatar)), cover && (await systemService.findAssetUrl(cover)), - atomService.findFirst({ + this.models.findFirst({ table: 'user_ipns_keys', - where: { userId: authorId }, + where: { userId: article.authorId }, }), ]) const ipnsKey = ipnsKeyRec?.ipnsKey + const publishedAt = createdAt.toISOString() const context: ArticlePageContext = { encrypted: false, meta: { title: `${title} - ${displayName} (${userName})`, description: summary, authorName: displayName, - image: articleCoverImg, + image: articleCoverImg ?? undefined, }, byline: { date: publishedAt, author: { name: `${displayName} (${userName})`, + userName, + displayName, uri: `https://${environment.siteDomain}/@${userName}`, }, website: { @@ -231,13 +509,12 @@ export class ArticleService extends BaseService { }, rss: ipnsKey ? { - ipnsKey, xml: '../rss.xml', json: '../feed.json', } : undefined, article: { - id: articleId, + id, author: { userName, displayName, @@ -308,23 +585,12 @@ export class ArticleService extends BaseService { // re-fill dataHash & mediaHash later in IPNS-listener logger.error(`failed publishToIPFS after ${retries} retries.`) + throw new NetworkError('failed publishToIPFS') } // DEPRECATED, To Be Deleted // moved to IPNS-Listener - public publishFeedToIPNS = async ({ - userName, - numArticles = 50, - incremental = false, - forceReplace = false, - updatedDrafts, - }: { - userName: string - numArticles?: number - incremental?: boolean - forceReplace?: boolean - updatedDrafts?: Item[] - }) => { + public publishFeedToIPNS = async ({ userName }: { userName: string }) => { const userService = new UserService(this.connections) try { @@ -340,75 +606,25 @@ export class ArticleService extends BaseService { } } - public sendArticleFeedMsgToSQS = async ({ - article, - author, - ipnsData, - }: { - article: { - id: string - title: string - slug: string - dataHash: string - mediaHash: string - } - author: { - userName: string - displayName: string - } - ipnsData: { - ipnsKey: string - lastDataHash: string - } - }) => - this.aws?.sqsSendMessage({ - messageGroupId: `ipfs-articles-${environment.env}:articles-feed`, - messageBody: { - articleId: article.id, - title: article.title, - url: `https://${environment.siteDomain}/@${author.userName}/${article.id}-${article.slug}`, - dataHash: article.dataHash, - mediaHash: article.mediaHash, - - // ipns info: - ipnsKey: ipnsData.ipnsKey, - lastDataHash: ipnsData.lastDataHash, - - // author info: - userName: author.userName, - displayName: author.displayName, - }, - queueUrl: QUEUE_URL.ipfsArticles, - }) - /** * Archive article */ - public archive = async (id: string) => { - const atomService = new AtomService(this.connections) - const targetArticle = await atomService.findFirst({ - table: 'article', - where: { id }, - }) - const articles = await atomService.findMany({ - table: 'article', - where: { draftId: targetArticle.draftId }, + public archive = async (id: string) => + this.baseUpdate(id, { + state: ARTICLE_STATE.archived, + pinned: false, + updatedAt: new Date(), }) - // update db - for (const article of articles) { - await this.baseUpdate(article.id, { - state: ARTICLE_STATE.archived, - pinned: false, - updatedAt: new Date(), - }) - } - } + public findVersionByMediaHash = async (mediaHash: string) => + this.models.findFirst({ table: 'article_version', where: { mediaHash } }) + public findArticleByShortHash = async (shortHash: string) => + this.models.findFirst({ table: 'article', where: { shortHash } }) public findByAuthor = async ( authorId: string, { - columns = ['draft_id'], + columns = ['*'], orderBy = 'newest', state = 'active', skip, @@ -425,7 +641,7 @@ export class ArticleService extends BaseService { skip?: number take?: number } = {} - ) => { + ): Promise => { const { id: targetTypeId } = await this.baseFindEntityTypeId('article') return this.knexRO( this.knexRO @@ -560,28 +776,6 @@ export class ArticleService extends BaseService { .select(columns) } - public findByTitle = async ({ - title, - oss = false, - filter, - }: { - title: string - oss?: boolean - filter?: Record - }) => { - const query = this.knex.select().from(this.table).where({ title }) - - if (!oss) { - query.andWhere({ state: ARTICLE_STATE.active }) - } - - if (filter && Object.keys(filter).length > 0) { - query.andWhere(filter) - } - - return query.orderBy('id', 'desc') - } - public findByCommentedAuthor = async ({ id, skip, @@ -590,7 +784,7 @@ export class ArticleService extends BaseService { id: string skip?: number take?: number - }) => + }): Promise => this.knex .select('article.*') .max('comment.id', { as: '_comment_id_' }) @@ -617,36 +811,6 @@ export class ArticleService extends BaseService { * * *********************************/ - public searchByMediaHash = async ({ - key, - oss = false, - filter, - }: { - key: string - oss?: boolean - filter?: Record - }) => { - const query = this.knex.select().from(this.table).where({ mediaHash: key }) - - if (!oss) { - query.andWhere({ state: ARTICLE_STATE.active }) - } - - if (filter && Object.keys(filter).length > 0) { - query.andWhere(filter) - } - - const rows = await query - if (rows.length > 0) { - return { - nodes: rows, - totalCount: rows.length, - } - } else { - throw new ServerError('article search by media hash failed') - } - } - public search = async ({ key: keyOriginal, take = 10, @@ -698,7 +862,6 @@ export class ArticleService extends BaseService { environment.searchPgArticleCoefficients?.[3] || 1 ) - // const c4 = +(coeffs?.[4] || environment.searchPgArticleCoefficients?.[4] || 1) // gather users that blocked viewer const excludeBlocked = exclude === 'blocked' && viewerId @@ -731,7 +894,6 @@ export class ArticleService extends BaseService { this.searchKnex.raw( 'percent_rank() OVER (ORDER BY num_views NULLS FIRST) AS views_rank' ), - // this.searchKnex.raw('(CASE WHEN title LIKE ? THEN 1 ELSE 0 END) ::float AS title_like_rank', [`%${key}%`]), this.searchKnex.raw( 'ts_rank(title_jieba_ts, query) AS title_ts_rank' ), @@ -785,17 +947,15 @@ export class ArticleService extends BaseService { } }) - const nodes = (await this.draftLoader.loadMany( - records.map((item: any) => item.id).filter(Boolean) - )) as Item[] + const nodes = await this.models.userIdLoader.loadMany( + records.map((item: { id: string }) => item.id).filter(Boolean) + ) - // const totalCount = Number.parseInt(countRes?.count, 10) || nodes.length const totalCount = records.length === 0 ? 0 : +records[0].totalCount logger.debug( `articleService::searchV2 searchKnex instance got ${nodes.length} nodes from: ${totalCount} total:`, { key, keyOriginal, baseQuery: baseQuery.toString() }, - // { countRes, articleIds } { sample: records?.slice(0, 3) } ) @@ -818,7 +978,7 @@ export class ArticleService extends BaseService { exclude?: GQLSearchExclude coefficients?: string quicksearch?: boolean - }) => { + }): Promise<{ nodes: Article[]; totalCount: number }> => { if (quicksearch) { return this.quicksearch({ key: keyOriginal, take, skip, filter }) } @@ -844,9 +1004,9 @@ export class ArticleService extends BaseService { records[0] ) - const nodes = (await this.draftLoader.loadMany( - records.map((item: any) => `${item.id}`).filter(Boolean) - )) as Item[] + const nodes = await this.models.articleIdLoader.loadMany( + records.map((item: { id: string }) => `${item.id}`).filter(Boolean) + ) return { nodes, totalCount } } catch (err) { @@ -865,19 +1025,27 @@ export class ArticleService extends BaseService { take?: number skip?: number filter?: GQLSearchFilter - }) => { + }): Promise<{ nodes: Article[]; totalCount: number }> => { const keySimplified = await t2sConverter.convertPromise(key) const keyTraditional = await s2tConverter.convertPromise(key) - const records = await this.knexRO - .select('id', this.knexRO.raw('COUNT(1) OVER() ::int AS total_count')) - .where(function () { - this.whereILike('title', `%${key}%`) - .orWhereILike('title', `%${keyTraditional}%`) - .orWhereILike('title', `%${keySimplified}%`) - }) - .from('article') - .orderBy('id', 'desc') + const q = this.knexRO('article') + .select('*', this.knexRO.raw('COUNT(1) OVER() ::int AS total_count')) + .whereIn( + 'id', + this.knexRO + .select('article_id') + .where(function () { + if (filter && filter.authorId) { + this.where({ authorId: filter.authorId }) + } + this.whereILike('title', `%${key}%`) + .orWhereILike('title', `%${keyTraditional}%`) + .orWhereILike('title', `%${keySimplified}%`) + }) + .from('article_version_newest') + ) .where({ state: ARTICLE_STATE.active }) + .orderBy('id', 'desc') .modify((builder: Knex.QueryBuilder) => { if (filter && filter.authorId) { builder.where({ authorId: filter.authorId }) @@ -889,21 +1057,19 @@ export class ArticleService extends BaseService { builder.offset(skip) } }) + const records = await q - const nodes = (await this.draftLoader.loadMany( - records.map((item: { id: string }) => item.id).filter(Boolean) - )) as Draft[] const totalCount = +(records?.[0]?.totalCount ?? 0) - return { nodes, totalCount } + return { nodes: records as Article[], totalCount } } /** * Boost & Score */ public setBoost = async ({ id, boost }: { id: string; boost: number }) => - this.baseUpdateOrCreate({ + this.baseUpdateOrCreate({ where: { articleId: id }, - data: { articleId: id, boost, updatedAt: new Date() }, + data: { articleId: id, boost }, table: 'article_boost', }) @@ -913,7 +1079,7 @@ export class ArticleService extends BaseService { * * *********************************/ /** - * Sum total appreciaton by a given article id. + * Sum total appreciation by a given article id. */ public sumAppreciation = async (articleId: string) => { const result = await this.knex @@ -1073,7 +1239,7 @@ export class ArticleService extends BaseService { id: articleId, }: { id: string - }): Promise => { + }): Promise => { const result = await this.knex .select('tag_id') .from('article_tag') @@ -1157,7 +1323,7 @@ export class ArticleService extends BaseService { * create new record and return */ if (!record || record.length === 0) { - await this.baseCreate( + await this.baseCreate( { ...newData, count: 1, @@ -1305,95 +1471,9 @@ export class ArticleService extends BaseService { * Response * * * *********************************/ - private makeResponseQuery = ({ - id, - order, - state, - fields = '*', - articleOnly = false, - }: { - id: string - order: string - state?: string - fields?: string - articleOnly?: boolean - }) => - this.knex.select(fields).from((wrapper: any) => { - wrapper - .select( - this.knex.raw('row_number() over (order by created_at) as seq, *') - ) - .from((knex: any) => { - const source = knex.union((operator: any) => { - operator - .select( - this.knex.raw( - "'Article' as type, entrance_id as entity_id, article_connection.created_at" - ) - ) - .from('article_connection') - .rightJoin( - 'article', - 'article_connection.entrance_id', - 'article.id' - ) - .where({ - 'article_connection.article_id': id, - 'article.state': state, - }) - }) - - if (articleOnly !== true) { - source.union((operator: any) => { - operator - .select( - this.knex.raw( - "'Comment' as type, id as entity_id, created_at" - ) - ) - .from('comment') - .where({ - targetId: id, - parentCommentId: null, - type: COMMENT_TYPE.article, - }) - }) - } - - source.as('base_sources') - return source - }) - .orderBy('created_at', order) - .as('sources') - }) - - private makeResponseFilterQuery = ({ - id, - entityId, - order, - state, - articleOnly, - }: { - id: string - entityId: string - order: string - state?: string - articleOnly?: boolean - }) => { - const query = this.makeResponseQuery({ - id, - order, - state, - fields: 'seq', - articleOnly, - }) - return query.where({ entityId }).first() - } - public findResponses = ({ id, order = 'desc', - state = ARTICLE_STATE.active, after, before, first, @@ -1403,40 +1483,98 @@ export class ArticleService extends BaseService { }: { id: string order?: string - state?: string - after?: any - before?: any + after?: { type: NODE_TYPES; id: string } + before?: { type: NODE_TYPES; id: string } first?: number includeAfter?: boolean includeBefore?: boolean articleOnly?: boolean }) => { - const query = this.makeResponseQuery({ id, order, state, articleOnly }) + const subQuery = this.knexRO + .select( + this.knexRO.raw('COUNT(1) OVER() AS total_count'), + this.knexRO.raw('MIN(created_at) OVER() AS min_cursor'), + this.knexRO.raw('MAX(created_at) OVER() AS max_cursor'), + '*' + ) + .from( + this.knexRO + .select( + this.knexRO.raw( + "'Article' as type, entrance_id as entity_id, article_connection.created_at" + ) + ) + .from('article_connection') + .rightJoin('article', 'article_connection.entrance_id', 'article.id') + .where({ + 'article_connection.article_id': id, + 'article.state': ARTICLE_STATE.active, + }) + .modify((builder: Knex.QueryBuilder) => { + if (articleOnly !== true) { + builder.union( + this.knexRO + .select( + this.knexRO.raw( + "'Comment' as type, id as entity_id, created_at" + ) + ) + .fromRaw('comment AS outer_comment') + .where({ + targetId: id, + parentCommentId: null, + }) + .andWhere((andWhereBuilder) => { + andWhereBuilder + .where({ state: COMMENT_STATE.active }) + .orWhere({ state: COMMENT_STATE.collapsed }) + .orWhere((orWhereBuilder) => { + orWhereBuilder.andWhere( + this.knexRO.raw( + '(SELECT COUNT(1) FROM comment WHERE state in (?, ?) and parent_comment_id = outer_comment.id)', + [COMMENT_STATE.active, COMMENT_STATE.collapsed] + ), + '>', + 0 + ) + }) + }) + ) + } + }) + .orderBy('created_at', order) + .as('source') + ) + + const query = this.knexRO.from(subQuery.as('t1')) + + const validTypes = [NODE_TYPES.Comment, NODE_TYPES.Article] if (after) { - const subQuery = this.makeResponseFilterQuery({ - id, - order, - state, - entityId: after, - articleOnly, - }) + if (!validTypes.includes(after.type)) { + throw new InvalidCursorError('after is invalid cursor') + } + const cursor = this.knexRO(after.type.toLowerCase()) + .select('created_at') + .where({ id: after.id }) + .first() if (includeAfter) { - query.andWhere('seq', order === 'asc' ? '>=' : '<=', subQuery) + query.andWhere('created_at', order === 'asc' ? '>=' : '<=', cursor) } else { - query.andWhere('seq', order === 'asc' ? '>' : '<', subQuery) + query.andWhere('created_at', order === 'asc' ? '>' : '<', cursor) } } if (before) { - const subQuery = this.makeResponseFilterQuery({ - id, - order, - state, - entityId: before, - }) + if (!validTypes.includes(before.type)) { + throw new InvalidCursorError('before is invalid cursor') + } + const cursor = this.knexRO(before.type.toLowerCase()) + .select('created_at') + .where({ id: before.id }) + .first() if (includeBefore) { - query.andWhere('seq', order === 'asc' ? '<=' : '>=', subQuery) + query.andWhere('created_at', order === 'asc' ? '<=' : '>=', cursor) } else { - query.andWhere('seq', order === 'asc' ? '<' : '>', subQuery) + query.andWhere('created_at', order === 'asc' ? '<' : '>', cursor) } } if (first) { @@ -1445,28 +1583,6 @@ export class ArticleService extends BaseService { return query } - public responseRange = async ({ - id, - order, - state, - }: { - id: string - order: string - state: string - }) => { - const query = this.makeResponseQuery({ id, order, state, fields: '' }) - const { count, max, min } = (await query - .max('seq') - .min('seq') - .count() - .first()) as Record - return { - count: parseInt(count, 10), - max: parseInt(max, 10), - min: parseInt(min, 10), - } - } - /********************************* * * * Transaction * @@ -1689,7 +1805,7 @@ export class ArticleService extends BaseService { notIn: string[] take?: number skip?: number - }) => { + }): Promise => { const { id: entityTypeId } = await this.baseFindEntityTypeId( TRANSACTION_TARGET_TYPE.article ) @@ -1715,6 +1831,23 @@ export class ArticleService extends BaseService { * Access * * * *********************************/ + public getAccess = async (id: string) => { + const articleCircle = await this.findArticleCircle(id) + + // not in circle, fallback to public + if (!articleCircle) { + return ARTICLE_ACCESS_TYPE.public + } + + // public + if (articleCircle.access === ARTICLE_ACCESS_TYPE.public) { + return ARTICLE_ACCESS_TYPE.public + } + + // paywall + return ARTICLE_ACCESS_TYPE.paywall + } + public findArticleCircle = async (articleId: string) => this.knex .select('article_circle.*') @@ -1744,12 +1877,12 @@ export class ArticleService extends BaseService { take: number maxTake: number oss: boolean - }) => { + }): Promise => { const query = this.knexRO - .select('article_set.draft_id', 'article_set.id') + .select('article_set.*') .from( this.knexRO - .select('id', 'draft_id', 'author_id') + .select('*') .from('article') .where({ state: ARTICLE_STATE.active }) .whereNotIn( @@ -1781,4 +1914,268 @@ export class ArticleService extends BaseService { .offset(skip) .limit(take) } + + private updateArticleConnections = async ({ + articleId, + connections, + }: { + articleId: string + connections: string[] + }) => { + const oldIds = ( + await this.findConnections({ + entranceId: articleId, + }) + ).map(({ articleId: id }: { articleId: string }) => id) + const newIds = uniq(connections) + + // do nothing if no change + if (isEqual(oldIds, newIds)) { + return + } + + const newIdsToAdd = difference(newIds, oldIds) + const oldIdsToDelete = difference(oldIds, newIds) + + // only validate new-added articles + if (newIdsToAdd.length) { + if ( + newIds.length > MAX_ARTICLES_PER_CONNECTION_LIMIT && + newIds.length >= oldIds.length + ) { + throw new ArticleCollectionReachLimitError( + `Not allow more than ${MAX_ARTICLES_PER_CONNECTION_LIMIT} articles in connection` + ) + } + await Promise.all( + newIdsToAdd.map(async (id) => { + const collectedArticle = await this.models.findUnique({ + table: 'article', + where: { id: articleId }, + }) + + if (!collectedArticle) { + throw new ArticleNotFoundError(`Cannot find article ${id}`) + } + + if (collectedArticle.state !== ARTICLE_STATE.active) { + throw new ForbiddenError(`Article ${id} cannot be collected.`) + } + }) + ) + } + + interface Item { + entranceId: string + articleId: string + order: number + } + const addItems: Item[] = [] + const updateItems: Item[] = [] + + // gather data + newIds.forEach((id: string, index: number) => { + const isNew = newIdsToAdd.includes(id) + if (isNew) { + addItems.push({ entranceId: articleId, articleId: id, order: index }) + } + if (!isNew && index !== oldIds.indexOf(id)) { + updateItems.push({ entranceId: articleId, articleId: id, order: index }) + } + }) + + await Promise.all([ + ...addItems.map((item) => + this.models.create({ + table: 'article_connection', + data: { + ...item, + }, + }) + ), + ...updateItems.map((item) => + this.models.update({ + table: 'article_connection', + where: { entranceId: item.entranceId, articleId: item.articleId }, + data: { order: item.order }, + }) + ), + ]) + + // delete unwanted + await this.models.deleteMany({ + table: 'article_connection', + where: { entranceId: articleId }, + whereIn: ['article_id', oldIdsToDelete], + }) + + // trigger notifications + const article = await this.models.articleIdLoader.load(articleId) + const notificationService = new NotificationService(this.connections) + newIdsToAdd.forEach(async (id) => { + const targetConnection = await this.models.findUnique({ + table: 'article', + where: { id }, + }) + if (targetConnection) { + notificationService.trigger({ + event: DB_NOTICE_TYPE.article_new_collected, + recipientId: targetConnection.authorId, + actorId: article.authorId, + entities: [ + { + type: 'target', + entityTable: 'article', + entity: targetConnection, + }, + { + type: 'collection', + entityTable: 'article', + entity: article, + }, + ], + }) + } + }) + } + + public getOrCreateTranslation = async ( + articleVersion: ArticleVersion, + language: string, + actorId?: string + ) => { + // paywalled content + const { id, articleId } = articleVersion + const { authorId } = await this.models.articleIdLoader.load(articleId) + let isPaywalledContent = false + const isAuthor = authorId === actorId + const articleCircle = await this.findArticleCircle(articleId) + if ( + !isAuthor && + articleCircle && + articleCircle.access === ARTICLE_ACCESS_TYPE.paywall + ) { + if (actorId) { + const paymentService = new PaymentService(this.connections) + const isCircleMember = await paymentService.isCircleMember({ + userId: actorId, + circleId: articleCircle.circleId, + }) + + // not circle member + if (!isCircleMember) { + isPaywalledContent = true + } + } else { + isPaywalledContent = true + } + } + + const { + title: originTitle, + summary: originSummary, + language: storedLanguage, + contentId, + } = articleVersion + const { content: originContent } = + await this.models.articleContentIdLoader.load(contentId) + + // it's same as original language + if (language === storedLanguage) { + return { + content: isPaywalledContent ? '' : originContent, + title: originTitle, + summary: originSummary, + language, + } + } + + // get translation + const translation = await this.models.findFirst({ + table: 'article_translation', + where: { articleId, language, articleVersionId: id }, + }) + + if (translation) { + return { + ...translation, + content: isPaywalledContent ? '' : translation.content, + } + } + + const gcp = new GCP() + + // or translate and store to db + const [title, content, summary] = await Promise.all( + [originTitle, originContent, originSummary].map((text) => + gcp.translate({ + content: text, + target: language, + }) + ) + ) + + if (title && content) { + const data = { + articleId, + title, + content, + summary, + language, + articleVersionId: id, + } + await this.models.upsert({ + table: 'article_translation', + where: { articleId, language, articleVersionId: id }, + create: data, + update: data, + }) + + // translate tags + const tagIds = await this.findTagIds({ id: articleId }) + if (tagIds && tagIds.length > 0) { + try { + const tags = await this.models.tagIdLoader.loadMany(tagIds) + await Promise.all( + tags.map(async (tag) => { + if (tag instanceof Error) { + return + } + const translatedTag = await gcp.translate({ + content: tag.content, + target: language, + }) + const tagData = { + tagId: tag.id, + content: translatedTag ?? '', + language, + } + await this.models.upsert({ + table: 'tag_translation', + where: { tagId: tag.id }, + create: tagData, + update: tagData, + }) + }) + ) + } catch (error) { + logger.error(error) + } + } + + await invalidateFQC({ + node: { type: NODE_TYPES.Article, id: articleId }, + redis: this.redis, + }) + + return { + title, + content: isPaywalledContent ? '' : content, + summary, + language, + } + } else { + return null + } + } } diff --git a/src/connectors/atomService.ts b/src/connectors/atomService.ts index c95ad7c67..6be1def48 100644 --- a/src/connectors/atomService.ts +++ b/src/connectors/atomService.ts @@ -1,136 +1,356 @@ -import type { Item, TableName, Connections } from 'definitions' +import type { + ActionArticle, + ActionCircle, + ActionTag, + ActionUser, + Announcement, + AnnouncementTranslation, + Appreciation, + Article, + ArticleBoost, + ArticleCircle, + ArticleConnection, + ArticleContent, + ArticleCountView, + ArticleReadTimeMaterialized, + ArticleRecommendSetting, + ArticleTag, + ArticleTranslation, + ArticleVersion, + Asset, + AssetMap, + BlockchainSyncRecord, + BlockchainTransaction, + BlockedSearchKeyword, + Blocklist, + Circle, + CircleInvitation, + CirclePrice, + CircleSubscription, + CircleSubscriptionItem, + Collection, + CollectionArticle, + Comment, + Connections, + CryptoWallet, + CryptoWalletSignature, + Customer, + Draft, + EntityType, + FeaturedCommentMaterialized, + MattersChoice, + MattersChoiceTopic, + PayoutAccount, + PunishRecord, + RecommendedArticlesFromReadTagsMaterialized, + Report, + SearchHistory, + SeedingUser, + TableName, + Tag, + TagTranslation, + Transaction, + User, + UserBadge, + UserIpnsKeys, + UserOauthLikecoinDB, + UserRestriction, + UserTagsOrder, + UsernameEditHistory, + VerificationCode, +} from 'definitions' import type { Knex } from 'knex' import DataLoader from 'dataloader' -import { EntityNotFoundError } from 'common/errors' -import { aws, cfsvc } from 'connectors' - -interface InitLoaderInput { - table: TableName - mode: 'id' | 'uuid' +import { + EntityNotFoundError, + ArticleNotFoundError, + CommentNotFoundError, +} from 'common/errors' + +type Mode = 'id' | 'uuid' + +type TableTypeMap = { + announcement: Announcement + announcement_translation: AnnouncementTranslation + blocked_search_keyword: BlockedSearchKeyword + blocklist: Blocklist + matters_choice: MattersChoice + user: User + user_ipns_keys: UserIpnsKeys + username_edit_history: UsernameEditHistory + user_restriction: UserRestriction + asset: Asset + asset_map: AssetMap + draft: Draft + article: Article + article_version: ArticleVersion + article_content: ArticleContent + article_circle: ArticleCircle + article_translation: ArticleTranslation + article_tag: ArticleTag + article_boost: ArticleBoost + article_connection: ArticleConnection + article_recommend_setting: ArticleRecommendSetting + article_count_view: ArticleCountView + article_read_time_materialized: ArticleReadTimeMaterialized + collection: Collection + collection_article: CollectionArticle + comment: Comment + featured_comment_materialized: FeaturedCommentMaterialized + action_user: ActionUser + action_article: ActionArticle + action_circle: ActionCircle + action_tag: ActionTag + circle: Circle + circle_price: CirclePrice + circle_invitation: CircleInvitation + circle_subscription: CircleSubscription + circle_subscription_item: CircleSubscriptionItem + customer: Customer + crypto_wallet: CryptoWallet + crypto_wallet_signature: CryptoWalletSignature + tag: Tag + tag_translation: TagTranslation + user_tags_order: UserTagsOrder + verification_code: VerificationCode + punish_record: PunishRecord + search_history: SearchHistory + payout_account: PayoutAccount + transaction: Transaction + blockchain_transaction: BlockchainTransaction + blockchain_sync_record: BlockchainSyncRecord + entity_type: EntityType + appreciation: Appreciation + seeding_user: SeedingUser + user_oauth_likecoin: UserOauthLikecoinDB + user_badge: UserBadge + report: Report + recommended_articles_from_read_tags_materialized: RecommendedArticlesFromReadTagsMaterialized + matters_choice_topic: MattersChoiceTopic } -interface FindUniqueInput { - table: TableName - where: { id: string } +type TableTypeMapKey = keyof TableTypeMap + +interface InitLoaderInput { + table: TableTypeMapKey + mode: Mode + error?: Error } -interface FindFirstInput { - table: TableName - select?: string[] - where: Record +type FindUniqueFn = < + Table extends TableTypeMapKey, + D extends TableTypeMap[Table] +>(params: { + table: Table + where: { id: string } | { hash: string } | { uuid: string } +}) => Promise + +type FindFirstFn = < + Table extends TableTypeMapKey, + D extends TableTypeMap[Table] +>(params: { + table: Table + select?: keyof D[] + where: + | Partial> + | ((builder: Knex.QueryBuilder) => Knex.QueryBuilder) whereIn?: [string, string[]] orderBy?: Array<{ column: string; order: 'asc' | 'desc' }> -} +}) => Promise + +type FindManyFn = < + Table extends TableTypeMapKey, + D extends TableTypeMap[Table] +>(params: { + table: Table + select?: Array + where?: + | Partial> + | ((builder: Knex.QueryBuilder) => Knex.QueryBuilder) -interface FindManyInput { - table: TableName - select?: string[] - where?: Record whereIn?: [string, string[]] orderBy?: Array<{ column: string; order: 'asc' | 'desc' }> orderByRaw?: string modifier?: (builder: Knex.QueryBuilder) => void skip?: number take?: number -} - -interface CreateInput { - table: TableName - data: Record -} - -interface UpdateInput { - table: TableName - where: Record - data: Record - columns?: string | string[] -} - -interface UpdateJsonColumnInput { - table: TableName - where: Record +}) => Promise + +type CreateFn = < + Table extends TableTypeMapKey, + D extends TableTypeMap[Table] +>(params: { + table: Table + data: Partial +}) => Promise + +type UpdateFn = < + Table extends TableTypeMapKey, + D extends TableTypeMap[Table] +>(params: { + table: Table + where: Partial> + data: Partial + columns?: Array | '*' +}) => Promise + +type UpdateManyFn = < + Table extends TableTypeMapKey, + D extends TableTypeMap[Table] +>(params: { + table: Table + where: Partial> + data: Partial + columns?: Array | '*' +}) => Promise + +type UpdateJsonColumnFn = < + Table extends TableTypeMapKey, + D extends TableTypeMap[Table] +>(params: { + table: Table + where: Partial> jsonColumn?: string // default extra column name is 'extra' removeKeys?: string[] // keys to remove from extra json column jsonData?: Record | null // resetNull?; boolean - columns?: string | string[] // returning columns -} - -interface UpsertInput { - table: TableName - where?: Record - create: Record - update: Record -} - -interface DeleteManyInput { - table: TableName - where?: Record + columns?: string[] | '*' // returning columns +}) => Promise + +type UpsertFn = < + Table extends TableTypeMapKey, + D extends TableTypeMap[Table] +>(params: { + table: Table + where?: Partial> + create: Partial + update: Partial +}) => Promise + +type DeleteManyFn = < + Table extends TableTypeMapKey, + D extends TableTypeMap[Table] +>(params: { + table: Table + where?: Partial> whereIn?: [string, string[]] -} - -interface CountInput { - table: TableName - where: Record +}) => Promise + +type CountFn = < + Table extends TableTypeMapKey, + D extends TableTypeMap[Table] +>(params: { + table: Table + where?: Partial> whereIn?: [string, string[]] -} - -interface MaxInput { - table: TableName - where?: Record - column: string +}) => Promise + +type MaxFn = < + Table extends TableTypeMapKey, + D extends TableTypeMap[Table] +>(params: { + table: Table + where: Partial> + column: keyof D +}) => Promise + +interface AtomDataLoader { + load: (key: K) => Promise + loadMany: (keys: readonly K[]) => Promise } /** * This object is a container for data loaders or system wide services. */ export class AtomService { - aws: typeof aws - cfsvc: typeof cfsvc - knex: Knex - - circleIdLoader: DataLoader - draftIdLoader: DataLoader - userIdLoader: DataLoader - topicIdLoader: DataLoader - chapterIdLoader: DataLoader - - constructor(connections: Connections) { - this.aws = aws - this.cfsvc = cfsvc + private knex: Knex + + public articleIdLoader: AtomDataLoader + public articleVersionIdLoader: AtomDataLoader + public articleContentIdLoader: AtomDataLoader + public circleIdLoader: AtomDataLoader + public commentIdLoader: AtomDataLoader + public collectionIdLoader: AtomDataLoader + public draftIdLoader: AtomDataLoader + public userIdLoader: AtomDataLoader + public tagIdLoader: AtomDataLoader + public transactionIdLoader: AtomDataLoader + public icymiTopicIdLoader: AtomDataLoader + + public constructor(connections: Connections) { this.knex = connections.knex - this.circleIdLoader = this.initLoader({ table: 'circle', mode: 'id' }) + this.articleIdLoader = this.initLoader({ + table: 'article', + mode: 'id', + error: new ArticleNotFoundError('Cannot find article'), + }) + this.articleVersionIdLoader = this.initLoader({ + table: 'article_version', + mode: 'id', + }) + this.articleContentIdLoader = this.initLoader({ + table: 'article_content', + mode: 'id', + }) this.draftIdLoader = this.initLoader({ table: 'draft', mode: 'id' }) + this.commentIdLoader = this.initLoader({ + table: 'comment', + mode: 'id', + error: new CommentNotFoundError('Cannot find comment'), + }) + this.collectionIdLoader = this.initLoader({ + table: 'collection', + mode: 'id', + }) + this.circleIdLoader = this.initLoader({ table: 'circle', mode: 'id' }) this.userIdLoader = this.initLoader({ table: 'user', mode: 'id' }) - this.topicIdLoader = this.initLoader({ table: 'topic', mode: 'id' }) - this.chapterIdLoader = this.initLoader({ table: 'chapter', mode: 'id' }) + this.tagIdLoader = this.initLoader({ table: 'tag', mode: 'id' }) + this.transactionIdLoader = this.initLoader({ + table: 'transaction', + mode: 'id', + }) + this.icymiTopicIdLoader = this.initLoader({ + table: 'matters_choice_topic', + mode: 'id', + }) } /* Data Loader */ /** * Initialize typical data loader. + * + * @remark + * + * loader throw error when it cannot find some entities. */ - initLoader = ({ table, mode }: InitLoaderInput) => { + public initLoader = ({ + table, + mode, + error, + }: InitLoaderInput): AtomDataLoader => { const batchFn = async (keys: readonly string[]) => { - let records = await this.findMany({ + const records = await this.findMany({ table, whereIn: [mode, keys as string[]], }) - if (records.findIndex((item: any) => !item) >= 0) { + if (records.findIndex((item: unknown) => !item) >= 0) { + if (error) { + throw error + } throw new EntityNotFoundError(`Cannot find entity from ${table}`) } // fix order based on keys - records = keys.map((key) => records.find((r: any) => r[mode] === key)) - - return records + return keys.map((key) => + records.find((r: any) => r[mode] === key.toString()) + ) as T[] } - return new DataLoader(batchFn) + return new DataLoader(batchFn) as AtomDataLoader } /* Basic CRUD */ @@ -140,7 +360,7 @@ export class AtomService { * * A Prisma like method for retrieving a record by specified id. */ - findUnique = async ({ table, where }: FindUniqueInput) => + public findUnique: FindUniqueFn = async ({ table, where }) => this.knex.select().from(table).where(where).first() /** @@ -148,7 +368,12 @@ export class AtomService { * * A Prisma like method for getting the first record in rows. */ - findFirst = async ({ table, where, whereIn, orderBy }: FindFirstInput) => { + public findFirst: FindFirstFn = async ({ + table, + where, + whereIn, + orderBy, + }) => { const query = this.knex.select().from(table).where(where) if (whereIn) { @@ -165,9 +390,9 @@ export class AtomService { /** * Find multiple records by given clauses. * - * A Prisma like mehtod for fetching records. + * A Prisma like method for fetching records. */ - findMany = async ({ + public findMany: FindManyFn = async ({ table, select = ['*'], where, @@ -177,7 +402,7 @@ export class AtomService { modifier, skip, take, - }: FindManyInput) => { + }) => { const query = this.knex.select(select).from(table) if (where) { @@ -215,7 +440,7 @@ export class AtomService { * * A Prisma like method for creating one record. */ - create = async ({ table, data }: CreateInput) => { + public create: CreateFn = async ({ table, data }) => { const [record] = await this.knex(table).insert(data).returning('*') return record } @@ -225,16 +450,20 @@ export class AtomService { * * A Prisma like method for updating a record. */ - update = async ({ table, where, data, columns = '*' }: UpdateInput) => { + public update: UpdateFn = async ({ table, where, data, columns = '*' }) => { const [record] = await this.knex .where(where) - .update(data) + .update( + isUpdateableTable(table) + ? { ...data, updatedAt: this.knex.fn.now() } + : data + ) .into(table) - .returning(columns) + .returning(columns as string) return record } - updateJsonColumn = async ({ + public updateJsonColumn: UpdateJsonColumnFn = async ({ table, where, jsonColumn = 'extra', // the json column's name @@ -242,7 +471,7 @@ export class AtomService { jsonData, // the extra data to append into jsonb data // resetNull, columns = '*', - }: UpdateJsonColumnInput) => { + }) => { const [record] = await this.knex .table(table) .where(where) @@ -269,17 +498,32 @@ export class AtomService { * * A Prisma like method for updating many records. */ - updateMany = async ({ table, where, data, columns = '*' }: UpdateInput) => - this.knex.where(where).update(data).into(table).returning(columns) + public updateMany: UpdateManyFn = async ({ + table, + where, + data, + columns = '*', + }) => { + const records = await this.knex + .where(where) + .update( + isUpdateableTable(table) + ? { ...data, updatedAt: this.knex.fn.now() } + : data + ) + .into(table) + .returning(columns as string) + return records + } /** * Upsert an unique record. * * A Prisma like method for updating or creating a record. */ - upsert = async ({ table, where, create, update }: UpsertInput) => { + public upsert: UpsertFn = async ({ table, where, create, update }) => { // TODO: Use onConflict instead - // @see {@url https://github.com/knex/knex/pull/3763} + // @see {@link https://github.com/knex/knex/pull/3763} const record = await this.knex(table) .select() .where(where as Record) @@ -293,7 +537,11 @@ export class AtomService { // update const [updatedRecord] = await this.knex(table) .where(where as Record) - .update(update) + .update( + isUpdateableTable(table) + ? { ...update, updatedAt: this.knex.fn.now() } + : update + ) .returning('*') return updatedRecord @@ -304,7 +552,7 @@ export class AtomService { * * A Prisma like method for deleting multiple records. */ - deleteMany = async ({ table, where, whereIn }: DeleteManyInput) => { + public deleteMany: DeleteManyFn = async ({ table, where, whereIn }) => { const action = this.knex(table) if (where) { action.where(where as Record) @@ -320,8 +568,11 @@ export class AtomService { * * A Prisma like method for counting records. */ - count = async ({ table, where, whereIn }: CountInput) => { - const action = this.knex.count().from(table).where(where) + public count: CountFn = async ({ table, where, whereIn }) => { + const action = this.knex.count().from(table) + if (where) { + action.where(where) + } if (whereIn) { action.whereIn(...whereIn) } @@ -335,11 +586,70 @@ export class AtomService { * * A Prisma like method for getting max. */ - max = async ({ table, where, column }: MaxInput) => { - const record = await this.knex(table) - .max(column) - .where(where as Record) - .first() + public max: MaxFn = async ({ table, where, column }) => { + const record = await this.knex(table).max(column).where(where).first() return parseInt(record ? (record.count as string) : '0', 10) } } + +export const isUpdateableTable = (table: TableName) => + UPATEABLE_TABLES.includes(table) + +const UPATEABLE_TABLES = [ + 'user', + 'user_oauth', + 'user_notify_setting', + 'article', + 'tag', + 'article_tag', + 'comment', + 'action_user', + 'action_comment', + 'action_article', + 'draft', + 'audio_draft', + 'notice', + 'asset', + 'verification_code', + 'push_device', + 'matters_today', + 'matters_choice', + 'article_boost', + 'tag_boost', + 'user_boost', + 'article_version', + 'article_connection', + 'oauth_client', + 'oauth_access_token', + 'oauth_authorization_code', + 'oauth_refresh_token', + 'user_oauth_likecoin', + 'article_read_count', + 'blocklist', + 'transaction', + 'punish_record', + 'feature_flag', + 'payout_account', + 'action_tag', + 'matters_choice_tag', + 'circle', + 'action_circle', + 'circle_price', + 'article_circle', + 'circle_subscription', + 'circle_subscription_item', + 'circle_invoice', + 'seeding_user', + 'announcement', + 'crypto_wallet', + 'crypto_wallet_signature', + 'article_translation', + 'tag_translation', + 'user_ipns_keys', + 'user_tags_order', + 'announcement_translation', + 'blockchain_sync_record', + 'blockchain_transaction', + 'collection', + 'matters_choice_topic', +] diff --git a/src/connectors/aws/index.ts b/src/connectors/aws/index.ts index 5e6eb5214..f18624165 100644 --- a/src/connectors/aws/index.ts +++ b/src/connectors/aws/index.ts @@ -11,12 +11,11 @@ import { getLogger } from 'common/logger' const logger = getLogger('service-aws') export class AWSService { - s3: AWS.S3 - sqs: AWS.SQS - sns?: AWS.SNS - cloudwatch: AWS.CloudWatch - s3Bucket: string - s3Endpoint: string + public s3: AWS.S3 + public sqs: AWS.SQS + public cloudwatch: AWS.CloudWatch + public s3Bucket: string + public s3Endpoint: string public constructor() { AWS.config.update(this.getAWSConfig()) @@ -24,9 +23,6 @@ export class AWSService { this.s3Bucket = this.getS3Bucket() this.s3Endpoint = this.getS3Endpoint() this.sqs = new AWS.SQS() - if (environment.awsArticlesSnsTopic) { - this.sns = new AWS.SNS() - } this.cloudwatch = new AWS.CloudWatch() } @@ -92,6 +88,7 @@ export class AWSService { ) { return key } + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { switch (err.code) { case 'NotFound': @@ -155,38 +152,6 @@ export class AWSService { ) } - public snsPublishMessage = async ({ - // MessageGroupId, - MessageBody, - }: { - // MessageGroupId: string - // Message: any - MessageBody: any - }) => { - if (isTest) { - return - } - const res = (await this.sns - ?.publish({ - Message: JSON.stringify({ - default: JSON.stringify(MessageBody), - }), - MessageStructure: 'json', - // MessageGroupId, - // MessageAttributes: {}, - // MessageDeduplicationId - // MessageBody: JSON.stringify(MessageBody), - // QueueUrl: environment.awsIpfsArticlesQueueUrl, - TopicArn: environment.awsArticlesSnsTopic, - }) - .promise()) as any - logger.info( - 'SNS sent message %j with request-id %s', - MessageBody, - res.ResponseMetadata.RequestId - ) - } - public putMetricData = async ({ MetricData, Namespace = 'MattersDev/Server', diff --git a/src/connectors/baseService.ts b/src/connectors/baseService.ts index 6d1c351e2..2c5b51b82 100644 --- a/src/connectors/baseService.ts +++ b/src/connectors/baseService.ts @@ -1,24 +1,26 @@ -import type { Connections } from 'definitions' +import type { Connections, BaseDBSchema } from 'definitions' import type { Redis } from 'ioredis' import { Knex } from 'knex' -import _ from 'lodash' import { getLogger } from 'common/logger' import { aws, cfsvc } from 'connectors' +import { AtomService, isUpdateableTable } from 'connectors' import { ItemData, TableName } from 'definitions' const logger = getLogger('service-base') -export class BaseService { +export class BaseService { protected table: TableName - protected aws: typeof aws - protected cfsvc: typeof cfsvc protected connections: Connections protected knex: Knex protected knexRO: Knex protected searchKnex: Knex protected redis: Redis + protected models: AtomService + + public aws: typeof aws + public cfsvc: typeof cfsvc public constructor(table: TableName, connections: Connections) { this.table = table @@ -29,8 +31,12 @@ export class BaseService { this.redis = connections.redis this.aws = aws this.cfsvc = cfsvc + this.models = new AtomService(connections) } + /** + * @deprecated Use `AtomService.count` instead + */ public baseCount = async ( where?: { [key: string]: any }, table?: TableName @@ -49,11 +55,13 @@ export class BaseService { /** * Find an item by a given id. + * + * @deprecated Use `AtomService.findUnique` instead */ - public baseFindById = async ( + public baseFindById = async ( id: string, table?: TableName - ): Promise => + ): Promise => this.knex // .select() .from(table || this.table) .where({ id }) @@ -61,27 +69,28 @@ export class BaseService { /** * Find items by given ids. + * + * @deprecated Use `AtomService.findMany` instead */ - - public baseFindByIds = async (ids: readonly string[], table?: TableName) => { - let rows = await this.knex + public baseFindByIds = async ( + ids: readonly string[], + table?: TableName + ): Promise => { + const rows = await this.knex .select() .from(table || this.table) .whereIn('id', ids as string[]) - rows = ids.map((id) => rows.find((r: any) => r.id === id)) - - return rows + return ids.map((id) => rows.find((r) => r.id === id)) } /** * Find an item by a given uuid. - * */ - public baseFindByUUID = async ( + public baseFindByUUID = async ( uuid: string, table?: TableName - ): Promise => { + ): Promise => { const result = await this.knex .select() .from(table || this.table) @@ -97,22 +106,22 @@ export class BaseService { /** * Find items by given ids. */ - public baseFindByUUIDs = async ( + public baseFindByUUIDs = async ( uuids: readonly string[], table?: TableName - ) => { - let rows = await this.knex + ): Promise => { + const rows = await this.knex .select() .from(table || this.table) .whereIn('uuid', uuids as string[]) - rows = uuids.map((uuid) => rows.find((r: any) => r.uuid === uuid)) - - return rows + return uuids.map((uuid) => rows.find((r) => r.uuid === uuid)) } /** * Find items by given "where", "offset" and "limit" + * + * @deprecated Use `AtomService.findMany` instead */ public baseFind = async ({ table, @@ -134,7 +143,7 @@ export class BaseService { }) => { if (returnTotalCount) { select.push( - this.knex.raw('count(1) OVER() AS total_count') as any as string + this.knex.raw('count(1) OVER() AS total_count') as unknown as string ) } @@ -159,15 +168,21 @@ export class BaseService { /** * Create item + * + * @privateRemarks + * + * `U extends S = S` is used to disable type inference from parameters and make S use T from Class generic when S not provide + * this idea is from {@link https://github.com/Microsoft/TypeScript/issues/14829#issuecomment-288902999} + * + * @deprecated Use `AtomService.create` instead */ - public baseCreate = async ( - data: ItemData, + public baseCreate = async ( + data: Partial, table?: TableName, columns: string[] = ['*'], - // onConflict?: [ 'ignore' ] | [ 'merge' ], modifier?: (builder: Knex.QueryBuilder) => void, trx?: Knex.Transaction - ) => { + ): Promise => { try { const query = this.knex(table || this.table) .insert(data) @@ -190,38 +205,38 @@ export class BaseService { /** * Create a batch of items */ - public baseBatchCreate = async ( - dataItems: ItemData[], + public baseBatchCreate = async ( + dataItems: Array>>, table?: TableName, trx?: Knex.Transaction - ) => { + ): Promise => { const query = this.knex .batchInsert(table || this.table, dataItems) .returning('*') if (trx) { query.transacting(trx) } - return query + return query as unknown as Promise } /** * Create or Update Item + * + * @deprecated Use `AtomService.upsert` instead */ - public baseUpdateOrCreate = async ({ + public baseUpdateOrCreate = async ({ where, data, table, createOptions, - updateUpdatedAt, trx, }: { - where: { [key: string]: any } - data: ItemData + where: Partial + data: Partial table?: TableName createOptions?: { [key: string]: any } - updateUpdatedAt?: boolean trx?: Knex.Transaction - }) => { + }): Promise => { const tableName = table || this.table const item = await this.knex(tableName).select().where(where).first() @@ -237,10 +252,11 @@ export class BaseService { // update const query = this.knex(tableName) .where(where) - .update({ - ...data, - ...(updateUpdatedAt ? { updatedAt: this.knex.fn.now() } : null), - }) + .update( + isUpdateableTable(tableName) + ? { ...data, updatedAt: this.knex.fn.now() } + : data + ) .returning('*') if (trx) { @@ -254,8 +270,9 @@ export class BaseService { /** * Find or Create Item + * */ - public baseFindOrCreate = async ({ + public baseFindOrCreate = async ({ where, data, table, @@ -265,13 +282,13 @@ export class BaseService { trx, }: { where: { [key: string]: any } - data: ItemData + data: Partial table?: TableName columns?: string[] modifier?: (builder: Knex.QueryBuilder) => void skipCreate?: boolean trx?: Knex.Transaction - }) => { + }): Promise => { const tableName = table || this.table const item = await this.knex(tableName).select(columns).where(where).first() @@ -286,16 +303,22 @@ export class BaseService { /** * Update an item by a given id. + * + * @deprecated Use `AtomService.update` instead */ - public baseUpdate = async ( + public baseUpdate = async ( id: string, - data: ItemData, + data: Partial, table?: TableName, trx?: Knex.Transaction - ) => { + ): Promise => { const query = this.knex .where('id', id) - .update({ ...data, updatedAt: this.knex.fn.now() }) + .update( + isUpdateableTable(table || this.table) + ? { ...data, updatedAt: this.knex.fn.now() } + : data + ) .into(table || this.table) .returning('*') @@ -309,20 +332,27 @@ export class BaseService { } /** * Update a batch of items by given ids. + * + * @deprecated Use `AtomService.updateMany` instead */ - public baseBatchUpdate = async ( + public baseBatchUpdate = async ( ids: string[], data: ItemData, table?: TableName - ) => + ): Promise => this.knex .whereIn('id', ids) - .update(data) + .update( + isUpdateableTable(table || this.table) + ? { ...data, updatedAt: this.knex.fn.now() } + : data + ) .into(table || this.table) .returning('*') /** * Delete an item by a given id. + * */ public baseDelete = async (id: string, table?: TableName) => this.knex(table || this.table) @@ -331,6 +361,8 @@ export class BaseService { /** * Delete a batch of items by given ids. + * + * @deprecated Use `AtomService.deleteMany` instead */ protected baseBatchDelete = async (ids: string[], table?: TableName) => this.knex(table || this.table) @@ -340,7 +372,7 @@ export class BaseService { /** * Find entity type id by a given type string. */ - public baseFindEntityTypeId = async (entityType: string) => + public baseFindEntityTypeId = async (entityType: TableName) => this.knexRO('entity_type').select('id').where({ table: entityType }).first() /** diff --git a/src/connectors/collectionService.ts b/src/connectors/collectionService.ts index c41e5dc6e..b7768975e 100644 --- a/src/connectors/collectionService.ts +++ b/src/connectors/collectionService.ts @@ -1,11 +1,5 @@ -import type { - Item, - Collection, - CollectionArticle, - Connections, -} from 'definitions' +import type { Collection, CollectionArticle, Connections } from 'definitions' -import DataLoader from 'dataloader' import { Knex } from 'knex' import { ARTICLE_STATE, MAX_PINNED_WORKS_LIMIT } from 'common/enums' @@ -17,24 +11,12 @@ import { ActionLimitExceededError, } from 'common/errors' import { BaseService, UserService } from 'connectors' -// import { getLogger } from 'common/logger' - -// const logger = getLogger('service-collection') - -export class CollectionService extends BaseService { - public dataloader: DataLoader +export class CollectionService extends BaseService { public constructor(connections: Connections) { super('collection', connections) - this.dataloader = new DataLoader(this.baseFindByIds) } - public loadById = async (id: string): Promise => - this.dataloader.load(id) as Promise - - public loadByIds = async (ids: readonly string[]): Promise => - this.dataloader.loadMany(ids) as Promise - public addArticles = async ( collectionId: string, articleIds: readonly string[] @@ -50,7 +32,7 @@ export class CollectionService extends BaseService { order: initOrder + index, })) ) - await this.baseUpdate(collectionId, { updatedAt: this.knex.fn.now() }) + await this.baseUpdate(collectionId, {}) } public createCollection = async ({ @@ -90,7 +72,6 @@ export class CollectionService extends BaseService { title, cover, description, - updatedAt: this.knex.fn.now(), }) public findAndCountCollectionsByUser = async ( @@ -119,7 +100,6 @@ export class CollectionService extends BaseService { const records = await this.knex('collection_article') .select( 'article_id', - 'draft_id', 'order', this.knex.raw('count(1) OVER() AS total_count') ) @@ -156,7 +136,7 @@ export class CollectionService extends BaseService { if (ids.length === 0) { return false } - const collections = await this.loadByIds(ids) + const collections = await this.models.collectionIdLoader.loadMany(ids) for (const collection of collections) { if (!collection) { @@ -186,7 +166,7 @@ export class CollectionService extends BaseService { .where('collection_id', collectionId) .whereIn('article_id', articleIds) .del() - await this.baseUpdate(collectionId, { updatedAt: this.knex.fn.now() }) + await this.baseUpdate(collectionId, {}) } public reorderArticles = async ( @@ -207,7 +187,7 @@ export class CollectionService extends BaseService { throw new UserInputError('Invalid articleId') } - await this.baseUpdate(collectionId, { updatedAt: this.knex.fn.now() }) + await this.baseUpdate(collectionId, {}) for (const { articleId, newPosition } of moves) { if ( @@ -266,7 +246,7 @@ export class CollectionService extends BaseService { userId: string, pinned: boolean ) => { - const collection = await this.loadById(collectionId) + const collection = await this.models.collectionIdLoader.load(collectionId) if (!collection) { throw new EntityNotFoundError('Collection not found') } @@ -285,11 +265,35 @@ export class CollectionService extends BaseService { } await this.baseUpdate(collectionId, { pinned, - pinnedAt: this.knex.fn.now(), + pinnedAt: this.knex.fn.now() as unknown as Date, }) return { ...collection, pinned } } + public findByAuthor = async ( + authorId: string, + { skip, take }: { skip?: number; take?: number } = {}, + filterEmpty = false + ): Promise => + this.knexRO('collection') + .where({ authorId }) + .orderBy('updatedAt', 'desc') + .modify((builder) => { + if (filterEmpty) { + builder.whereExists( + this.knexRO('collection_article') + .select(this.knex.raw(1)) + .whereRaw('collection.id = collection_article.collection_id') + ) + } + if (skip !== undefined && Number.isFinite(skip)) { + builder.offset(skip) + } + if (take !== undefined && Number.isFinite(take)) { + builder.limit(take) + } + }) + public findPinnedByAuthor = async (authorId: string) => this.baseFind({ where: { authorId, pinned: true } }) } diff --git a/src/connectors/commentService.ts b/src/connectors/commentService.ts index 98b49fd85..e0aff04e0 100644 --- a/src/connectors/commentService.ts +++ b/src/connectors/commentService.ts @@ -4,48 +4,31 @@ import type { GQLVote, Comment, Connections, - Item, + ValueOf, } from 'definitions' -import DataLoader from 'dataloader' - import { ARTICLE_PIN_COMMENT_LIMIT, COMMENT_STATE, COMMENT_TYPE, USER_ACTION, } from 'common/enums' -import { CommentNotFoundError } from 'common/errors' import { BaseService } from 'connectors' -interface CommentFilter { - targetId?: string - targetTypeId?: string +export interface CommentFilter { + type: ValueOf + targetId: string + targetTypeId: string + parentCommentId?: string | null authorId?: string state?: string - parentCommentId?: string | null } -export class CommentService extends BaseService { - public dataloader: DataLoader - +export class CommentService extends BaseService { public constructor(connections: Connections) { super('comment', connections) - this.dataloader = new DataLoader(async (ids: readonly string[]) => { - const result = await this.baseFindByIds(ids) - - if (result.findIndex((item: any) => !item) >= 0) { - throw new CommentNotFoundError('Cannot find comment') - } - return result - }) } - public loadById = async (id: string): Promise => - this.dataloader.load(id) as Promise - public loadByIds = async (ids: string[]): Promise => - this.dataloader.loadMany(ids) as Promise - /** * Count comments by a given article id. */ @@ -74,14 +57,18 @@ export class CommentService extends BaseService { id: string skip?: number take?: number - }) => { + }): Promise<[Comment[], number]> => { let where: { [key: string]: string | boolean } = { parentCommentId: id, } let query = null const sortCreatedAt = (by: 'desc' | 'asc') => - this.knex.select().from(this.table).where(where).orderBy('created_at', by) + this.knex + .select(['*', this.knex.raw('count(1) OVER() AS total_count')]) + .from(this.table) + .where(where) + .orderBy('created_at', by) if (author) { where = { ...where, authorId: author } @@ -101,8 +88,9 @@ export class CommentService extends BaseService { if (take || take === 0) { query.limit(take) } + const records = await query - return query + return [records, records[0] ? parseInt(records[0].totalCount, 10) : 0] } /** @@ -117,21 +105,36 @@ export class CommentService extends BaseService { includeAfter = false, includeBefore = false, }: GQLCommentsInput & { where: CommentFilter; order?: string }): Promise< - Comment[] + [Comment[], number] > => { - const query = this.knex - .select() - .from(this.table) + const subQuery = this.knexRO + .select(this.knexRO.raw('COUNT(id) OVER() AS total_count'), '*') + .fromRaw('comment AS outer_comment') .where(where) + .andWhere((andWhereBuilder) => { + // filter archived/banned comments when `where.state` params is not specified + // and where.parent_comment_id is specified + // as we don't want to show archived/banned comments for normal users, but not the case in oss + if (!('state' in where) && 'parentCommentId' in where) { + andWhereBuilder + .where({ state: COMMENT_STATE.active }) + .orWhere({ state: COMMENT_STATE.collapsed }) + .orWhere((orWhereBuilder) => { + orWhereBuilder + .andWhere( + this.knexRO.raw( + '(SELECT COUNT(1) FROM comment WHERE state in (?, ?) and parent_comment_id = outer_comment.id)', + [COMMENT_STATE.active, COMMENT_STATE.collapsed] + ), + '>', + 0 + ) + }) + } + }) .orderBy('created_at', order) - if (before) { - if (includeBefore) { - query.andWhere('id', order === 'asc' ? '<=' : '>=', before) - } else { - query.andWhere('id', order === 'asc' ? '<' : '>', before) - } - } + const query = this.knexRO.from(subQuery.as('t1')) if (after) { if (includeAfter) { @@ -140,32 +143,18 @@ export class CommentService extends BaseService { query.andWhere('id', order === 'asc' ? '>' : '<', after) } } - + if (before) { + if (includeBefore) { + query.andWhere('id', order === 'asc' ? '<=' : '>=', before) + } else { + query.andWhere('id', order === 'asc' ? '<' : '>', before) + } + } if (first) { query.limit(first) } - - return query - } - - /** - * Find id range with given filter - */ - public range = async (where: CommentFilter) => { - const { count, max, min } = (await this.knex - .select() - .from(this.table) - .where(where) - .min('id') - .max('id') - .count() - .first()) as Record - - return { - count: parseInt(count, 10), - min: parseInt(min, 10), - max: parseInt(max, 10), - } + const records = await query + return [records, +records[0]?.totalCount || 0] } /********************************* @@ -190,7 +179,7 @@ export class CommentService extends BaseService { return this.baseUpdateOrCreate({ where: data, - data: { updatedAt: new Date(), ...data }, + data: data, table: 'action_comment', }) } diff --git a/src/connectors/draftService.ts b/src/connectors/draftService.ts index b6d4fe81e..65dc20df5 100644 --- a/src/connectors/draftService.ts +++ b/src/connectors/draftService.ts @@ -1,23 +1,13 @@ -import type { Draft, Connections, Item } from 'definitions' - -import DataLoader from 'dataloader' +import type { Draft, Connections } from 'definitions' import { PUBLISH_STATE } from 'common/enums' import { BaseService } from 'connectors' -export class DraftService extends BaseService { - public dataloader: DataLoader - +export class DraftService extends BaseService { public constructor(connections: Connections) { super('draft', connections) - this.dataloader = new DataLoader(this.baseFindByIds) } - public loadById = async (id: string): Promise => - this.dataloader.load(id) as Promise - public loadByIds = async (ids: string[]): Promise => - this.dataloader.loadMany(ids) as Promise - public countByAuthor = async (authorId: string) => { const result = await this.knex(this.table) .where({ authorId, archived: false }) @@ -51,7 +41,4 @@ export class DraftService extends BaseService { .where({ authorId, archived: false }) .andWhereNot({ publishState: PUBLISH_STATE.published }) .orderBy('updated_at', 'desc') - - public findByMediaHash = async (mediaHash: string) => - this.knex.select().from(this.table).where({ mediaHash }).first() } diff --git a/src/connectors/gcp/index.ts b/src/connectors/gcp/index.ts index 0c182ea5a..6c78dd566 100644 --- a/src/connectors/gcp/index.ts +++ b/src/connectors/gcp/index.ts @@ -1,9 +1,7 @@ import { v3 as TranslateAPI } from '@google-cloud/translate' -import axios from 'axios' import { LANGUAGE } from 'common/enums' -import { environment, isTest } from 'common/environment' -import { ActionFailedError, UserInputError } from 'common/errors' +import { environment } from 'common/environment' import { getLogger } from 'common/logger' const logger = getLogger('service-gcp') @@ -13,7 +11,7 @@ const { zh_hans, zh_hant, en } = LANGUAGE export class GCP { private translateAPI: TranslateAPI.TranslationServiceClient - constructor() { + public constructor() { this.translateAPI = new TranslateAPI.TranslationServiceClient({ projectId: environment.gcpProjectId, keyFilename: environment.translateCertPath, @@ -90,45 +88,6 @@ export class GCP { return } } - - public recaptcha = async ({ token, ip }: { token?: string; ip?: string }) => { - // skip test - if (isTest) { - return true - } - - if (!token) { - throw new UserInputError( - 'operation is only allowed on matters.{town,news}' - ) - } - - // Turing test with recaptcha - const { data } = await axios({ - method: 'post', - url: 'https://www.google.com/recaptcha/api/siteverify', - params: { - secret: environment.recaptchaSecret, - response: token, - remoteip: ip, - }, - }) - - const { success, score } = data - - if (!success) { - logger.warn('gcp recaptcha no success: %j', data) - throw new ActionFailedError(`please try again: ${data['error-codes']}`) - } - - // fail for less than 0.5 - if (score < 0.5) { - logger.info('very likely bot traffic: %j', data) - } - - // pass - return score > 0.0 - } } export const gcp = new GCP() diff --git a/src/connectors/index.ts b/src/connectors/index.ts index 283963524..0f42a89bf 100644 --- a/src/connectors/index.ts +++ b/src/connectors/index.ts @@ -23,3 +23,4 @@ export * from './oauthService' export * from './paymentService' export * from './exchangeRate' export * from './collectionService' +export * from './recommendationService' diff --git a/src/connectors/likecoin/index.ts b/src/connectors/likecoin/index.ts index 4b68304db..7738b6765 100644 --- a/src/connectors/likecoin/index.ts +++ b/src/connectors/likecoin/index.ts @@ -83,7 +83,6 @@ const ENDPOINTS = { total: '/like/info/like/amount', like: '/like/likebutton', rate: '/misc/price', - superlike: '/like/share', iscnPublish: '/iscn/new?claim=1', cosmosTx: '/cosmos/lcd/cosmos/tx/v1beta1/txs', } @@ -430,87 +429,6 @@ export class LikeCoin { messageDeduplicationId: v4(), }) - /** - * Super Like - */ - public superlike = async ({ - authorLikerId, - liker, - iscn_id, - url, - likerIp, - userAgent, - }: { - authorLikerId: string - liker: UserOAuthLikeCoin - iscn_id?: string - url: string - likerIp?: string - userAgent: string - }) => { - const endpoint = `${ENDPOINTS.superlike}/${authorLikerId}/` - const result = await this.request({ - ip: likerIp, - userAgent, - endpoint, - withClientCredential: true, - method: 'POST', - liker, - data: _.omitBy( - { - iscn_id, - referrer: url, // encodeURI(url), - }, - _.isNil - ), - }) - const data = _.get(result, 'data') - if (data) { - return data - } else { - throw result - } - } - - public canSuperLike = async ({ - liker, - iscn_id, - url, - likerIp, - userAgent, - }: { - liker: UserOAuthLikeCoin - iscn_id?: string - url: string - likerIp?: string - userAgent: string - }) => { - const endpoint = `${ENDPOINTS.superlike}/self` - - const res = await this.request({ - endpoint, - method: 'GET', - ip: likerIp, - userAgent, - withClientCredential: true, - params: _.omitBy( - { - iscn_id, - referrer: url, // encodeURI(url), - }, - _.isNil - ), - liker, - }) - const data = _.get(res, 'data') - - if (!data) { - throw res - } - - return data.canSuperLike - } - public iscnPublish = async ({ mediaHash, ipfsHash, @@ -531,7 +449,7 @@ export class LikeCoin { userName: string title: string description: string - datePublished: string // in format like 'YYYY-mm-dd' // "datePublished": "2019-04-19", + datePublished: string // in format like 'YYYY-mm-dd' url: string tags: string[] liker: UserOAuthLikeCoin @@ -571,12 +489,12 @@ export class LikeCoin { }, ], type: 'Article', - name: title, // "使用矩陣計算遞歸關係式", - description, // "description": "An article on computing recursive function with matrix multiplication.", - datePublished, // "datePublished": "2019-04-19", - url, // "url": "https://nnkken.github.io/post/recursive-relation/", + name: title, + description, + datePublished, + url, // "usageInfo": "https://creativecommons.org/licenses/by/4.0", - keywords: tags, // ["matrix","recursion","keyword3"] + keywords: tags, } const res = await this.request({ diff --git a/src/connectors/notificationService/index.ts b/src/connectors/notificationService/index.ts index d56bbe556..268fea39a 100644 --- a/src/connectors/notificationService/index.ts +++ b/src/connectors/notificationService/index.ts @@ -1,4 +1,4 @@ -import type { Connections } from 'definitions' +import type { Connections, UserNotifySetting } from 'definitions' import { BUNDLED_NOTICE_TYPE, @@ -6,13 +6,8 @@ import { OFFICIAL_NOTICE_EXTEND_TYPE, } from 'common/enums' import { getLogger } from 'common/logger' -import { BaseService, UserService } from 'connectors' -import { - LANGUAGES, - NotificationPrarms, - PutNoticeParams, - User, -} from 'definitions' +import { UserService, AtomService, ArticleService } from 'connectors' +import { LANGUAGES, NotificationPrarms, PutNoticeParams } from 'definitions' import { mail } from './mail' import { Notice } from './notice' @@ -20,12 +15,13 @@ import trans from './translations' const logger = getLogger('service-notification') -export class NotificationService extends BaseService { - mail: typeof mail - notice: Notice +export class NotificationService { + public mail: typeof mail + public notice: Notice + private connections: Connections public constructor(connections: Connections) { - super('noop', connections) + this.connections = connections this.mail = mail this.notice = new Notice(connections) } @@ -42,6 +38,7 @@ export class NotificationService extends BaseService { params: NotificationPrarms, language: LANGUAGES ): Promise => { + const articleService = new ArticleService(this.connections) switch (params.event) { // entity-free case DB_NOTICE_TYPE.user_new_follower: @@ -114,7 +111,7 @@ export class NotificationService extends BaseService { data: params.data, // update latest comment to DB `data` field bundle: { mergeData: true }, } - // act as official annonuncement + // act as official announcement case DB_NOTICE_TYPE.official_announcement: return { type: DB_NOTICE_TYPE.official_announcement, @@ -160,7 +157,11 @@ export class NotificationService extends BaseService { type: DB_NOTICE_TYPE.official_announcement, recipientId: params.recipientId, message: trans.article_banned(language, { - title: params.entities[0].entity.title, + title: ( + await articleService.loadLatestArticleVersion( + params.entities[0].entity.id + ) + ).title, }), entities: params.entities, } @@ -178,7 +179,11 @@ export class NotificationService extends BaseService { type: DB_NOTICE_TYPE.official_announcement, recipientId: params.recipientId, message: trans.article_reported(language, { - title: params.entities[0].entity.title, + title: ( + await articleService.loadLatestArticleVersion( + params.entities[0].entity.id + ) + ).title, }), entities: params.entities, } @@ -188,10 +193,8 @@ export class NotificationService extends BaseService { } private async __trigger(params: NotificationPrarms) { - const userService = new UserService(this.connections) - const recipient = (await userService.dataloader.load( - params.recipientId - )) as User + const atomService = new AtomService(this.connections) + const recipient = await atomService.userIdLoader.load(params.recipientId) if (!recipient) { logger.warn(`recipient ${params.recipientId} not found, skipped`) @@ -213,10 +216,11 @@ export class NotificationService extends BaseService { } // skip if user disable notify + const userService = new UserService(this.connections) const notifySetting = await userService.findNotifySetting(recipient.id) const enable = await this.notice.checkUserNotifySetting({ event: params.event, - setting: notifySetting, + setting: notifySetting as UserNotifySetting, }) if (!enable) { diff --git a/src/connectors/notificationService/mail/sendPayment.ts b/src/connectors/notificationService/mail/sendPayment.ts index df76aa4d2..3b9f6addd 100644 --- a/src/connectors/notificationService/mail/sendPayment.ts +++ b/src/connectors/notificationService/mail/sendPayment.ts @@ -1,7 +1,8 @@ +import type { LANGUAGES, UserHasUsername } from 'definitions' + import { EMAIL_TEMPLATE_ID } from 'common/enums' import { environment } from 'common/environment' import { mailService } from 'connectors' -import { LANGUAGES, UserHasUsername } from 'definitions' import { trans } from './utils' @@ -24,7 +25,7 @@ export const sendPayment = async ({ | 'payout' recipient: Pick tx?: { - recipient: Pick + recipient?: Pick sender?: Pick amount: number currency: string diff --git a/src/connectors/notificationService/notice.ts b/src/connectors/notificationService/notice.ts index 55267aab0..d6aa174dd 100644 --- a/src/connectors/notificationService/notice.ts +++ b/src/connectors/notificationService/notice.ts @@ -1,5 +1,6 @@ import type { GQLNotificationSettingType, + Notice as NoticeDB, NoticeData, NoticeDetail, NoticeEntitiesMap, @@ -33,7 +34,7 @@ const mergeDataCustomizer = (objValue: any, srcValue: any) => { const mergeDataWith = (objValue: any, srcValue: any) => mergeWith(objValue, srcValue, mergeDataCustomizer) -export class Notice extends BaseService { +export class Notice extends BaseService { public dataloader: DataLoader public constructor(connections: Connections) { diff --git a/src/connectors/oauthService.ts b/src/connectors/oauthService.ts index ceb6d1879..03b469918 100644 --- a/src/connectors/oauthService.ts +++ b/src/connectors/oauthService.ts @@ -1,7 +1,11 @@ import type { Falsey, OAuthAuthorizationCode, + OAuthAuthorizationCodeDB, OAuthClient, + OAuthClientDB, + OAuthAccessTokenDB, + OAuthRefreshTokenDB, OAuthRefreshToken, OAuthToken, User, @@ -19,11 +23,11 @@ import { import { environment } from 'common/environment' import { getLogger } from 'common/logger' import { isScopeAllowed, toGlobalId } from 'common/utils' -import { BaseService, UserService } from 'connectors' +import { BaseService } from 'connectors' const logger = getLogger('service-oauth') -export class OAuthService extends BaseService { +export class OAuthService extends BaseService { public constructor(connections: Connections) { super('oauth_client', connections) } @@ -53,10 +57,7 @@ export class OAuthService extends BaseService { }) => this.baseUpdateOrCreate({ where: { clientId: params.clientId }, - data: { - ...params, - updatedAt: new Date(), - }, + data: params, table: 'oauth_client', }) @@ -72,7 +73,7 @@ export class OAuthService extends BaseService { return this.toOAuthClient(client) } - private toOAuthClient = (dbClient: any) => { + private toOAuthClient = (dbClient: OAuthClientDB): OAuthClient | Falsey => { if (!dbClient) { return } @@ -121,8 +122,7 @@ export class OAuthService extends BaseService { } const client = (await this.getClientById(token.clientId)) as OAuthClient - const userService = new UserService(this.connections) - const user = (await userService.loadById(token.userId)) as User + const user = await this.models.userIdLoader.load(token.userId) return { accessToken: token.token, @@ -140,7 +140,7 @@ export class OAuthService extends BaseService { ): Promise => { const scope = token.scope instanceof Array ? token.scope : [token.scope] - const accessToken = await this.baseCreate( + const accessToken = await this.baseCreate( { token: token.accessToken, expires: token.accessTokenExpiresAt, @@ -150,7 +150,7 @@ export class OAuthService extends BaseService { }, 'oauth_access_token' ) - const refreshToken = await this.baseCreate( + const refreshToken = await this.baseCreate( { token: token.refreshToken, expires: token.refreshTokenExpiresAt, @@ -199,8 +199,7 @@ export class OAuthService extends BaseService { .where({ code: authorizationCode }) .first() const client = (await this.getClientById(code.clientId)) as OAuthClient - const userService = new UserService(this.connections) - const user = (await userService.loadById(code.userId)) as User + const user = await this.models.userIdLoader.load(code.userId) if (!code) { return @@ -221,12 +220,13 @@ export class OAuthService extends BaseService { client: OAuthClient, user: User ): Promise => { - const authorizationCode = await this.baseCreate( + const authorizationCode = await this.baseCreate( { code: code.authorizationCode, expires: code.expiresAt, - redirect_uri: code.redirectUri, - scope: code.scope, + redirectUri: code.redirectUri, + scope: + code.scope instanceof Array ? code.scope : [code.scope as string], clientId: client.id, userId: user.id, }, @@ -239,7 +239,7 @@ export class OAuthService extends BaseService { return { authorizationCode: authorizationCode.code, - expiresAt: new Date(authorizationCode.exipres), + expiresAt: new Date(authorizationCode.expires), redirectUri: authorizationCode.redirectUri, scope: authorizationCode.scope, client, @@ -279,8 +279,7 @@ export class OAuthService extends BaseService { .where({ token: refreshToken }) .first() const client = (await this.getClientById(token.clientId)) as OAuthClient - const userService = new UserService(this.connections) - const user = (await userService.loadById(token.userId)) as User + const user = await this.models.userIdLoader.load(token.userId) if (!token) { return @@ -364,8 +363,7 @@ export class OAuthService extends BaseService { * * *********************************/ public generateTokenForLikeCoin = async ({ userId }: { userId: string }) => { - const userService = new UserService(this.connections) - const user = (await userService.loadById(userId)) as User + const user = await this.models.userIdLoader.load(userId) const name = environment.likecoinOAuthClientName const client = await this.findClientByName({ name }) const oauthClient = this.toOAuthClient(client) diff --git a/src/connectors/opensea/index.ts b/src/connectors/opensea/index.ts index 113ae9ed1..b9d79be88 100644 --- a/src/connectors/opensea/index.ts +++ b/src/connectors/opensea/index.ts @@ -7,9 +7,9 @@ import { getLogger } from 'common/logger' const logger = getLogger('service-opensea') export class OpenSeaService extends RESTDataSource { - apiKey?: string | undefined + private apiKey?: string | undefined - constructor() { + public constructor() { super() // Sets the base URL for the REST API // https://rinkeby-api.opensea.io/api @@ -18,13 +18,13 @@ export class OpenSeaService extends RESTDataSource { this.apiKey = environment.openseaAPIKey } - override willSendRequest(_: string, request: AugmentedRequest) { + public override willSendRequest(_: string, request: AugmentedRequest) { if (this.apiKey) { request.headers['X-API-KEY'] = this.apiKey } } - async getAssets({ + public async getAssets({ owner, asset_contract_address = contract.Ethereum.traveloggersAddress, }: { diff --git a/src/connectors/paymentService.ts b/src/connectors/paymentService.ts index 84e25a1e4..e1042268b 100644 --- a/src/connectors/paymentService.ts +++ b/src/connectors/paymentService.ts @@ -1,12 +1,14 @@ import type { CirclePrice, + Customer, + BlockchainTransaction, Transaction, - EmailableUser, Connections, - Item, + UserHasUsername, + LANGUAGES, } from 'definitions' -import DataLoader from 'dataloader' +import slugify from '@matters/slugify' import { Knex } from 'knex' import { v4 } from 'uuid' @@ -25,22 +27,19 @@ import { import { ServerError } from 'common/errors' import { getLogger } from 'common/logger' import { getUTC8Midnight, numRound } from 'common/utils' -import { AtomService, BaseService, NotificationService } from 'connectors' +import { ArticleService, BaseService, NotificationService } from 'connectors' import { stripe } from './stripe' const logger = getLogger('service-payment') -export class PaymentService extends BaseService { +export class PaymentService extends BaseService { public stripe: typeof stripe - public dataloader: DataLoader public constructor(connections: Connections) { super('transaction', connections) this.stripe = stripe - - this.dataloader = new DataLoader(this.baseFindByIds) } /********************************* @@ -222,11 +221,18 @@ export class PaymentService extends BaseService { const { id: entityTypeId } = await this.baseFindEntityTypeId(targetType) targetTypeId = entityTypeId } + let articleVersionId + if (targetId && targetType === TRANSACTION_TARGET_TYPE.article) { + const articleService = new ArticleService(this.connections) + articleVersionId = ( + await articleService.loadLatestArticleVersion(targetId) + ).id + } return this.baseCreate( { - amount, - fee, + amount: amount.toString(), + fee: fee ? fee.toString() : undefined, state, currency, @@ -238,6 +244,7 @@ export class PaymentService extends BaseService { senderId, recipientId, targetId, + articleVersionId, targetType: targetTypeId, remark, }, @@ -249,7 +256,7 @@ export class PaymentService extends BaseService { } public findBlockchainTransactionById = async (id: string) => - this.baseFindById(id, 'blockchain_transaction') + this.models.findUnique({ table: 'blockchain_transaction', where: { id } }) public findOrCreateBlockchainTransaction = async ( { chainId, txHash }: { chainId: string | number; txHash: string }, @@ -271,7 +278,12 @@ export class PaymentService extends BaseService { ...(data || {}), } - return this.baseFindOrCreate({ where, data: toInsert, table, trx }) + return this.baseFindOrCreate({ + where, + data: toInsert as unknown as BlockchainTransaction, + table, + trx, + }) } public findOrCreateTransactionByBlockchainTxHash = async ({ @@ -369,9 +381,9 @@ export class PaymentService extends BaseService { }, trx?: Knex.Transaction ) => - this.baseUpdate( + this.baseUpdate( id, - { updatedAt: new Date(), state }, + { state }, 'blockchain_transaction', trx ) @@ -398,12 +410,7 @@ export class PaymentService extends BaseService { state, } - return this.baseUpdate( - id, - { updatedAt: new Date(), ...data }, - 'transaction', - trx - ) + return this.baseUpdate(id, data, 'transaction', trx) } /** @@ -456,7 +463,7 @@ export class PaymentService extends BaseService { throw new ServerError('failed to create customer') } - return this.baseCreate( + return this.baseCreate( { userId: user.id, provider, @@ -699,7 +706,10 @@ export class PaymentService extends BaseService { TRANSACTION_TARGET_TYPE.circlePrice ) for (const p of prices) { - const circle = await this.baseFindById(p.circleId, 'circle') + const circle = await this.baseFindById<{ owner: string }>( + p.circleId, + 'circle' + ) await trx('transaction').insert({ amount: p.amount, currency: p.currency, @@ -710,7 +720,7 @@ export class PaymentService extends BaseService { providerTxId: v4(), senderId: userId, - recipientId: circle.owner, + recipientId: circle?.owner, targetType: entityTypeId, targetId: p.id, @@ -1035,10 +1045,30 @@ export class PaymentService extends BaseService { /********************************* * * - * notification * + * Donation * * * *********************************/ + public isDonator = async (userId: string, articleId: string) => { + const { id: entityTypeId } = await this.baseFindEntityTypeId( + TRANSACTION_TARGET_TYPE.article + ) + const count = await this.models.count({ + table: 'transaction', + where: { + purpose: TRANSACTION_PURPOSE.donation, + targetType: entityTypeId, + targetId: articleId, + senderId: userId, + }, + whereIn: [ + 'state', + [TRANSACTION_STATE.succeeded, TRANSACTION_STATE.pending], + ], + }) + return count > 0 + } + public notifyDonation = async ({ tx, sender, @@ -1046,34 +1076,44 @@ export class PaymentService extends BaseService { article, }: { tx: Transaction - sender?: EmailableUser - recipient: EmailableUser + sender?: { + id: string + displayName: string + userName: string + email: string | null + language: LANGUAGES + } + recipient: { + id: string + displayName: string + userName: string + email: string | null + language: LANGUAGES + } article: { - title: string - slug: string + id: string authorId: string - mediaHash: string - draftId: string + shortHash: string } }) => { - const atomService = new AtomService(this.connections) const notificationService = new NotificationService(this.connections) + const articleService = new ArticleService(this.connections) const amount = parseFloat(tx.amount) - const author = await atomService.findFirst({ + const author = (await this.models.findUnique({ table: 'user', where: { id: article.authorId }, - }) - const draft = await atomService.findFirst({ - table: 'draft', - where: { id: article.draftId }, - }) + })) as UserHasUsername + const articleVersion = await articleService.loadLatestArticleVersion( + article.id + ) - const hasReplyToDonator = !!draft.replyToDonator + const hasReplyToDonator = !!articleVersion.replyToDonator const _article = { id: tx.targetId, - title: article.title, - slug: article.slug, - mediaHash: article.mediaHash, + title: articleVersion.title, + slug: slugify(articleVersion.title), + mediaHash: articleVersion.mediaHash, + shortHash: article.shortHash, author: { displayName: author.displayName, userName: author.userName, @@ -1082,7 +1122,7 @@ export class PaymentService extends BaseService { } // send email to sender - if (sender) { + if (sender?.email) { const donationCount = await this.donationCount(sender.id) await notificationService.mail.sendPayment({ to: sender.email, @@ -1116,23 +1156,26 @@ export class PaymentService extends BaseService { ? ('receivedDonationLikeCoin' as const) : ('receivedDonation' as const) - await notificationService.mail.sendPayment({ - to: recipient.email, - recipient: { - displayName: recipient.displayName, - userName: recipient.userName, - }, - type: mailType, - tx: { - recipient, - sender, - amount, - currency: tx.currency, - }, - article: _article, - language: recipient.language, - }) + if (recipient.email) { + await notificationService.mail.sendPayment({ + to: recipient.email, + recipient: { + displayName: recipient.displayName, + userName: recipient.userName, + }, + type: mailType, + tx: { + recipient, + sender, + amount, + currency: tx.currency, + }, + article: _article, + language: recipient.language, + }) + } } + private donationCount = async (senderId: string) => { const result = await this.knex('transaction') .where({ diff --git a/src/connectors/queue/__test__/payToByBlockchain.test.ts b/src/connectors/queue/__test__/payToByBlockchain.test.ts index 8bca6388a..2410ba4b6 100644 --- a/src/connectors/queue/__test__/payToByBlockchain.test.ts +++ b/src/connectors/queue/__test__/payToByBlockchain.test.ts @@ -15,13 +15,13 @@ import { TRANSACTION_STATE, TRANSACTION_TARGET_TYPE, } from 'common/enums' -import { contract } from 'common/environment' import { PaymentQueueJobDataError } from 'common/errors' import { PaymentService } from 'connectors' import { CurationContract } from 'connectors/blockchain' import { PayToByBlockchainQueue } from 'connectors/queue' import { genConnections, closeConnections } from '../../__test__/utils' +import { contract } from 'common/environment' // setup mock @@ -35,7 +35,7 @@ jest.mock('connectors/blockchain', () => ({ fetchLogs: mockFetchLogs, fetchBlockNumber: mockFetchBlockNumber, chainId, - address: contract.Polygon.curationAddress, + address: contract.Optimism.curationAddress, })), })) @@ -47,7 +47,7 @@ let paymentService: PaymentService beforeAll(async () => { connections = await genConnections() paymentService = new PaymentService(connections) -}, 30000) +}, 50000) afterAll(async () => { await closeConnections(connections) @@ -66,7 +66,7 @@ const recipientId = '1' const senderId = '2' const targetId = '1' const targetType = TRANSACTION_TARGET_TYPE.article -const chainId = BLOCKCHAIN_CHAINID.Polygon +const chainId = BLOCKCHAIN_CHAINID.Optimism const invalidTxhash = '0x209375f2de9ee7c2eed5e24eb30d0196a416924cd956a194e7060f9dcb39515b' @@ -84,7 +84,7 @@ const notMinedHash = const invalidTxReceipt = { blockNumber: 1, from: '0x999999cf1046e68e36e1aa2e0e07105eddd1f08f', - to: contract.Polygon.curationAddress, + to: contract.Optimism.curationAddress, txHash: invalidTxhash, reverted: false, events: [], @@ -92,7 +92,7 @@ const invalidTxReceipt = { const failedTxReceipt = { blockNumber: 1, from: '0x999999cf1046e68e36e1aa2e0e07105eddd1f08f', - to: contract.Polygon.curationAddress, + to: contract.Optimism.curationAddress, txHash: failedTxhash, reverted: true, events: [], @@ -101,7 +101,7 @@ const validEvent = { curatorAddress: '0x999999cf1046e68e36e1aa2e0e07105eddd1f08f', creatorAddress: '0x999999cf1046e68e36e1aa2e0e07105eddd1f08e', uri: 'ipfs://someIpfsDataHash1', - tokenAddress: contract.Polygon.tokenAddress, + tokenAddress: contract.Optimism.tokenAddress, amount: '1000000', } const nativeTokenEvent = { @@ -114,7 +114,7 @@ const nativeTokenEvent = { const txReceipt = { blockNumber: 1, from: '0x999999cf1046e68e36e1aa2e0e07105eddd1f08f', - to: contract.Polygon.curationAddress, + to: contract.Optimism.curationAddress, txHash, reverted: false, events: [validEvent], @@ -322,7 +322,7 @@ describe('payToByBlockchainQueue.payTo', () => { describe('payToByBlockchainQueue._syncCurationEvents', () => { const latestBlockNum = BigInt(30000128) const safeBlockNum = - latestBlockNum - BigInt(BLOCKCHAIN_SAFE_CONFIRMS[BLOCKCHAIN.Polygon]) + latestBlockNum - BigInt(BLOCKCHAIN_SAFE_CONFIRMS[BLOCKCHAIN.Optimism]) const txTable = 'transaction' const blockchainTxTable = 'blockchain_transaction' const eventTable = 'blockchain_curation_event' @@ -354,12 +354,12 @@ describe('payToByBlockchainQueue._syncCurationEvents', () => { expect(await knex(syncRecordTable).count()).toEqual([{ count: '0' }]) // create record // @ts-ignore - await queue._handleSyncCurationEvents('Polygon') + await queue._handleSyncCurationEvents('Optimism') expect(await knex(syncRecordTable).count()).toEqual([{ count: '1' }]) const oldSavepoint = await knex(syncRecordTable).first() // update record // @ts-ignore - await queue._handleSyncCurationEvents('Polygon') + await queue._handleSyncCurationEvents('Optimism') expect(await knex(syncRecordTable).count()).toEqual([{ count: '1' }]) const newSavepoint = await knex(syncRecordTable).first() expect(new Date(newSavepoint.updatedAt).getTime()).toBeGreaterThan( @@ -367,7 +367,7 @@ describe('payToByBlockchainQueue._syncCurationEvents', () => { ) }) test('fetch logs', async () => { - const contractAddress = contract.Polygon.curationAddress + const contractAddress = contract.Optimism.curationAddress const curation = new CurationContract(chainId, contractAddress) const oldSavepoint1 = BigInt(20000000) @@ -405,18 +405,18 @@ describe('payToByBlockchainQueue._syncCurationEvents', () => { }) test('handle empty logs', async () => { // @ts-ignore - await queue._syncCurationEvents([], 'Polygon') + await queue._syncCurationEvents([], 'Optimism') }) test('handle native token curation logs', async () => { const nativeTokenLog = { txHash: txHash2, - address: contract.Polygon.curationAddress, + address: contract.Optimism.curationAddress, blockNumber: 1, removed: false, event: nativeTokenEvent, } // @ts-ignore - await queue._syncCurationEvents([nativeTokenLog], 'Polygon') + await queue._syncCurationEvents([nativeTokenLog], 'Optimism') expect( await knex(eventTable).where({ tokenAddress: null }).count() ).toEqual([{ count: '1' }]) @@ -427,13 +427,13 @@ describe('payToByBlockchainQueue._syncCurationEvents', () => { await knex(txTable).del() const removedLog = { txHash, - address: contract.Polygon.curationAddress, + address: contract.Optimism.curationAddress, blockNumber: 1, removed: true, event: validEvent, } // @ts-ignore - await queue._syncCurationEvents([removedLog], 'Polygon') + await queue._syncCurationEvents([removedLog], 'Optimism') expect(await knex(txTable).count()).toEqual([{ count: '0' }]) expect(await knex(blockchainTxTable).count()).toEqual([{ count: '0' }]) expect(await knex(eventTable).count()).toEqual([{ count: '0' }]) @@ -445,7 +445,7 @@ describe('payToByBlockchainQueue._syncCurationEvents', () => { const invalidLogs = [ { txHash: 'fakeTxhash2', - address: contract.Polygon.curationAddress, + address: contract.Optimism.curationAddress, blockNumber: 2, removed: false, event: { @@ -455,7 +455,7 @@ describe('payToByBlockchainQueue._syncCurationEvents', () => { }, { txHash: 'fakeTxhash3', - address: contract.Polygon.curationAddress, + address: contract.Optimism.curationAddress, blockNumber: 3, removed: false, event: { @@ -465,7 +465,7 @@ describe('payToByBlockchainQueue._syncCurationEvents', () => { }, { txHash: 'fakeTxhash4', - address: contract.Polygon.curationAddress, + address: contract.Optimism.curationAddress, blockNumber: 4, removed: false, event: { @@ -475,7 +475,7 @@ describe('payToByBlockchainQueue._syncCurationEvents', () => { }, ] // @ts-ignore - await queue._syncCurationEvents(invalidLogs, 'Polygon') + await queue._syncCurationEvents(invalidLogs, 'Optimism') expect(await knex(txTable).count()).toEqual([{ count: '0' }]) expect(await knex(blockchainTxTable).count()).toEqual([{ count: '3' }]) expect(await knex(eventTable).count()).toEqual([{ count: '3' }]) @@ -489,7 +489,7 @@ describe('payToByBlockchainQueue._syncCurationEvents', () => { const logs = [ { txHash, - address: contract.Polygon.curationAddress, + address: contract.Optimism.curationAddress, blockNumber: 1, removed: false, event: { @@ -498,7 +498,7 @@ describe('payToByBlockchainQueue._syncCurationEvents', () => { }, ] // @ts-ignore - await queue._syncCurationEvents(logs, 'Polygon') + await queue._syncCurationEvents(logs, 'Optimism') expect(await knex(txTable).count()).toEqual([{ count: '1' }]) const tx = await knex(txTable).first() const blockchainTx = await knex(blockchainTxTable).first() @@ -513,7 +513,7 @@ describe('payToByBlockchainQueue._syncCurationEvents', () => { await knex(txTable).update({ state: TRANSACTION_STATE.pending }) // @ts-ignore - await queue._syncCurationEvents(logs, 'Polygon') + await queue._syncCurationEvents(logs, 'Optimism') expect(await knex(txTable).count()).toEqual([{ count: '1' }]) const updatedTx = await knex(txTable).where('id', tx.id).first() const updatedBlockchainTx = await knex(blockchainTxTable) @@ -535,7 +535,7 @@ describe('payToByBlockchainQueue._syncCurationEvents', () => { }) // @ts-ignore - await queue._syncCurationEvents(logs, 'Polygon') + await queue._syncCurationEvents(logs, 'Optimism') expect(await knex(txTable).count()).toEqual([{ count: '1' }]) const updatedTx2 = await knex(txTable).where('id', tx.id).first() const updatedBlockchainTx2 = await knex(blockchainTxTable) @@ -587,7 +587,7 @@ describe('payToByBlockchainQueue._syncCurationEvents', () => { const logs = [ { txHash: txHash3, - address: contract.Polygon.curationAddress, + address: contract.Optimism.curationAddress, blockNumber: 1, removed: false, event: { @@ -596,7 +596,7 @@ describe('payToByBlockchainQueue._syncCurationEvents', () => { }, ] // @ts-ignore - await queue._syncCurationEvents(logs, 'Polygon') + await queue._syncCurationEvents(logs, 'Optimism') const updatedBlockchainTx = await paymentService.findOrCreateBlockchainTransaction({ @@ -621,7 +621,7 @@ describe('payToByBlockchainQueue._syncCurationEvents', () => { expect(mockNotify).not.toHaveBeenCalled() // @ts-ignore - await queue._syncCurationEvents(logs, 'Polygon') + await queue._syncCurationEvents(logs, 'Optimism') expect(mockNotify).not.toHaveBeenCalled() }) diff --git a/src/connectors/queue/__test__/publication.test.ts b/src/connectors/queue/__test__/publication.test.ts index 8c39fe4c8..b428839a9 100644 --- a/src/connectors/queue/__test__/publication.test.ts +++ b/src/connectors/queue/__test__/publication.test.ts @@ -2,53 +2,48 @@ import type { Connections } from 'definitions' import type { Knex } from 'knex' import Redis from 'ioredis' -import { RedisMemoryServer } from 'redis-memory-server' -import { v4 } from 'uuid' import { ARTICLE_STATE, PUBLISH_STATE } from 'common/enums' -import { DraftService, ArticleService, UserService } from 'connectors' +import { environment } from 'common/environment' +import { AtomService } from 'connectors' import { PublicationQueue } from 'connectors/queue' import { genConnections, closeConnections } from '../../__test__/utils' -const redisServer = new RedisMemoryServer() +// NOTE: because redis is not mocked here, this test may fail (expect "published" but received: "pending") before `flushall` resetting the queue describe('publicationQueue.publishArticle', () => { let connections: Connections let queue: PublicationQueue - let draftService: DraftService - let articleService: ArticleService - let userService: UserService + let atomService: AtomService let knex: Knex beforeAll(async () => { connections = await genConnections() knex = connections.knex - draftService = new DraftService(connections) - articleService = new ArticleService(connections) - userService = new UserService(connections) - const port = await redisServer.getPort() - const host = await redisServer.getHost() + atomService = new AtomService(connections) queue = new PublicationQueue(connections, { createClient: () => { return new Redis({ - port, - host, + host: environment.queueHost, + port: environment.queuePort, maxRetriesPerRequest: null, enableReadyCheck: false, }) }, }) - }, 30000) + }, 50000) afterAll(async () => { await closeConnections(connections) - redisServer.stop() }) test('publish not pending draft', async () => { const notPendingDraftId = '1' - const draft = await draftService.baseFindById(notPendingDraftId) - expect(draft.state).not.toBe(PUBLISH_STATE.pending) + const draft = await atomService.findUnique({ + table: 'draft', + where: { id: notPendingDraftId }, + }) + expect(draft.publishState).not.toBe(PUBLISH_STATE.pending) const job = await queue.publishArticle({ draftId: notPendingDraftId, @@ -58,28 +53,35 @@ describe('publicationQueue.publishArticle', () => { }) test('publish pending draft successfully', async () => { - const { draft, content, contentHTML } = await createPendingDraft( - draftService - ) + const { draft } = await createPendingDraft(atomService) const job = await queue.publishArticle({ draftId: draft.id, }) await job.finished() expect(await job.getState()).toBe('completed') - const updatedDraft = await draftService.baseFindById(draft.id) - const updatedArticle = await articleService.baseFindById( - updatedDraft.articleId - ) - console.log(updatedDraft) - expect(updatedDraft.content).toBe(contentHTML) - expect(updatedDraft.contentMd.includes(content)).toBeTruthy() + const updatedDraft = await atomService.findUnique({ + table: 'draft', + where: { id: draft.id }, + }) + const updatedArticle = await atomService.findUnique({ + table: 'article', + where: { id: updatedDraft.articleId as string }, + }) expect(updatedDraft.publishState).toBe(PUBLISH_STATE.published) expect(updatedArticle.state).toBe(ARTICLE_STATE.active) + + // article connections are handled + const connections = await atomService.findMany({ + table: 'article_connection', + where: { entranceId: updatedArticle.id }, + }) + expect(connections).toHaveLength(3) }) test('publish pending draft concurrently', async () => { - const { draft } = await createPendingDraft(draftService) + const countBefore = (await knex('article').count().first())!.count + const { draft } = await createPendingDraft(atomService) const job1 = await queue.publishArticle({ draftId: draft.id, }) @@ -87,53 +89,28 @@ describe('publicationQueue.publishArticle', () => { draftId: draft.id, }) await Promise.all([job1.finished(), job2.finished()]) - const articleCount = await knex('article') - .where('draft_id', draft.id) - .count() // only one article is created - expect(articleCount[0].count).toBe('1') - }) - - test.skip('publish pending draft unsuccessfully', async () => { - // mock - userService.baseFindById = async (_) => { - throw Error('mock error in queue test') - } - const { draft } = await createPendingDraft(draftService) - const job = await queue.publishArticle({ - draftId: draft.id, - }) - try { - await job.finished() - } catch { - // pass - } - expect(await job.getState()).toBe('failed') - - const updatedDraft = await draftService.baseFindById(draft.id) - const updatedArticle = await articleService.baseFindById( - updatedDraft.articleId - ) - - expect(updatedDraft.publishState).toBe(PUBLISH_STATE.error) - expect(updatedArticle.state).toBe(ARTICLE_STATE.error) + const count = (await knex('article').count().first())!.count + expect(+count - +countBefore).toBe(1) }) }) -const createPendingDraft = async (draftService: DraftService) => { +const createPendingDraft = async (atomService: AtomService) => { const content = Math.random().toString() const contentHTML = `

${content} abc

` + const connections = ['1', '2', '3'] return { - draft: await draftService.baseCreate({ - authorId: '1', - uuid: v4(), - title: 'test title', - summary: 'test summary', - content: contentHTML, - publishState: PUBLISH_STATE.pending, + draft: await atomService.create({ + table: 'draft', + data: { + authorId: '1', + title: 'test title', + summary: 'test summary', + content: contentHTML, + publishState: PUBLISH_STATE.pending, + collection: connections, + }, }), - content, - contentHTML, } } diff --git a/src/connectors/queue/appreciation.ts b/src/connectors/queue/appreciation.ts index 4646cf9b2..04492094e 100644 --- a/src/connectors/queue/appreciation.ts +++ b/src/connectors/queue/appreciation.ts @@ -16,7 +16,6 @@ import { environment } from 'common/environment' import { ActionLimitExceededError, ArticleNotFoundError, - ForbiddenError, UserNotFoundError, } from 'common/errors' import { getLogger } from 'common/logger' @@ -42,7 +41,7 @@ interface AppreciationParams { export class AppreciationQueue extends BaseQueue { public constructor(connections: Connections) { - // make it a bit slower on handling jobs in order to reduce courrent operations + // make it a bit slower on handling jobs in order to reduce concurrent operations super(QUEUE_NAME.appreciation, connections, { limiter: { max: 1, duration: 500 }, }) @@ -105,9 +104,6 @@ export class AppreciationQueue extends BaseQueue { if (!article) { throw new ArticleNotFoundError('article does not exist') } - if (article.authorId === senderId) { - throw new ForbiddenError('cannot appreciate your own article') - } // check appreciate left const appreciateLeft = await articleService.appreciateLeftByUser({ @@ -147,7 +143,7 @@ export class AppreciationQueue extends BaseQueue { likerIp: senderIP, userAgent, authorLikerId: author.likerId, - url: `https://${environment.siteDomain}/@${author.userName}/${article.slug}-${article.mediaHash}`, + url: `https://${environment.siteDomain}/a/${article.shortHash}`, amount: validAmount, }) } @@ -172,6 +168,7 @@ export class AppreciationQueue extends BaseQueue { job.progress(100) done(null, job.data) + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { logger.error(err) done(err) diff --git a/src/connectors/queue/asset.ts b/src/connectors/queue/asset.ts index a7443f8ec..21dcbd2eb 100644 --- a/src/connectors/queue/asset.ts +++ b/src/connectors/queue/asset.ts @@ -4,7 +4,7 @@ import Queue from 'bull' import { QUEUE_JOB, QUEUE_NAME, QUEUE_PRIORITY } from 'common/enums' import { getLogger } from 'common/logger' -import { AtomService } from 'connectors' +import { AtomService, aws, cfsvc } from 'connectors' import { BaseQueue } from './baseQueue' @@ -15,7 +15,7 @@ interface AssetParams { } export class AssetQueue extends BaseQueue { - constructor(connections: Connections) { + public constructor(connections: Connections) { super(QUEUE_NAME.asset, connections) this.addConsumers() } @@ -58,23 +58,24 @@ export class AssetQueue extends BaseQueue { }) // delete db records - await atomService.knex.transaction(async (trx) => { + await this.connections.knex.transaction(async (trx) => { await trx('asset_map').whereIn('asset_id', ids).del() await trx('asset').whereIn('id', ids).del() }) - // delete s3 object + // delete files in S3/Cloudflare Images await Promise.all( assets .map((asset) => [ - atomService.aws.baseDeleteFile(asset.path), - atomService.cfsvc.baseDeleteFile(asset.path), + aws.baseDeleteFile(asset.path), + cfsvc.baseDeleteFile(asset.path), ]) .flat() ) job.progress(100) done(null, job.data) + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { logger.error(err) done(err) diff --git a/src/connectors/queue/index.ts b/src/connectors/queue/index.ts index 184066570..31a2eac4d 100644 --- a/src/connectors/queue/index.ts +++ b/src/connectors/queue/index.ts @@ -6,4 +6,3 @@ export * from './payTo' export * from './appreciation' export * from './revision' export * from './asset' -export * from './ipfs' diff --git a/src/connectors/queue/ipfs.ts b/src/connectors/queue/ipfs.ts deleted file mode 100644 index a2cedeca7..000000000 --- a/src/connectors/queue/ipfs.ts +++ /dev/null @@ -1,160 +0,0 @@ -import type { Connections } from 'definitions' - -import Queue from 'bull' -import _ from 'lodash' - -import { - // HOUR, - MINUTE, - PIN_STATE, - QUEUE_JOB, - QUEUE_NAME, - QUEUE_PRIORITY, - SLACK_MESSAGE_STATE, -} from 'common/enums' -import { getLogger } from 'common/logger' -import { timeout } from 'common/utils' -import { ipfsServers, AtomService } from 'connectors' -import SlackService from 'connectors/slack' - -import { BaseQueue } from './baseQueue' - -const logger = getLogger('queue-ipfs') - -export class IPFSQueue extends BaseQueue { - slackService: InstanceType - ipfs: typeof ipfsServers - - constructor(connections: Connections) { - super(QUEUE_NAME.ipfs, connections) - - this.ipfs = ipfsServers - this.slackService = new SlackService() - - this.addConsumers() - } - - /** - * Producers - */ - addRepeatJobs = async () => { - // verify pinning hashes every 30 minutes - this.q.add( - QUEUE_JOB.verifyIPFSPinHashes, - {}, - { - priority: QUEUE_PRIORITY.LOW, - repeat: { every: MINUTE * 30 }, - } - ) - } - - /** - * Consumers - */ - private addConsumers = () => { - this.q.process(QUEUE_JOB.verifyIPFSPinHashes, this.verifyIPFSPinHashes) - } - - private verifyIPFSPinHashes: Queue.ProcessCallbackFunction = async ( - job, - done - ) => { - const atomService = new AtomService(this.connections) - - try { - logger.info('[schedule job] verify IPFS pinning hashes') - - // obtain first 500 pinning drafts - const pinningDrafts = await atomService.findMany({ - table: 'draft', - where: { pinState: PIN_STATE.pinning }, - take: 500, - orderBy: [{ column: 'id', order: 'desc' }], - }) - - job.progress(30) - - const succeedIds: string[] = [] - const failedIds: string[] = [] - const chunks = _.chunk(pinningDrafts, 10) - - const verifyHash = async (draft: any) => { - // ping hash - await this.ipfs.client.get(draft.dataHash) - - // mark as pin state as `pinned` - await this.markDraftPinStateAs( - { - draftId: draft.id, - pinState: PIN_STATE.pinned, - }, - atomService - ) - - succeedIds.push(draft.id) - logger.info( - `[schedule job] draft ${draft.id} (${draft.dataHash}) was pinned.` - ) - } - - for (const drafts of chunks) { - await Promise.all( - drafts.map(async (draft) => { - try { - await timeout(5000, verifyHash(draft)) - } catch (error) { - // mark as pin state as `failed` - await this.markDraftPinStateAs( - { - draftId: draft.id, - pinState: PIN_STATE.failed, - }, - atomService - ) - - failedIds.push(draft.id) - logger.error(error) - } - }) - ) - } - - job.progress(100) - if (pinningDrafts.length >= 1) { - this.slackService.sendQueueMessage({ - data: { succeedIds, failedIds }, - title: `${QUEUE_NAME.ipfs}:verifyIPFSPinHashes`, - message: `Completed handling ${pinningDrafts.length} hashes.`, - state: SLACK_MESSAGE_STATE.successful, - }) - } - done(null, { succeedIds, failedIds }) - } catch (err: any) { - logger.error(err) - this.slackService.sendQueueMessage({ - title: `${QUEUE_NAME.ipfs}:verifyIPFSPinHashes`, - message: `Failed to process cron job`, - state: SLACK_MESSAGE_STATE.failed, - }) - done(err) - } - } - - private markDraftPinStateAs = async ( - { - draftId, - pinState, - }: { - draftId: string - pinState: PIN_STATE - }, - atomService: AtomService - ) => { - await atomService.update({ - table: 'draft', - where: { id: draftId }, - data: { pinState }, - }) - } -} diff --git a/src/connectors/queue/migration.ts b/src/connectors/queue/migration.ts index 19537f822..a1e736190 100644 --- a/src/connectors/queue/migration.ts +++ b/src/connectors/queue/migration.ts @@ -5,7 +5,6 @@ import { normalizeArticleHTML, sanitizeHTML, } from '@matters/matters-editor/transformers' -import { v4 } from 'uuid' import { ASSET_TYPE, @@ -122,11 +121,13 @@ export class MigrationQueue extends BaseQueue { // put draft const draft = await draftService.baseCreate({ authorId: userId, - uuid: v4(), title, summary: content && makeSummary(content), content: - content && normalizeArticleHTML(sanitizeHTML(content)), + content && + normalizeArticleHTML( + sanitizeHTML(content, { maxEmptyParagraphs: -1 }) + ), }) // add asset and assetmap @@ -164,6 +165,7 @@ export class MigrationQueue extends BaseQueue { job.progress(100) done(null, 'Migration has finished.') + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { logger.error(err) done(err) diff --git a/src/connectors/queue/payTo/blockchain.ts b/src/connectors/queue/payTo/blockchain.ts index 4e782a728..463fb4019 100644 --- a/src/connectors/queue/payTo/blockchain.ts +++ b/src/connectors/queue/payTo/blockchain.ts @@ -1,4 +1,9 @@ -import type { EmailableUser, Connections, GQLChain } from 'definitions' +import type { + EmailableUser, + Connections, + BlockchainTransaction, + GQLChain, +} from 'definitions' import { invalidateFQC } from '@matters/apollo-response-cache' import Queue from 'bull' @@ -150,7 +155,7 @@ export class PayToByBlockchainQueue extends BaseQueue { data: { from: txReceipt.from, to: txReceipt.to, - blockNumber: txReceipt.blockNumber, + blockNumber: txReceipt.blockNumber.toString(), }, }) } else { @@ -171,17 +176,21 @@ export class PayToByBlockchainQueue extends BaseQueue { const [recipient, sender, article] = await Promise.all([ userService.baseFindById(tx.recipientId), - userService.baseFindById(tx.senderId), + userService.baseFindById(tx.senderId as string), atomService.findFirst({ table: 'article', where: { id: tx.targetId }, }), ]) + const articleService = new ArticleService(this.connections) + const articleVersion = await articleService.loadLatestArticleVersion( + article.id + ) // cancel tx and success blockchain tx if it's invalid // Note: sender and recipient's ETH address may change after tx is created const isValidTx = await this.containMatchedEvent(txReceipt.events, { - cid: article.dataHash, + cid: articleVersion.dataHash, amount: tx.amount, // support USDT only for now tokenAddress: contract[chain].tokenAddress, @@ -201,7 +210,7 @@ export class PayToByBlockchainQueue extends BaseQueue { // anonymize tx if sender's ETH address is not matched const isSenderMatched = txReceipt.events .map((e) => e.curatorAddress) - .every((address) => ignoreCaseMatch(address, sender.ethAddress || '')) + .every((address) => ignoreCaseMatch(address, sender?.ethAddress || '')) if (!isSenderMatched) { await atomService.update({ table: 'transaction', @@ -216,8 +225,8 @@ export class PayToByBlockchainQueue extends BaseQueue { // notify recipient and sender (if needed) await paymentService.notifyDonation({ tx, - sender: isSenderMatched ? sender : undefined, - recipient, + sender: isSenderMatched ? (sender as EmailableUser) : undefined, + recipient: recipient as EmailableUser, article, }) @@ -238,6 +247,12 @@ export class PayToByBlockchainQueue extends BaseQueue { let syncedBlockNum: { [key: string]: number } = {} ;[BLOCKCHAIN.Polygon, BLOCKCHAIN.Optimism].forEach(async (chain) => { + // FIXME: pause support for the Polygon testnet + // @see {src/common/enums/payment.ts:L59} + if (chain === BLOCKCHAIN.Polygon && !isProd) { + return + } + try { const blockNum = await this._handleSyncCurationEvents(chain) syncedBlockNum = { ...syncedBlockNum, [chain]: blockNum } @@ -284,13 +299,12 @@ export class PayToByBlockchainQueue extends BaseQueue { update: { chainId, contractAddress, - blockNumber: newSavepoint, - updatedAt: this.connections.knex.fn.now(), + blockNumber: newSavepoint.toString(), }, create: { chainId, contractAddress, - blockNumber: newSavepoint, + blockNumber: newSavepoint.toString(), }, }) @@ -302,7 +316,7 @@ export class PayToByBlockchainQueue extends BaseQueue { chain: GQLChain, blockchainTx: { id: string - transactionId: string + transactionId: string | null state: BLOCKCHAIN_TRANSACTION_STATE }, services: { @@ -334,15 +348,19 @@ export class PayToByBlockchainQueue extends BaseQueue { return } - // skip if recipeint or article is not found + // skip if recipient or article is not found const creatorUser = await userService.findByEthAddress(event.creatorAddress) if (!creatorUser) { return } const cid = extractCid(event.uri) + const articleVersion = await atomService.findFirst({ + table: 'article_version', + where: { dataHash: cid }, + }) const article = await atomService.findFirst({ table: 'article', - where: { authorId: creatorUser.id, dataHash: cid }, + where: { id: articleVersion?.articleId, authorId: creatorUser.id }, }) if (!article) { return @@ -392,7 +410,7 @@ export class PayToByBlockchainQueue extends BaseQueue { table: 'transaction', where: { id: tx.id }, data: { - amount, + amount: amount.toString(), targetId: article.id, currency: PAYMENT_CURRENCY.USDT, provider: PAYMENT_PROVIDER.blockchain, @@ -421,7 +439,7 @@ export class PayToByBlockchainQueue extends BaseQueue { }, trx ) - await paymentService.baseUpdate( + await paymentService.baseUpdate( blockchainTx.id, { transactionId: tx.id }, 'blockchain_transaction', @@ -607,7 +625,7 @@ export class PayToByBlockchainQueue extends BaseQueue { targetId: string, userService: UserService ) => { - // manaully invalidate cache + // manually invalidate cache if (targetType) { const entity = await userService.baseFindEntityTypeTable(targetType) const entityType = diff --git a/src/connectors/queue/payTo/matters.ts b/src/connectors/queue/payTo/matters.ts index dacaf5327..5d9a8485a 100644 --- a/src/connectors/queue/payTo/matters.ts +++ b/src/connectors/queue/payTo/matters.ts @@ -1,4 +1,4 @@ -import type { Connections } from 'definitions' +import type { Connections, EmailableUser } from 'definitions' import { invalidateFQC } from '@matters/apollo-response-cache' import Queue from 'bull' @@ -25,7 +25,7 @@ interface PaymentParams { } export class PayToByMattersQueue extends BaseQueue { - constructor(connections: Connections) { + public constructor(connections: Connections) { super(QUEUE_NAME.payTo, connections) this.addConsumers() } @@ -34,7 +34,7 @@ export class PayToByMattersQueue extends BaseQueue { * Producer for payTo. * */ - payTo = ({ txId }: PaymentParams) => + public payTo = ({ txId }: PaymentParams) => this.q.add( QUEUE_JOB.payTo, { txId }, @@ -122,8 +122,8 @@ export class PayToByMattersQueue extends BaseQueue { // 4. recipient or sender not existed if ( balance < 0 || - tx.amount > PAYMENT_MAXIMUM_PAYTO_AMOUNT.HKD || - tx.amount + hasPaid > PAYMENT_MAXIMUM_PAYTO_AMOUNT.HKD || + parseFloat(tx.amount) > PAYMENT_MAXIMUM_PAYTO_AMOUNT.HKD || + parseFloat(tx.amount) + hasPaid > PAYMENT_MAXIMUM_PAYTO_AMOUNT.HKD || !recipient || !sender ) { @@ -144,12 +144,12 @@ export class PayToByMattersQueue extends BaseQueue { // notification await paymentService.notifyDonation({ tx, - sender, - recipient, + sender: sender as EmailableUser, + recipient: recipient as EmailableUser, article, }) - // manaully invalidate cache + // manually invalidate cache if (tx.targetType) { const entity = await userService.baseFindEntityTypeTable(tx.targetType) const entityType = diff --git a/src/connectors/queue/payout.ts b/src/connectors/queue/payout.ts index d96758c7a..821c11d5d 100644 --- a/src/connectors/queue/payout.ts +++ b/src/connectors/queue/payout.ts @@ -17,7 +17,6 @@ import { AtomService, ExchangeRate, PaymentService, - UserService, NotificationService, } from 'connectors' import SlackService from 'connectors/slack' @@ -90,7 +89,6 @@ export class PayoutQueue extends BaseQueue { const slack = new SlackService() const atomService = new AtomService(this.connections) const paymentService = new PaymentService(this.connections) - const userService = new UserService(this.connections) const notificationService = new NotificationService(this.connections) const data = job.data as PaymentParams @@ -149,6 +147,7 @@ export class PayoutQueue extends BaseQueue { const exchangeRate = new ExchangeRate(this.connections.redis) try { HKDtoUSD = (await exchangeRate.getRate('HKD', 'USD')).rate + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { slack.sendStripeAlert({ data, @@ -188,12 +187,12 @@ export class PayoutQueue extends BaseQueue { // update tx await paymentService.baseUpdate(tx.id, { state: TRANSACTION_STATE.succeeded, - provider_tx_id: transfer.id, + providerTxId: transfer.id, updatedAt: new Date(), }) // notifications - const user = await userService.loadById(tx.senderId) + const user = await atomService.userIdLoader.load(tx.senderId) if (user.email && user.userName && user.displayName) { notificationService.mail.sendPayment({ @@ -204,7 +203,6 @@ export class PayoutQueue extends BaseQueue { }, type: 'payout', tx: { - recipient, amount: net, currency: tx.currency, }, diff --git a/src/connectors/queue/publication.ts b/src/connectors/queue/publication.ts index a07e5b021..9b6f12f9c 100644 --- a/src/connectors/queue/publication.ts +++ b/src/connectors/queue/publication.ts @@ -1,18 +1,20 @@ import type { CustomQueueOpts } from './utils' -import type { Connections } from 'definitions' +import type { BasicAcceptedElems } from 'cheerio' +import type { + Connections, + UserOAuthLikeCoin, + Article, + ArticleVersion, + ArticleConnection, +} from 'definitions' import { invalidateFQC } from '@matters/apollo-response-cache' -import { makeSummary } from '@matters/ipns-site-generator' -import { html2md } from '@matters/matters-editor/transformers' -import slugify from '@matters/slugify' import Queue from 'bull' import * as cheerio from 'cheerio' import { - ARTICLE_STATE, DB_NOTICE_TYPE, NODE_TYPES, - PIN_STATE, PUBLISH_STATE, QUEUE_CONCURRENCY, QUEUE_JOB, @@ -22,13 +24,7 @@ import { } from 'common/enums' import { environment } from 'common/environment' import { getLogger } from 'common/logger' -import { - countWords, - extractAssetDataFromHtml, - fromGlobalId, - normalizeTagInput, - // stripAllPunct, -} from 'common/utils' +import { fromGlobalId, normalizeTagInput } from 'common/utils' import { TagService, DraftService, @@ -37,6 +33,7 @@ import { SystemService, NotificationService, AtomService, + aws, } from 'connectors' import { BaseQueue } from './baseQueue' @@ -44,12 +41,12 @@ import { BaseQueue } from './baseQueue' const logger = getLogger('queue-publication') export class PublicationQueue extends BaseQueue { - constructor(connections: Connections, customOpts?: CustomQueueOpts) { + public constructor(connections: Connections, customOpts?: CustomQueueOpts) { super(QUEUE_NAME.publication, connections, customOpts) this.addConsumers() } - publishArticle = ({ + public publishArticle = ({ draftId, iscnPublish, }: { @@ -65,7 +62,7 @@ export class PublicationQueue extends BaseQueue { } ) - refreshIPNSFeed = ({ + public refreshIPNSFeed = ({ userName, numArticles = 50, forceReplace, @@ -74,11 +71,11 @@ export class PublicationQueue extends BaseQueue { numArticles?: number forceReplace?: boolean }) => - this.q.add( - QUEUE_JOB.refreshIPNSFeed, - { userName, numArticles, forceReplace } - // { priority: QUEUE_PRIORITY.CRITICAL, } - ) + this.q.add(QUEUE_JOB.refreshIPNSFeed, { + userName, + numArticles, + forceReplace, + }) /** * Cusumers @@ -116,8 +113,10 @@ export class PublicationQueue extends BaseQueue { draftId: string iscnPublish?: boolean } - let draft = await draftService.baseFindById(draftId) - let article + const draft = await atomService.findUnique({ + table: 'draft', + where: { id: draftId }, + }) // Step 1: checks if (!draft || draft.publishState !== PUBLISH_STATE.pending) { @@ -127,322 +126,248 @@ export class PublicationQueue extends BaseQueue { } await job.progress(5) - try { - const summary = draft.summary || makeSummary(draft.content) - const wordCount = countWords(draft.content) - - // Step 2: create an article - const articleData = { - ...draft, - draftId: draft.id, - // dataHash, - // mediaHash, - summary, - wordCount, - slug: slugify(draft.title), - } - article = await (draft.articleId - ? articleService.baseUpdate(draft.articleId, articleData) - : articleService.createArticle(articleData)) - - await job.progress(20) - - // Step 3: update draft and article state - let contentMd = '' - try { - contentMd = html2md(draft.content) - } catch (e) { - logger.warn('draft %s failed to convert HTML to Markdown', draft.id) - } - const [publishedDraft, _] = await Promise.all([ - draftService.baseUpdate(draft.id, { - articleId: article.id, - summary, - wordCount, - contentMd, - // dataHash, - // mediaHash, - archived: true, - // iscnId, - publishState: PUBLISH_STATE.published, - pinState: PIN_STATE.pinned, - }), - // this.articleService.baseUpdate(article.id, { iscnId }), - articleService.baseUpdate(article.id, { - state: ARTICLE_STATE.active, - }), - ]) - - await job.progress(30) - - const author = await userService.baseFindById(draft.authorId) - const { userName, displayName } = author - let tags = draft.tags as string[] - - // Note: the following steps won't affect the publication. - // Section1: update local DB related - try { - // Step 4: handle collection, circles, tags & mentions - await this.handleCollection({ draft, article }) - await job.progress(40) - - await this.handleCircle({ - draft, - article, - // secret: key // TO update secret in 'article_circle' later after IPFS published - }) - await job.progress(45) - - tags = await this.handleTags({ draft, article }) - await job.progress(50) - - await this.handleMentions({ draft, article }) - await job.progress(60) - - /** - * Step 5: Handle Assets - * - * Relationship between asset_map and entity: - * - * cover -> article - * embed -> draft - * - * @see {@url https://github.com/thematters/matters-server/pull/1510} - */ - const [{ id: draftEntityTypeId }, { id: articleEntityTypeId }] = - await Promise.all([ - systemService.baseFindEntityTypeId('draft'), - systemService.baseFindEntityTypeId('article'), - ]) - - // Remove unused assets - await this.deleteUnusedAssets({ draftEntityTypeId, draft }) - await job.progress(70) - - // Swap cover assets from draft to article - const coverAssets = await systemService.findAssetAndAssetMap({ - entityTypeId: draftEntityTypeId, - entityId: draft.id, - assetType: 'cover', - }) - await systemService.swapAssetMapEntity( - coverAssets.map((ast: any) => ast.id), - articleEntityTypeId, - article.id - ) - await job.progress(75) - } catch (err) { - // ignore errors caused by these steps - logger.warn('optional step failed: %j', { err, job, draft }) - } + // Step 2: create an article + const [article, articleVersion] = await articleService.createArticle(draft) - // Step 7: trigger notifications - notificationService.trigger({ - event: DB_NOTICE_TYPE.article_published, - recipientId: article.authorId, - entities: [{ type: 'target', entityTable: 'article', entity: article }], + await job.progress(20) + + await draftService.baseUpdate(draft.id, { + publishState: PUBLISH_STATE.published, + articleId: article.id, + }) + + await job.progress(30) + + let tags: string[] = [] + // Note: the following steps won't affect the publication. + // Section1: update local DB related + try { + // Step 4: handle collection, circles, tags & mentions + await this.handleConnections(article, articleVersion) + await job.progress(40) + + await this.handleCircle({ + article, + articleVersion, + // secret: key // TO update secret in 'article_circle' later after IPFS published }) + await job.progress(45) + + tags = await this.handleTags({ article, articleVersion }) + await job.progress(50) + + await this.handleMentions({ article, content: draft.content }) + await job.progress(60) + + /** + * Step 5: Handle Assets + * + * Relationship between asset_map and entity: + * + * cover -> article + * embed -> draft + * + * @see {@url https://github.com/thematters/matters-server/pull/1510} + */ + const [{ id: draftEntityTypeId }, { id: articleEntityTypeId }] = + await Promise.all([ + systemService.baseFindEntityTypeId('draft'), + systemService.baseFindEntityTypeId('article'), + ]) + + // Remove unused assets + // await this.deleteUnusedAssets({ draftEntityTypeId, draft }) + await job.progress(70) - // Step 8: invalidate user cache - invalidateFQC({ - node: { type: NODE_TYPES.User, id: article.authorId }, - redis: this.connections.redis, + // Swap cover assets from draft to article + const coverAssets = await systemService.findAssetAndAssetMap({ + entityTypeId: draftEntityTypeId, + entityId: draft.id, + assetType: 'cover', }) + await systemService.swapAssetMapEntity( + coverAssets.map((ast: { id: string }) => ast.id), + articleEntityTypeId, + article.id + ) + await job.progress(75) + } catch (err) { + // ignore errors caused by these steps + logger.warn('optional step failed: %j', { err, job, draft }) + } - // Section2: publish to external services like: IPFS / IPNS / ISCN / etc... - let ipnsRes: any - try { - // publish content to IPFS - const { - contentHash: dataHash, - mediaHash, - key, - } = (await articleService.publishToIPFS(draft))! - await job.progress(80) - ;[article, draft] = await Promise.all([ - articleService.baseUpdate(article.id, { - dataHash, - mediaHash, - }), - draftService.baseUpdate(draft.id, { - dataHash, - mediaHash, - }), - ]) + // Step 7: trigger notifications + notificationService.trigger({ + event: DB_NOTICE_TYPE.article_published, + recipientId: article.authorId, + entities: [{ type: 'target', entityTable: 'article', entity: article }], + }) - if (key && draft.access) { - const data = { - articleId: article.id, - circleId: draft.circleId, - // secret: key, - } - - await atomService.update({ - table: 'article_circle', - where: data, - data: { - ...data, - secret: key, - access: draft.access, - updatedAt: this.connections.knex.fn.now(), - }, - }) - } + // Step 8: invalidate user cache + invalidateFQC({ + node: { type: NODE_TYPES.User, id: article.authorId }, + redis: this.connections.redis, + }) - // Step: iscn publishing - // handling both cases of set to true or false, but not omit (undefined) - if (iscnPublish || draft.iscnPublish != null) { - const liker = (await userService.findLiker({ - userId: author.id, - }))! // as NonNullable - const cosmosWallet = await userService.likecoin.getCosmosWallet({ - liker, - }) - - const iscnId = await userService.likecoin.iscnPublish({ - mediaHash: `hash://sha256/${mediaHash}`, - ipfsHash: `ipfs://${dataHash}`, - cosmosWallet, - userName: `${displayName} (@${userName})`, - title: draft.title, - description: summary, - datePublished: article.createdAt?.toISOString().substring(0, 10), - url: `https://${environment.siteDomain}/@${userName}/${article.id}-${article.slug}-${article.mediaHash}`, - tags, - liker, - }) - - ;[article, draft] = await Promise.all([ - articleService.baseUpdate(article.id, { - iscnId, - }), - draftService.baseUpdate(draft.id, { - iscnId, - iscnPublish: iscnPublish || draft.iscnPublish, - }), - ]) + // Section2: publish to external services like: IPFS / IPNS / ISCN / etc... + const author = await atomService.userIdLoader.load(article.authorId) + let dataHash + let mediaHash + try { + // publish content to IPFS + const { + contentHash, + mediaHash: _mediaHash, + key, + } = await articleService.publishToIPFS( + article, + articleVersion, + draft.content + ) + dataHash = contentHash + mediaHash = _mediaHash + + await job.progress(80) + await atomService.update({ + table: 'article_version', + data: { + dataHash, + mediaHash, + }, + where: { id: articleVersion.id }, + }) + + if (key && articleVersion.circleId) { + const data = { + articleId: article.id, + circleId: articleVersion.circleId, + // secret: key, } - await job.progress(90) - ipnsRes = await articleService.publishFeedToIPNS({ - userName, - // incremental: true, // attach the last just published article - updatedDrafts: [draft], + await atomService.update({ + table: 'article_circle', + where: data, + data: { + ...data, + secret: key, + access: articleVersion.access, + }, }) - - await job.progress(95) - } catch (err) { - // ignore errors caused by these steps - logger.warn( - 'job IPFS optional step failed (will retry async later in listener):', - err, - job, - draft - ) } - // invalidate article cache - invalidateFQC({ - node: { type: NODE_TYPES.Article, id: article.id }, - redis: this.connections.redis, - }) + // Step: iscn publishing + // handling both cases of set to true or false, but not omit (undefined) + if (iscnPublish || draft.iscnPublish != null) { + const liker = (await userService.findLiker({ + userId: article.authorId, + })) as UserOAuthLikeCoin + const cosmosWallet = await userService.likecoin.getCosmosWallet({ + liker, + }) - await job.progress(100) + const { displayName, userName } = author + const iscnId = await userService.likecoin.iscnPublish({ + mediaHash: `hash://sha256/${mediaHash}`, + ipfsHash: `ipfs://${dataHash}`, + cosmosWallet, + userName: `${displayName} (@${userName})`, + title: articleVersion.title, + description: articleVersion.summary, + datePublished: article.createdAt?.toISOString().substring(0, 10), + url: `https://${environment.siteDomain}/a/${article.shortHash}`, + tags, + liker, + }) - // no await to notify async - articleService - .sendArticleFeedMsgToSQS({ article, author, ipnsData: ipnsRes }) - .catch((err: Error) => logger.error('failed sqs notify:', err)) - - // no await to notify async - atomService.aws - .snsPublishMessage({ - // MessageGroupId: `ipfs-articles-${environment.env}:articles-feed`, - MessageBody: { - articleId: article.id, - title: article.title, - url: `https://${environment.siteDomain}/@${userName}/${article.id}-${article.slug}`, - dataHash: article.dataHash, - mediaHash: article.mediaHash, - - // ipns info: - ipnsKey: ipnsRes?.ipnsKey, - lastDataHash: ipnsRes?.lastDataHash, - - // author info: - userName, - displayName, - }, + await atomService.update({ + table: 'article_version', + where: { id: article.id }, + data: { iscnId }, }) - // .then(res => {}) - .catch((err: Error) => logger.error('failed sns notify:', err)) + } + await job.progress(90) - // no await to put data async - atomService.aws.putMetricData({ - MetricData: [ - { - MetricName: METRICS_NAMES.ArticlePublishCount, - // Counts: [1], - Timestamp: new Date(), - Unit: 'Count', - Value: 1, - }, - ], - }) + if (author.userName) { + await articleService.publishFeedToIPNS({ userName: author.userName }) + } - done(null, { - articleId: article.id, - draftId: publishedDraft.id, - dataHash: publishedDraft.dataHash, - mediaHash: publishedDraft.mediaHash, - iscnPublish: iscnPublish || draft.iscnPublish, - iscnId: article.iscnId, - }) - } catch (err: any) { - await Promise.all([ - articleService.baseUpdate(article.id, { - state: ARTICLE_STATE.error, - }), - draftService.baseUpdate(draft.id, { - publishState: PUBLISH_STATE.error, - }), - ]) - done(err) + await job.progress(95) + } catch (err) { + // ignore errors caused by these steps + logger.warn( + 'job IPFS optional step failed (will retry async later in listener):', + err, + job, + draft + ) } + // invalidate article cache + invalidateFQC({ + node: { type: NODE_TYPES.Article, id: article.id }, + redis: this.connections.redis, + }) + + await job.progress(100) + + // no await to put data async + aws.putMetricData({ + MetricData: [ + { + MetricName: METRICS_NAMES.ArticlePublishCount, + // Counts: [1], + Timestamp: new Date(), + Unit: 'Count', + Value: 1, + }, + ], + }) + + done(null, { + articleId: article.id, + draftId: draft.id, + dataHash: dataHash, + mediaHash: mediaHash, + iscnPublish: iscnPublish || draft.iscnPublish, + iscnId: articleVersion.iscnId, + }) } - private handleCollection = async ({ - draft, - article, - }: { - draft: any - article: any - }) => { - if (!draft.collection || draft.collection.length <= 0) { + private handleConnections = async ( + article: Article, + articleVersion: ArticleVersion + ) => { + if (articleVersion.connections.length <= 0) { return } const articleService = new ArticleService(this.connections) const notificationService = new NotificationService(this.connections) - const items = draft.collection.map((articleId: string, index: number) => ({ - entranceId: article.id, - articleId, - order: index, - // createdAt: new Date(), // default to CURRENT_TIMESTAMP - // updatedAt: new Date(), // default to CURRENT_TIMESTAMP - })) - await articleService.baseBatchCreate(items, 'article_connection') + const items = articleVersion.connections.map( + (articleId: string, index: number) => ({ + entranceId: article.id, + articleId, + order: index, + }) + ) + await articleService.baseBatchCreate( + items, + 'article_connection' + ) // trigger notifications - draft.collection.forEach(async (id: string) => { - const collection = await articleService.baseFindById(id) + articleVersion.connections.forEach(async (id: string) => { + const connection = await articleService.baseFindById(id) + if (!connection) { + logger.warn(`article connection not found: ${id}`) + return + } notificationService.trigger({ event: DB_NOTICE_TYPE.article_new_collected, - recipientId: collection.authorId, + recipientId: connection.authorId, actorId: article.authorId, entities: [ - { type: 'target', entityTable: 'article', entity: collection }, + { type: 'target', entityTable: 'article', entity: connection }, { type: 'collection', entityTable: 'article', @@ -454,15 +379,15 @@ export class PublicationQueue extends BaseQueue { } private handleCircle = async ({ - draft, article, + articleVersion, secret, }: { - draft: any - article: any - secret?: any + article: Article + articleVersion: ArticleVersion + secret?: string }) => { - if (!draft.circleId) { + if (!articleVersion.circleId) { return } @@ -470,29 +395,30 @@ export class PublicationQueue extends BaseQueue { const atomService = new AtomService(this.connections) const notificationService = new NotificationService(this.connections) - if (draft.access) { + if (articleVersion.access) { const data = { - articleId: article.id, - circleId: draft.circleId, + articleId: articleVersion.articleId, + circleId: articleVersion.circleId, ...(secret ? { secret } : {}), } await atomService.upsert({ table: 'article_circle', where: data, - create: { ...data, access: draft.access }, + create: { ...data, access: articleVersion.access }, update: { ...data, - access: draft.access, - updatedAt: this.connections.knex.fn.now(), + access: articleVersion.access, }, }) } // handle 'circle_new_article' notification - const recipients = await userService.findCircleRecipients(draft.circleId) + const recipients = await userService.findCircleRecipients( + articleVersion.circleId + ) - recipients.forEach((recipientId: any) => { + recipients.forEach((recipientId: string) => { notificationService.trigger({ event: DB_NOTICE_TYPE.circle_new_article, recipientId, @@ -501,20 +427,20 @@ export class PublicationQueue extends BaseQueue { }) await invalidateFQC({ - node: { type: NODE_TYPES.Circle, id: draft.circleId }, + node: { type: NODE_TYPES.Circle, id: articleVersion.circleId }, redis: this.connections.redis, }) } private handleTags = async ({ - draft, article, + articleVersion, }: { - draft: any - article: any + article: Article + articleVersion: ArticleVersion }) => { const tagService = new TagService(this.connections) - let tags = draft.tags as string[] + let tags = articleVersion.tags as string[] if (tags && tags.length > 0) { // get tag editor @@ -522,8 +448,6 @@ export class PublicationQueue extends BaseQueue { ? [environment.mattyId, article.authorId] : [article.authorId] - // tags = Array.from(new Set(tags.map(stripAllPunct).filter(Boolean))) - // create tag records, return tag record if already exists const dbTags = ( (await Promise.all( @@ -537,9 +461,7 @@ export class PublicationQueue extends BaseQueue { }, { columns: ['id', 'content'], - skipCreate: - // !content // || content.length > MAX_TAG_CONTENT_LENGTH, - normalizeTagInput(content) !== content, + skipCreate: normalizeTagInput(content) !== content, } ) ) @@ -560,15 +482,15 @@ export class PublicationQueue extends BaseQueue { } private handleMentions = async ({ - draft, article, + content, }: { - draft: any - article: any + article: Article + content: string }) => { - const $ = cheerio.load(draft.content) + const $ = cheerio.load(content) const mentionIds = $('a.mention') - .map((index: number, node: any) => { + .map((node: BasicAcceptedElems) => { const id = $(node).attr('data-id') if (id) { return id @@ -596,40 +518,41 @@ export class PublicationQueue extends BaseQueue { /** * Delete unused assets from S3 and DB, skip if error is thrown. */ - private deleteUnusedAssets = async ({ - draftEntityTypeId, - draft, - }: { - draftEntityTypeId: string - draft: any - }) => { - const systemService = new SystemService(this.connections) - try { - const [assets, uuids] = await Promise.all([ - systemService.findAssetAndAssetMap({ - entityTypeId: draftEntityTypeId, - entityId: draft.id, - }), - extractAssetDataFromHtml(draft.content), - ]) - - const unusedAssetPaths: { [id: string]: string } = {} - assets.forEach((asset: any) => { - const isCover = draft.cover === asset.assetId - const isEmbed = uuids && uuids.includes(asset.uuid) - - if (!isCover && !isEmbed) { - unusedAssetPaths[`${asset.assetId}`] = asset.path - } - }) - - if (Object.keys(unusedAssetPaths).length > 0) { - await systemService.deleteAssetAndAssetMap(unusedAssetPaths) - } - } catch (e) { - logger.error(e) - } - } + // TOFIX: `extractAssetDataFromHtml` and `systemService.deleteAssetAndAssetMap` are broken + // private deleteUnusedAssets = async ({ + // draftEntityTypeId, + // draft, + // }: { + // draftEntityTypeId: string + // draft: Draft + // }) => { + // const systemService = new SystemService(this.connections) + // try { + // const [assets, uuids] = await Promise.all([ + // systemService.findAssetAndAssetMap({ + // entityTypeId: draftEntityTypeId, + // entityId: draft.id, + // }), + // extractAssetDataFromHtml(draft.content), + // ]) + + // const unusedAssetPaths: { [id: string]: string } = {} + // assets.forEach((asset: any) => { + // const isCover = draft.cover === asset.assetId + // const isEmbed = uuids && uuids.includes(asset.uuid) + + // if (!isCover && !isEmbed) { + // unusedAssetPaths[`${asset.assetId}`] = asset.path + // } + // }) + + // if (Object.keys(unusedAssetPaths).length > 0) { + // await systemService.deleteAssetAndAssetMap(unusedAssetPaths) + // } + // } catch (e) { + // logger.error(e) + // } + // } private handleRefreshIPNSFeed: Queue.ProcessCallbackFunction = async ( diff --git a/src/connectors/queue/revision.ts b/src/connectors/queue/revision.ts index 2ee860ff0..378d4e4fe 100644 --- a/src/connectors/queue/revision.ts +++ b/src/connectors/queue/revision.ts @@ -1,34 +1,28 @@ -import type { Connections } from 'definitions' +import type { Connections, Article } from 'definitions' import { invalidateFQC } from '@matters/apollo-response-cache' -import { makeSummary } from '@matters/ipns-site-generator' -import slugify from '@matters/slugify' import Queue from 'bull' import * as cheerio from 'cheerio' import _difference from 'lodash/difference' -import _uniq from 'lodash/uniq' import { ARTICLE_STATE, DB_NOTICE_TYPE, NODE_TYPES, - PIN_STATE, - PUBLISH_STATE, QUEUE_CONCURRENCY, QUEUE_JOB, QUEUE_NAME, QUEUE_PRIORITY, } from 'common/enums' import { environment } from 'common/environment' +import { ServerError } from 'common/errors' import { getLogger } from 'common/logger' -import { countWords, fromGlobalId } from 'common/utils' +import { fromGlobalId } from 'common/utils' import { AtomService, NotificationService, - DraftService, ArticleService, UserService, - SystemService, } from 'connectors' import { BaseQueue } from './baseQueue' @@ -36,17 +30,19 @@ import { BaseQueue } from './baseQueue' const logger = getLogger('queue-revision') interface RevisedArticleData { - draftId: string + articleId: string + oldArticleVersionId: string + newArticleVersionId: string iscnPublish?: boolean } export class RevisionQueue extends BaseQueue { - constructor(connections: Connections) { + public constructor(connections: Connections) { super(QUEUE_NAME.revision, connections) this.addConsumers() } - publishRevisedArticle = (data: RevisedArticleData) => + public publishRevisedArticle = (data: RevisedArticleData) => this.q.add(QUEUE_JOB.publishRevisedArticle, data, { priority: QUEUE_PRIORITY.CRITICAL, }) @@ -68,312 +64,218 @@ export class RevisionQueue extends BaseQueue { */ private handlePublishRevisedArticle: Queue.ProcessCallbackFunction = async (job, done) => { - const { draftId, iscnPublish } = job.data as RevisedArticleData + const { + articleId, + oldArticleVersionId, + newArticleVersionId, + iscnPublish, + } = job.data as RevisedArticleData - const draftService = new DraftService(this.connections) const articleService = new ArticleService(this.connections) const userService = new UserService(this.connections) - const systemService = new SystemService(this.connections) const notificationService = new NotificationService(this.connections) const atomService = new AtomService(this.connections) - let draft = await draftService.baseFindById(draftId) + const article = await atomService.articleIdLoader.load(articleId) + const oldArticleVersion = await atomService.articleVersionIdLoader.load( + oldArticleVersionId + ) + const newArticleVersion = await atomService.articleVersionIdLoader.load( + newArticleVersionId + ) // Step 1: checks - if (!draft) { + if (!article) { job.progress(100) - done(null, `Revision draft ${draftId} not found`) + done(null, `Revised article ${articleId} not found`) return } - if (draft.publishState !== PUBLISH_STATE.pending) { + if (!oldArticleVersion) { job.progress(100) - done(null, `Revision draft ${draftId} isn't in pending state.`) + done(null, `old article version ${oldArticleVersionId} not found`) return } - let article = await articleService.baseFindById(draft.articleId) - if (!article) { + + if (!newArticleVersion) { job.progress(100) - done(null, `Revised article ${draft.articleId} not found`) + done(null, `new article version ${newArticleVersionId} not found`) return } + if (article.state !== ARTICLE_STATE.active) { job.progress(100) - done(null, `Revised article ${draft.articleId} is not active`) + done(null, `Revised article ${article.id} is not active`) return } - const preDraft = await draftService.baseFindById(article.draftId) job.progress(10) + // Section1: update local DB related + const { content: newContent } = + await atomService.articleContentIdLoader.load( + newArticleVersion.contentId + ) try { - const summary = draft.summary || makeSummary(draft.content) - const wordCount = countWords(draft.content) - - // Step 2: publish content to IPFS - const revised = { ...draft, summary } - - // Step 3: update draft - ;[draft] = await Promise.all([ - draftService.baseUpdate(draft.id, { - // dataHash, - // mediaHash, - wordCount, - archived: true, - // iscnId, - publishState: PUBLISH_STATE.published, - pinState: PIN_STATE.pinned, - }), - // iscnId && this.articleService.baseUpdate(article.id, { iscnId }), - ]) - - job.progress(40) - - // Step 4: update back to article - const revisionCount = - (article.revisionCount || 0) + (iscnPublish ? 0 : 1) // skip revisionCount for iscnPublish retry - const updatedArticle = await articleService.baseUpdate(article.id, { - draftId: draft.id, - dataHash: null, // TBD in Section2 - mediaHash: null, - summary, - wordCount, - revisionCount, - slug: slugify(draft.title), - }) - job.progress(50) - - const author = await userService.baseFindById(article.authorId) - const { userName, displayName } = author - - // Note: the following steps won't affect the publication. - // Section1: update local DB related - try { - // Step 5: copy previous draft asset maps for current draft - // Note: collection and tags are handled in edit resolver. - // @see src/mutations/article/editArticle.ts - const { id: entityTypeId } = await systemService.baseFindEntityTypeId( - 'draft' - ) - await systemService.copyAssetMapEntities({ - source: preDraft.id, - target: draft.id, - entityTypeId, - }) - - // Step 7: handle newly added mentions + // Step 2: handle newly added mentions + if (newArticleVersion.contentId !== oldArticleVersion.contentId) { + const { content: oldContent } = + await atomService.articleContentIdLoader.load( + oldArticleVersion.contentId + ) await this.handleMentions( { - article: updatedArticle, - preDraftContent: preDraft.content, - content: draft.content, + article, + preContent: oldContent, + content: newContent, }, notificationService ) - - job.progress(70) - } catch (err) { - // ignore errors caused by these steps - logger.warn('job failed at optional step: %j', { - err, - job, - draftId: draft.id, - }) } + job.progress(70) + } catch (err) { + // ignore errors caused by these steps + logger.warn('job failed at optional step: %j', { + err, + job, + articleVersionId: newArticleVersionId, + }) + } + + // Step 3: trigger notifications + notificationService.trigger({ + event: DB_NOTICE_TYPE.revised_article_published, + recipientId: article.authorId, + entities: [{ type: 'target', entityTable: 'article', entity: article }], + }) - // Step 8: trigger notifications - notificationService.trigger({ - event: DB_NOTICE_TYPE.revised_article_published, - recipientId: article.authorId, - entities: [ - { type: 'target', entityTable: 'article', entity: article }, - ], + // Step 4: invalidate article and user cache + await Promise.all([ + invalidateFQC({ + node: { type: NODE_TYPES.User, id: article.authorId }, + redis: this.connections.redis, + }), + invalidateFQC({ + node: { type: NODE_TYPES.Article, id: article.id }, + redis: this.connections.redis, + }), + ]) + + // Section2: publish to external services like: IPFS / IPNS / ISCN / etc... + const author = await atomService.userIdLoader.load(article.authorId) + const { userName, displayName } = author + try { + // Step5: ipfs publishing + const { + contentHash: dataHash, + mediaHash, + key, + } = await articleService.publishToIPFS( + article, + newArticleVersion, + newContent + ) + + // update dataHash and mediaHash + await atomService.update({ + table: 'article_version', + where: { id: newArticleVersion.id }, + data: { dataHash, mediaHash }, }) - // Step 9: invalidate article and user cache - await Promise.all([ - invalidateFQC({ - node: { type: NODE_TYPES.User, id: article.authorId }, - redis: this.connections.redis, - }), - invalidateFQC({ - node: { type: NODE_TYPES.Article, id: article.id }, - redis: this.connections.redis, - }), - ]) - - // Section2: publish to external services like: IPFS / IPNS / ISCN / etc... - let ipnsRes: any - try { - const { - contentHash: dataHash, - mediaHash, - key, - } = (await articleService.publishToIPFS(revised))! - - ;[draft, article] = await Promise.all([ - draftService.baseUpdate(draft.id, { - dataHash, - mediaHash, - }), - articleService.baseUpdate(article.id, { - dataHash, - mediaHash, - }), - ]) - - // update secret - if (key) { - await this.handleCircle( - { - article, - circleId: draft.circleId, - secret: key, - }, - atomService - ) - } + // update secret + if (key && newArticleVersion.circleId) { + await atomService.update({ + table: 'article_circle', + where: { + articleId: articleId, + circleId: newArticleVersion.circleId, + }, + data: { + secret: key, + }, + }) + } - // Step: iscn publishing - if (iscnPublish) { - const liker = (await userService.findLiker({ - userId: author.id, - }))! - const cosmosWallet = await userService.likecoin.getCosmosWallet({ - liker, - }) - - const iscnId = await userService.likecoin.iscnPublish({ - mediaHash: `hash://sha256/${mediaHash}`, - ipfsHash: `ipfs://${dataHash}`, - cosmosWallet, // 'TBD', - userName: `${displayName} (@${userName})`, - title: draft.title, - description: summary, - datePublished: article.created_at?.toISOString().substring(0, 10), - url: `https://${environment.siteDomain}/@${userName}/${article.id}-${article.slug}-${mediaHash}`, - tags: draft.tags, - - // for liker auth&headers info - liker, - // likerIp, - // userAgent, - }) - - // handling both cases of set to true or false, but not omit (undefined) - ;[draft, article] = await Promise.all([ - draftService.baseUpdate(draft.id, { - iscnId, - iscnPublish, // : iscnPublish || draft.iscnPublish, - }), - articleService.baseUpdate(article.id, { - iscnId, - }), - ]) + // Step6: iscn publishing + if (iscnPublish) { + const liker = await userService.findLiker({ + userId: author.id, + }) + // expect liker to be found + if (!liker) { + throw new ServerError(`Liker not found for user ${author.id}`) } + const cosmosWallet = await userService.likecoin.getCosmosWallet({ + liker, + }) - ipnsRes = await articleService.publishFeedToIPNS({ - userName, - // incremental: true, // attach the last just published article - updatedDrafts: [draft], - forceReplace: true, + const iscnId = await userService.likecoin.iscnPublish({ + mediaHash: `hash://sha256/${mediaHash}`, + ipfsHash: `ipfs://${dataHash}`, + cosmosWallet, // 'TBD', + userName: `${displayName} (@${userName})`, + title: newArticleVersion.title, + description: newArticleVersion.summary, + datePublished: article.createdAt.toISOString().substring(0, 10), + url: `https://${environment.siteDomain}/a/${article.shortHash}`, + tags: newArticleVersion.tags, + + // for liker auth&headers info + liker, + // likerIp, + // userAgent, }) - } catch (err) { - logger.warn('job failed at optional step: %j', { - err, - job, - draftId: draft.id, + + // handling both cases of set to true or false, but not omit (undefined) + await atomService.update({ + table: 'article_version', + where: { id: newArticleVersion.id }, + data: { iscnId }, }) } - job.progress(100) - - // no await to notify async - articleService - .sendArticleFeedMsgToSQS({ article, author, ipnsData: ipnsRes }) - .catch((err: Error) => logger.error('failed sqs notify:', err)) - - // no await to notify async - atomService.aws - ?.snsPublishMessage({ - // MessageGroupId: `ipfs-articles-${environment.env}:articles-feed`, - MessageBody: { - articleId: article.id, - title: article.title, - url: `https://${environment.siteDomain}/@${userName}/${article.id}-${article.slug}`, - dataHash: article.dataHash, - mediaHash: article.mediaHash, - - // ipns info: - ipnsKey: ipnsRes?.ipnsKey, - lastDataHash: ipnsRes?.lastDataHash, - - // author info: - userName, - displayName, - }, + if (userName) { + await articleService.publishFeedToIPNS({ + userName, }) - .catch((err: Error) => logger.error('failed sns notify:', err)) - - done(null, { - articleId: article.id, - draftId: draft.id, - dataHash: article.dataHash, - mediaHash: article.mediaHash, - iscnPublish, // : iscnPublish || draft.iscnPublish, - iscnId: draft.iscnId, - }) - } catch (err: any) { - await draftService.baseUpdate(draft.id, { - publishState: PUBLISH_STATE.error, + } + } catch (err) { + logger.warn('job failed at optional step: %j', { + err, + job, + articleVersionId: newArticleVersionId, }) + } - notificationService.trigger({ - event: DB_NOTICE_TYPE.revised_article_not_published, - recipientId: article.authorId, - entities: [ - { type: 'target', entityTable: 'article', entity: article }, - ], - }) + job.progress(100) - done(err) - } - } + const updated = await atomService.findUnique({ + table: 'article_version', + where: { id: newArticleVersionId }, + }) - private handleCircle = async ( - { - article, - circleId, - secret, - }: { - article: any - circleId: string - secret: string - }, - atomService: AtomService - ) => { - await atomService.update({ - table: 'article_circle', - where: { articleId: article.id, circleId }, - data: { - secret, - updatedAt: this.connections.knex.fn.now(), - }, - }) - } + done(null, { + articleId: article.id, + dataHash: updated.dataHash, + mediaHash: updated.mediaHash, + iscnPublish, + iscnId: updated.iscnId, + }) + } private handleMentions = async ( { article, - preDraftContent, + preContent, content, }: { - article: any - preDraftContent: string + article: Article + preContent: string content: string }, notificationService: NotificationService ) => { // gather pre-draft ids - let $ = cheerio.load(preDraftContent) + let $ = cheerio.load(preContent) const filter = (index: number, node: any) => { const id = $(node).attr('data-id') if (id) { diff --git a/src/connectors/queue/user.ts b/src/connectors/queue/user.ts index 80a0afc94..b25acd159 100644 --- a/src/connectors/queue/user.ts +++ b/src/connectors/queue/user.ts @@ -1,4 +1,4 @@ -import type { Connections } from 'definitions' +import type { Connections, PunishRecord } from 'definitions' import Queue from 'bull' @@ -22,7 +22,7 @@ interface ArchiveUserData { } export class UserQueue extends BaseQueue { - constructor(connections: Connections) { + public constructor(connections: Connections) { super(QUEUE_NAME.user, connections) this.addConsumers() } @@ -89,7 +89,7 @@ export class UserQueue extends BaseQueue { data, }) - await userService.baseUpdate( + await userService.baseUpdate( record.id, { archived: true }, 'punish_record' @@ -100,6 +100,7 @@ export class UserQueue extends BaseQueue { }) users.push(record.userId) job.progress(((index + 1) / records.length) * 100) + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { logger.error(err) } @@ -107,6 +108,7 @@ export class UserQueue extends BaseQueue { ) done(null, users) + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { done(err) } diff --git a/src/connectors/recommendationService.ts b/src/connectors/recommendationService.ts new file mode 100644 index 000000000..ea3176244 --- /dev/null +++ b/src/connectors/recommendationService.ts @@ -0,0 +1,207 @@ +import type { Connections, Article } from 'definitions' +import type { Knex } from 'knex' + +import { + MATTERS_CHOICE_TOPIC_STATE, + MATTERS_CHOICE_TOPIC_VALID_PIN_AMOUNTS, + ARTICLE_STATE, + DEFAULT_TAKE_PER_PAGE, +} from 'common/enums' +import { + UserInputError, + EntityNotFoundError, + ActionFailedError, +} from 'common/errors' + +import { AtomService } from './atomService' + +export class RecommendationService { + private connections: Connections + private models: AtomService + private knexRO: Knex + + public constructor(connections: Connections) { + this.connections = connections + this.knexRO = connections.knexRO + this.models = new AtomService(this.connections) + } + + public createIcymiTopic = async ({ + title, + articleIds, + pinAmount, + note, + }: { + title: string + articleIds: string[] + pinAmount: number + note?: string + }) => { + if (!MATTERS_CHOICE_TOPIC_VALID_PIN_AMOUNTS.includes(pinAmount)) { + throw new UserInputError('Invalid pin amount') + } + const articles = await this.models.articleIdLoader.loadMany(articleIds) + for (const article of articles) { + if (!article || article.state !== ARTICLE_STATE.active) { + throw new UserInputError('Invalid article') + } + } + return this.models.create({ + table: 'matters_choice_topic', + data: { + title, + articles: articleIds, + pinAmount, + note, + state: MATTERS_CHOICE_TOPIC_STATE.editing, + }, + }) + } + + public updateIcymiTopic = async ( + id: string, + { + title, + articleIds, + pinAmount, + note, + }: { + title?: string + articleIds?: string[] + pinAmount?: number + note?: string + } + ) => { + if ( + pinAmount && + !MATTERS_CHOICE_TOPIC_VALID_PIN_AMOUNTS.includes(pinAmount) + ) { + throw new UserInputError('Invalid pin amount') + } + if (articleIds) { + const articles = await this.models.articleIdLoader.loadMany(articleIds) + for (const article of articles) { + if (!article || article.state !== ARTICLE_STATE.active) { + throw new UserInputError('Invalid article') + } + } + } + const topic = await this.models.findUnique({ + table: 'matters_choice_topic', + where: { id }, + }) + if (!topic) { + throw new EntityNotFoundError('Topic not found') + } + if (topic.state === MATTERS_CHOICE_TOPIC_STATE.archived) { + throw new ActionFailedError('Invalid topic state') + } + return this.models.update({ + table: 'matters_choice_topic', + where: { id }, + data: { title, articles: articleIds, pinAmount, note }, + }) + } + + public publishIcymiTopic = async (id: string) => { + const topic = await this.models.findUnique({ + table: 'matters_choice_topic', + where: { id }, + }) + if (!topic) { + throw new EntityNotFoundError('Topic not found') + } + if (topic.state !== MATTERS_CHOICE_TOPIC_STATE.editing) { + throw new ActionFailedError('Invalid topic state') + } + if (topic.articles.length < topic.pinAmount) { + throw new ActionFailedError('Articles amount less than pinAmount') + } + const publisheds = await this.models.findMany({ + table: 'matters_choice_topic', + where: { state: MATTERS_CHOICE_TOPIC_STATE.published }, + }) + await Promise.all(publisheds.map((t) => this.archiveIcymiTopic(t.id))) + + return this.models.update({ + table: 'matters_choice_topic', + where: { id }, + data: { + state: MATTERS_CHOICE_TOPIC_STATE.published, + publishedAt: new Date(), + }, + }) + } + + public archiveIcymiTopic = async (id: string) => { + const topic = await this.models.findUnique({ + table: 'matters_choice_topic', + where: { id }, + }) + if (topic.state === MATTERS_CHOICE_TOPIC_STATE.editing) { + await this.models.deleteMany({ + table: 'matters_choice_topic', + where: { id }, + }) + return null + } else if (topic.state === MATTERS_CHOICE_TOPIC_STATE.published) { + for (const articleId of topic.articles.reverse()) { + await this.models.upsert({ + table: 'matters_choice', + where: { articleId }, + create: { articleId }, + update: {}, + }) + } + return this.models.update({ + table: 'matters_choice_topic', + where: { id }, + data: { state: MATTERS_CHOICE_TOPIC_STATE.archived }, + }) + } else { + throw new ActionFailedError('Invalid topic state') + } + } + + public findIcymiArticles = async ({ + take, + skip, + }: { + take?: number + skip?: number + }) => { + const MAX_ITEM_COUNT = DEFAULT_TAKE_PER_PAGE * 50 + const records = await this.knexRO + .select( + 'article.*', + this.knexRO.raw('COUNT(1) OVER() ::int AS total_count') + ) + .from( + this.knexRO + .select() + .from('matters_choice') + .whereNotIn( + 'article_id', + this.knexRO + .select(this.knexRO.raw('UNNEST(articles)')) + .from('matters_choice_topic') + .where('state', MATTERS_CHOICE_TOPIC_STATE.published) + ) + .orderBy('updated_at', 'desc') + .limit(MAX_ITEM_COUNT) + .as('choice') + ) + .leftJoin('article', 'choice.article_id', 'article.id') + .where({ state: ARTICLE_STATE.active }) + .modify((builder) => { + if (skip !== undefined && Number.isFinite(skip)) { + builder.offset(skip) + } + if (take !== undefined && Number.isFinite(take)) { + builder.limit(take) + } + }) + + return [records as Article[], records[0]?.totalCount || 0] + } +} diff --git a/src/connectors/systemService.ts b/src/connectors/systemService.ts index fd8f721e1..25a79d365 100644 --- a/src/connectors/systemService.ts +++ b/src/connectors/systemService.ts @@ -3,9 +3,17 @@ import type { SkippedListItemType, Viewer, Connections, + ReportType, + ReportReason, + Report, + Asset, + BaseDBSchema, + LogRecord, + Blocklist, } from 'definitions' +import type { Knex } from 'knex' -import { Knex } from 'knex' +import { invalidateFQC } from '@matters/apollo-response-cache' import { v4 } from 'uuid' import { @@ -16,14 +24,17 @@ import { USER_ROLE, FEATURE_NAME, FEATURE_FLAG, + COMMENT_STATE, + COMMENT_TYPE, + NODE_TYPES, } from 'common/enums' import { getLogger } from 'common/logger' import { BaseService } from 'connectors' const logger = getLogger('service-system') -export class SystemService extends BaseService { - featureFlagTable: string +export class SystemService extends BaseService { + private featureFlagTable: string public constructor(connections: Connections) { super('noop', connections) @@ -168,12 +179,15 @@ export class SystemService extends BaseService { /** * Find asset by a given uuid */ - public findAssetByUUID = async (uuid: string) => + public findAssetByUUID = async (uuid: string): Promise => this.baseFindByUUID(uuid, 'asset') public findAssetByPath = async (path: string) => this.knex('asset').where('path', path).first() + /** + * Find or create asset and asset_map record by path + */ public findAssetOrCreateByPath = async ( // path: string, data: ItemData, @@ -241,7 +255,7 @@ export class SystemService extends BaseService { * Find the url of an asset by a given id. */ public findAssetUrl = async (id: string): Promise => { - const result = await this.baseFindById(id, 'asset') + const result = await this.baseFindById(id, 'asset') return result ? this.genAssetUrl(result) : null } @@ -289,27 +303,36 @@ export class SystemService extends BaseService { /** * Copy entity of asset map by given ids + * + * @remarks + * + * Delete actual assets carefully after using this method, + * only delete the actual asset when all other related asset_map record have been removed + * */ public copyAssetMapEntities = async ({ source, target, - entityTypeId, }: { - source: string - target: string - entityTypeId: string + source: { entityTypeId: string; entityId: string } + target: { entityTypeId: string; entityId: string } }) => { const maps = await this.knex .select() .from('asset_map') - .where({ entityTypeId, entityId: source }) + .where({ entityTypeId: source.entityTypeId, entityId: source.entityId }) await Promise.all( maps.map((map) => - this.baseCreate( - { ...map, id: undefined, entityId: target }, - 'asset_map' - ) + this.models.create({ + table: 'asset_map', + data: { + ...map, + id: undefined, + entityTypeId: target.entityTypeId, + entityId: target.entityId, + }, + }) ) ) } @@ -357,7 +380,7 @@ export class SystemService extends BaseService { this.knex.select().from('log_record').where(where).first() public logRecord = async (data: { userId: string; type: string }) => - this.baseUpdateOrCreate({ + this.baseUpdateOrCreate({ where: data, data: { readAt: new Date(), ...data }, table: 'log_record', @@ -418,7 +441,7 @@ export class SystemService extends BaseService { }) => { const where = { type, value } - return this.baseUpdateOrCreate({ + return this.baseUpdateOrCreate({ where, data: { type, @@ -426,7 +449,6 @@ export class SystemService extends BaseService { note, archived, uuid: uuid || v4(), - updatedAt: new Date(), }, table: 'blocklist', }) @@ -445,9 +467,9 @@ export class SystemService extends BaseService { } public updateSkippedItem = async ( - where: Record, - data: Record - ) => { + where: Partial, + data: Partial + ): Promise => { const [updateItem] = await this.knex .where(where) .update(data) @@ -455,4 +477,105 @@ export class SystemService extends BaseService { .returning('*') return updateItem } + + /** + * Create a report of target. + * + * @remarks + * The target could be an article or a comment. + * When the target is a comment, collapse the comment base on reports amount and reporters. + */ + public submitReport = async ({ + targetType, + targetId, + reporterId, + reason, + }: { + targetType: ReportType + targetId: string + reporterId: string + reason: ReportReason + }): Promise => { + if (targetType === NODE_TYPES.Article) { + const ret = await this.knex('report') + .insert({ + articleId: targetId, + reporterId, + reason, + }) + .returning('*') + return ret[0] + } else { + const ret = await this.knex('report') + .insert({ + commentId: targetId, + reporterId, + reason, + }) + .returning('*') + + await this.tryCollapseComment(targetId) + + return ret[0] + } + } + + /** + * Collapse the article comment if its reports are created by more than 3 different users or 1 article author + * + * @returns true if the comment is collapsed, otherwise false + * + */ + private tryCollapseComment = async (commentId: string): Promise => { + const comment = await this.models.findUnique({ + table: 'comment', + where: { id: commentId }, + }) + + if ( + !comment || + comment.state === COMMENT_STATE.collapsed || + comment.type !== COMMENT_TYPE.article + ) { + return false + } + + const reports = await this.knex('report') + .select(['id', 'reporterId']) + .distinctOn('reporterId') + .where({ commentId }) + + if (reports.length >= 3) { + await this.models.update({ + table: 'comment', + where: { id: commentId }, + data: { state: COMMENT_STATE.collapsed }, + }) + await invalidateFQC({ + node: { id: commentId, type: NODE_TYPES.Comment }, + redis: this.redis, + }) + return true + } + + const { authorId } = await this.models.findUnique({ + table: 'article', + where: { id: comment.targetId }, + }) + + if (authorId && reports.find((r) => r.reporterId === authorId)) { + await this.models.update({ + table: 'comment', + where: { id: commentId }, + data: { state: COMMENT_STATE.collapsed }, + }) + await invalidateFQC({ + node: { id: commentId, type: NODE_TYPES.Comment }, + redis: this.redis, + }) + return true + } + + return false + } } diff --git a/src/connectors/tagService.ts b/src/connectors/tagService.ts index 9d78fd43f..0584b468c 100644 --- a/src/connectors/tagService.ts +++ b/src/connectors/tagService.ts @@ -1,34 +1,29 @@ -import type { Connections, Item, ItemData, Tag } from 'definitions' +import type { Connections, Item, ItemData, Tag, TagBoost } from 'definitions' -import DataLoader from 'dataloader' import { Knex } from 'knex' +import { difference } from 'lodash' import { ARTICLE_STATE, + MAX_TAGS_PER_ARTICLE_LIMIT, DEFAULT_TAKE_PER_PAGE, TAG_ACTION, VIEW, + MATERIALIZED_VIEW, } from 'common/enums' import { environment } from 'common/environment' +import { TooManyTagsForArticleError, ForbiddenError } from 'common/errors' import { getLogger } from 'common/logger' -import { normalizeSearchKey } from 'common/utils' +import { normalizeSearchKey, normalizeTagInput } from 'common/utils' import { BaseService } from 'connectors' const logger = getLogger('service-tag') -export class TagService extends BaseService { - public dataloader: DataLoader - +export class TagService extends BaseService { public constructor(connections: Connections) { super('tag', connections) - this.dataloader = new DataLoader(this.baseFindByIds) } - public loadById = async (id: string): Promise => - this.dataloader.load(id) as Promise - public loadByIds = async (ids: string[]): Promise => - this.dataloader.loadMany(ids) as Promise - /** * Find tags */ @@ -203,7 +198,7 @@ export class TagService extends BaseService { userId: string skip?: number take?: number - }) => + }): Promise> => this.knex .select('target_id AS id') .from('action_tag') @@ -233,7 +228,7 @@ export class TagService extends BaseService { owner, }: { content: string - cover?: string + cover?: string | null creator: string description?: string editors: string[] @@ -379,7 +374,7 @@ export class TagService extends BaseService { } return this.baseUpdateOrCreate({ where: data, - data: { updatedAt: new Date(), ...data }, + data: data, table: 'action_tag', }) } @@ -468,7 +463,6 @@ export class TagService extends BaseService { where: data, data, table: 'action_tag', - updateUpdatedAt: true, }) : this.knex.from('action_tag').where(data).del() } @@ -646,11 +640,9 @@ export class TagService extends BaseService { records[0] ) - const nodes = (await this.dataloader.loadMany( - // records.map(({ id }) => id) - // records.map((item: any) => item.id).filter(Boolean) + const nodes = await this.models.tagIdLoader.loadMany( records.map((item: any) => `${item.id}`).filter(Boolean) - )) as Item[] + ) return { nodes, totalCount } } catch (err) { @@ -679,9 +671,9 @@ export class TagService extends BaseService { } public setBoost = async ({ id, boost }: { id: string; boost: number }) => - this.baseUpdateOrCreate({ + this.baseUpdateOrCreate({ where: { tagId: id }, - data: { tagId: id, boost, updatedAt: new Date() }, + data: { tagId: id, boost }, table: 'tag_boost', }) @@ -704,10 +696,10 @@ export class TagService extends BaseService { // recent 1 week, 1 month, or 3 months? top?: 'r1w' | 'r2w' | 'r1m' | 'r3m' minAuthors?: number - }) => + }): Promise> => this.knex .select('id') - .from(VIEW.tags_lasts_view) + .from(MATERIALIZED_VIEW.tags_lasts_view_materialized) .modify(function (this: Knex.QueryBuilder) { if (minAuthors) { this.where('num_authors', '>=', minAuthors) @@ -787,7 +779,7 @@ export class TagService extends BaseService { } public addTagRecommendation = (tagId: string) => - this.baseFindOrCreate({ + this.baseFindOrCreate({ where: { tagId }, data: { tagId }, table: 'matters_choice_tag', @@ -861,7 +853,7 @@ export class TagService extends BaseService { let result: any try { - result = await this.knex(VIEW.tags_lasts_view) + result = await this.knex(MATERIALIZED_VIEW.tags_lasts_view_materialized) .select('id', 'content', 'id_slug', 'num_authors', 'num_articles') .where(function (this: Knex.QueryBuilder) { this.where('id', '=', tagId) @@ -907,7 +899,7 @@ export class TagService extends BaseService { let result: any try { - result = await this.knexRO(VIEW.tags_lasts_view) + result = await this.knexRO(MATERIALIZED_VIEW.tags_lasts_view_materialized) .select('id', 'content', 'id_slug', 'num_authors', 'num_articles') .where(function (this: Knex.QueryBuilder) { this.where('tag_id', tagId) @@ -973,8 +965,7 @@ export class TagService extends BaseService { builder.orWhereIn( 'tag_id', this.knex - .from(VIEW.tags_lasts_view) - // .joinRaw('CROSS JOIN unnest(dup_tag_ids) AS x(id)') + .from(MATERIALIZED_VIEW.tags_lasts_view_materialized) .whereRaw('dup_tag_ids @> ARRAY[?] ::int[]', tagId) .select(this.knex.raw('UNNEST(dup_tag_ids)')) ) @@ -1060,10 +1051,15 @@ export class TagService extends BaseService { */ public findArticleCovers = async ({ id }: { id: string }) => this.knexRO - .select('article.cover') + .select('article_version_newest.cover') .from('article_tag') - .join('article', 'article_id', 'article.id') - .whereNotNull('cover') + .join( + 'article_version_newest', + 'article_tag.article_id', + 'article_version_newest.article_id' + ) + .join('article', 'article_tag.article_id', 'article.id') + .whereNotNull('article_version_newest.cover') .andWhere({ tagId: id, state: ARTICLE_STATE.active, @@ -1082,7 +1078,7 @@ export class TagService extends BaseService { }: { tagId: string content: string - }) => this.baseUpdate(tagId, { content, updatedAt: this.knex.fn.now() }) + }) => this.baseUpdate(tagId, { content }) public mergeTags = async ({ tagIds, @@ -1153,11 +1149,83 @@ export class TagService extends BaseService { */ public findRelatedTags = async ({ id }: { id: string; content?: string }) => this.knex - .from(VIEW.tags_lasts_view) + .from(MATERIALIZED_VIEW.tags_lasts_view_materialized) .joinRaw( 'CROSS JOIN jsonb_to_recordset(top_rels) AS x(tag_rel_id int, count_rel int, count_common int, similarity float)' ) .where(this.knex.raw(`dup_tag_ids @> ARRAY[?] ::int[]`, [id])) .select('x.tag_rel_id AS id') .orderByRaw('x.count_rel * x.similarity DESC NULLS LAST') + + public updateArticleTags = async ({ + articleId, + actorId, + tags, + }: { + articleId: string + actorId: string + tags: string[] + }) => { + const article = await this.models.articleIdLoader.load(articleId) + // validate + const oldIds = (await this.findByArticleId({ articleId: article.id })).map( + ({ id: tagId }: { id: string }) => tagId + ) + + if ( + tags && + tags.length > MAX_TAGS_PER_ARTICLE_LIMIT && + tags.length > oldIds.length + ) { + throw new TooManyTagsForArticleError( + `Not allow more than ${MAX_TAGS_PER_ARTICLE_LIMIT} tags on an article` + ) + } + + // create tag records + const tagEditors = environment.mattyId + ? [environment.mattyId, article.authorId] + : [article.authorId] + const dbTags = ( + await Promise.all( + tags.filter(Boolean).map(async (content: string) => + this.create( + { + content, + creator: article.authorId, + editors: tagEditors, + owner: article.authorId, + }, + { + columns: ['id', 'content'], + skipCreate: normalizeTagInput(content) !== content, // || content.length > MAX_TAG_CONTENT_LENGTH, + } + ) + ) + ) + ).map(({ id, content }) => ({ id: `${id}`, content })) + + const newIds = dbTags.map(({ id: tagId }) => tagId) + + // check if add tags include matty's tag + const mattyTagId = environment.mattyChoiceTagId || '' + const isMatty = environment.mattyId === actorId + const addIds = difference(newIds, oldIds) + if (addIds.includes(mattyTagId) && !isMatty) { + throw new ForbiddenError('not allow to add official tag') + } + + // add + await this.createArticleTags({ + articleIds: [article.id], + creator: article.authorId, + tagIds: addIds, + }) + + // delete unwanted + await this.deleteArticleTagsByTagIds({ + articleId: article.id, + tagIds: difference(oldIds, newIds), + }) + } } diff --git a/src/connectors/userService.ts b/src/connectors/userService.ts index 188554477..7641d3355 100644 --- a/src/connectors/userService.ts +++ b/src/connectors/userService.ts @@ -1,20 +1,25 @@ import type { - GQLUserRestriction, + UserRestriction, + Article, Item, ItemData, UserOAuthLikeCoin, + UserOauthLikecoinDB, UserOAuthLikeCoinAccountType, + UserNotifySetting, User, - VerficationCode, + ActionUser, + VerificationCode, ValueOf, SocialAccount, Connections, + PunishRecord, LANGUAGES, + UserBoost, } from 'definitions' import axios from 'axios' import { compare } from 'bcrypt' -import DataLoader from 'dataloader' import jwt from 'jsonwebtoken' import { Knex } from 'knex' import _, { random } from 'lodash' @@ -120,17 +125,15 @@ const logger = getLogger('service-user') // const SEARCH_DEFAULT_TEXT_RANK_THRESHOLD = 0.0001 -export class UserService extends BaseService { +export class UserService extends BaseService { private ipfs: typeof ipfsServers public likecoin: LikeCoin - public dataloader: DataLoader - constructor(connections: Connections) { + public constructor(connections: Connections) { super('user', connections) this.ipfs = ipfsServers this.likecoin = new LikeCoin(connections) - this.dataloader = new DataLoader(this.baseFindByIds) } /********************************* @@ -138,16 +141,10 @@ export class UserService extends BaseService { * Account * * * *********************************/ - public loadById = async (id: string): Promise => - this.dataloader.load(id) as Promise - public loadByIds = async (ids: string[]): Promise => - this.dataloader.loadMany(ids) as Promise - public create = async ( { userName, displayName, - // description, password, email, ethAddress, @@ -157,7 +154,6 @@ export class UserService extends BaseService { }: { userName?: string displayName?: string - // description?: string password?: string email?: string ethAddress?: string @@ -182,8 +178,6 @@ export class UserService extends BaseService { emailVerified, userName, displayName, - // description, - // avatar, passwordHash, agreeOn: new Date(), state: USER_STATE.active, @@ -198,7 +192,7 @@ export class UserService extends BaseService { undefined, trx ) - await this.baseCreate( + await this.baseCreate( { userId: user.id }, 'user_notify_setting', undefined, @@ -392,7 +386,6 @@ export class UserService extends BaseService { : { passwordHash } const user = await this.baseUpdate(userId, { ...data, - updatedAt: new Date(), }) return user } @@ -436,7 +429,7 @@ export class UserService extends BaseService { .first() public setEmail = async (userId: string, email: string): Promise => { - const user = await this.loadById(userId) + const user = await this.models.userIdLoader.load(userId) try { const res = await this._setEmail(user, email) auditLog({ @@ -447,6 +440,7 @@ export class UserService extends BaseService { status: 'succeeded', }) return res + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { auditLog({ actorId: userId, @@ -525,6 +519,7 @@ export class UserService extends BaseService { status: 'succeeded', }) return res + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { auditLog({ actorId: user.id, @@ -566,6 +561,7 @@ export class UserService extends BaseService { status: 'succeeded', }) return res + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { auditLog({ actorId: userId, @@ -598,7 +594,7 @@ export class UserService extends BaseService { let data: Partial = { userName } if (fillDisplayName) { - const user = await this.loadById(userId) + const user = await this.models.userIdLoader.load(userId) data = { ...data, displayName: genDisplayName(user) ?? userName } } @@ -864,7 +860,7 @@ export class UserService extends BaseService { { sample: records?.slice(0, 3) } ) - const nodes = (await this.dataloader.loadMany( + const nodes = (await this.models.userIdLoader.loadMany( records.map(({ id }) => id) )) as Item[] @@ -910,10 +906,8 @@ export class UserService extends BaseService { records[0] ) - const nodes = (await this.dataloader.loadMany( - // records.map(({ id }) => id) - // records.map((item: any) => item.id).filter(Boolean) - records.map((item: any) => `${item.id}`).filter(Boolean) + const nodes = (await this.models.userIdLoader.loadMany( + records.map((item: { id: string }) => `${item.id}`).filter(Boolean) )) as Item[] return { nodes, totalCount } @@ -1063,7 +1057,7 @@ export class UserService extends BaseService { } return this.baseUpdateOrCreate({ where: data, - data: { updatedAt: new Date(), ...data }, + data: data, table: 'action_user', }) } @@ -1205,9 +1199,9 @@ export class UserService extends BaseService { action: USER_ACTION.block, } - return this.baseUpdateOrCreate({ + return this.baseUpdateOrCreate({ where: data, - data: { updatedAt: new Date(), ...data }, + data, table: 'action_user', }) } @@ -1369,9 +1363,9 @@ export class UserService extends BaseService { } public setBoost = async ({ id, boost }: { id: string; boost: number }) => - this.baseUpdateOrCreate({ + this.baseUpdateOrCreate({ where: { userId: id }, - data: { userId: id, boost, updatedAt: new Date() }, + data: { userId: id, boost }, table: 'user_boost', }) @@ -1468,18 +1462,16 @@ export class UserService extends BaseService { * Notify Setting * * * *********************************/ - public findNotifySetting = async (userId: string): Promise => + public findNotifySetting = async ( + userId: string + ): Promise => this.knex.select().from('user_notify_setting').where({ userId }).first() public updateNotifySetting = async ( id: string, data: ItemData - ): Promise => - this.baseUpdate( - id, - { updatedAt: new Date(), ...data }, - 'user_notify_setting' - ) + ): Promise => + this.baseUpdate(id, data, 'user_notify_setting') /********************************* * * @@ -1541,7 +1533,7 @@ export class UserService extends BaseService { take: number skip: number }) => { - const result = await this.knex('article') + const result = await this.knexRO('article') .select('read.read_at', 'article.*') .rightJoin( this.knex @@ -1557,7 +1549,10 @@ export class UserService extends BaseService { .limit(take) .offset(skip) - return result.map(({ readAt, ...article }: any) => ({ readAt, article })) + return result.map(({ readAt, ...article }: { readAt: Date } & Article) => ({ + readAt, + article, + })) } public clearReadHistory = async ({ @@ -1596,7 +1591,7 @@ export class UserService extends BaseService { }: { userId?: string | null email: string - type: string + type: keyof typeof VERIFICATION_CODE_TYPE strong?: boolean expiredAt?: Date }) => { @@ -1608,7 +1603,7 @@ export class UserService extends BaseService { 8 )() - return this.baseCreate( + return this.baseCreate( { uuid: v4(), userId, @@ -1680,7 +1675,7 @@ export class UserService extends BaseService { await trx.commit() } - public confirmVerificationCode = async (code: VerficationCode) => { + public confirmVerificationCode = async (code: VerificationCode) => { if (code.status !== VERIFICATION_CODE_STATUS.active) { throw new Error('cannot verfiy a not-active code') } @@ -1709,11 +1704,7 @@ export class UserService extends BaseService { public findVerificationCodes = async ({ where, }: { - where?: { - [key: string]: any - type?: keyof typeof VERIFICATION_CODE_TYPE - status?: VERIFICATION_CODE_STATUS - } + where?: Partial }) => { const query = this.knex .select() @@ -1737,7 +1728,7 @@ export class UserService extends BaseService { }, trx?: Knex.Transaction ) => { - let data: any = { status } + let data: Partial = { status } if (status === VERIFICATION_CODE_STATUS.used) { data = { ...data, usedAt: new Date() } @@ -1745,12 +1736,7 @@ export class UserService extends BaseService { data = { ...data, verifiedAt: new Date() } } - return this.baseUpdate( - codeId, - { updatedAt: new Date(), ...data }, - 'verification_code', - trx - ) + return this.baseUpdate(codeId, data, 'verification_code', trx) } /********************************* @@ -1872,8 +1858,10 @@ export class UserService extends BaseService { }): Promise => { let userLikerId = likerId if (userId) { - const user = await this.dataloader.load(userId) - userLikerId = user.likerId + const user = await this.models.userIdLoader.load(userId) + if (user.likerId) { + userLikerId = user.likerId + } } if (!userLikerId) { @@ -1901,10 +1889,10 @@ export class UserService extends BaseService { accountType: UserOAuthLikeCoinAccountType accessToken: string refreshToken?: string - expires?: number + expires?: Date scope?: string[] }) => { - let user = await this.dataloader.load(userId) + let user = await this.models.userIdLoader.load(userId) await this.knex .select() @@ -1913,14 +1901,12 @@ export class UserService extends BaseService { .del() user = await this.baseUpdate(userId, { - updatedAt: new Date(), likerId, }) - await this.baseUpdateOrCreate({ + await this.baseUpdateOrCreate({ where: { likerId }, data: { - updatedAt: new Date(), likerId, accountType, accessToken, @@ -2078,9 +2064,7 @@ export class UserService extends BaseService { if (!user) { return } - const atomService = new AtomService(this.connections) - - const ipnsKeyRec = await atomService.findFirst({ + const ipnsKeyRec = await this.models.findFirst({ table: 'user_ipns_keys', where: { userId: user.id }, }) @@ -2101,7 +2085,7 @@ export class UserService extends BaseService { // if (!ipnsKey && res) { ipnsKey = res?.Id } const ipnsKey = imported.Id - return atomService.create({ + return this.models.create({ table: 'user_ipns_keys', data: { userId: user.id, @@ -2117,14 +2101,11 @@ export class UserService extends BaseService { * Restrictions * * * *********************************/ - public findRestrictions = async ( - id: string - ): Promise => { + public findRestrictions = async (id: string): Promise => { const table = 'user_restriction' - const atomService = new AtomService(this.connections) - return atomService.findMany({ + return this.models.findMany({ table, - select: ['type', 'created_at'], + select: ['type', 'createdAt'], where: { userId: id }, }) } @@ -2148,8 +2129,7 @@ export class UserService extends BaseService { type: keyof typeof USER_RESTRICTION_TYPE ) => { const table = 'user_restriction' - const atomService = new AtomService(this.connections) - await atomService.create({ table, data: { userId: id, type } }) + await this.models.create({ table, data: { userId: id, type } }) } public removeRestriction = async ( @@ -2157,8 +2137,7 @@ export class UserService extends BaseService { type: keyof typeof USER_RESTRICTION_TYPE ) => { const table = 'user_restriction' - const atomService = new AtomService(this.connections) - await atomService.deleteMany({ table, where: { userId: id, type } }) + await this.models.deleteMany({ table, where: { userId: id, type } }) } public findRestrictedUsersAndCount = async ({ @@ -2205,7 +2184,7 @@ export class UserService extends BaseService { // insert record into punish_record if (typeof banDays === 'number') { const expiredAt = getPunishExpiredDate(banDays) - await this.baseCreate( + await this.baseCreate( { userId, state: USER_STATE.banned, @@ -2360,7 +2339,6 @@ export class UserService extends BaseService { throw new CryptoWalletExistsError('eth address already has a user') } const updatedUser = await this.baseUpdate(userId, { - updatedAt: this.knex.fn.now(), ethAddress: ethAddress.toLowerCase(), // save the lower case ones }) @@ -2369,7 +2347,7 @@ export class UserService extends BaseService { await atomService.update({ table: 'crypto_wallet', where: { userId, archived: false }, - data: { updatedAt: this.knex.fn.now(), archived: true }, + data: { archived: true }, }) return updatedUser @@ -2397,7 +2375,7 @@ export class UserService extends BaseService { } private _removeWallet = async (userId: string): Promise => { - const user = await this.loadById(userId) + const user = await this.models.userIdLoader.load(userId) if (!user.ethAddress) { throw new ActionFailedError('user does not have a wallet') } @@ -2510,7 +2488,7 @@ export class UserService extends BaseService { }) let user if (socialAcount) { - user = await this.loadById(socialAcount.userId) + user = await this.models.userIdLoader.load(socialAcount.userId) if (user && user.state === USER_STATE.archived) { throw new ForbiddenByStateError('social account is archived') } @@ -2636,7 +2614,7 @@ export class UserService extends BaseService { providerAccountId, }) if (socialAccount) { - const user = await this.loadById(socialAccount.userId) + const user = await this.models.userIdLoader.load(socialAccount.userId) if (user.state === USER_STATE.archived) { throw new ForbiddenByStateError('social account is archived') } @@ -2947,7 +2925,7 @@ export class UserService extends BaseService { } private countLoginMethods = async (userId: string) => { - const user = await this.loadById(userId) + const user = await this.models.userIdLoader.load(userId) const email = user.email ? 1 : 0 const wallet = user.ethAddress ? 1 : 0 const socialAccounts = await this.findSocialAccountsByUserId(userId) diff --git a/src/definitions/action.d.ts b/src/definitions/action.d.ts new file mode 100644 index 000000000..59ec1bc9d --- /dev/null +++ b/src/definitions/action.d.ts @@ -0,0 +1,43 @@ +import { + CIRCLE_ACTION, + TAG_ACTION, + USER_ACTION, + ARTICLE_ACTION, +} from 'common/enums' + +export interface ActionCircle { + id: string + action: CIRCLE_ACTION + userId: string + targetId: string + createdAt: Date + updatedAt: Date +} + +export interface ActionTag { + id: string + action: TAG_ACTION + userId: string + targetId: string + createdAt: Date + updatedAt: Date +} + +export interface ActionArticle { + id: string + action: ARTICLE_ACTION + userId: string + targetId: string + articleVersionId: string + createdAt: Date + updatedAt: Date +} + +export interface ActionUser { + id: string + action: USER_ACTION + userId: string + targetId: string + createdAt: Date + updatedAt: Date +} diff --git a/src/definitions/announcement.d.ts b/src/definitions/announcement.d.ts new file mode 100644 index 000000000..750e742af --- /dev/null +++ b/src/definitions/announcement.d.ts @@ -0,0 +1,25 @@ +export interface Announcement { + id: string + title: string + cover: string | null + content: string | null + link: string | null + type: 'community' | 'product' | 'seminar' + visible: boolean + order: number + createdAt: Date + updatedAt: Date + expiredAt: Date | null +} + +export interface AnnouncementTranslation { + id: string + announcementId: string + language: string + title: string + cover: string | null + content: string | null + link: string | null + createdAt: Date + updatedAt: Date +} diff --git a/src/definitions/article.d.ts b/src/definitions/article.d.ts index 96d904395..1d388aa7b 100644 --- a/src/definitions/article.d.ts +++ b/src/definitions/article.d.ts @@ -1,28 +1,134 @@ -import { ARTICLE_STATE } from 'common/enums' +import { + ARTICLE_STATE, + ARTICLE_ACCESS_TYPE, + ARTICLE_LICENSE_TYPE, +} from 'common/enums' import { LANGUAGES } from './language' export interface Article { id: string - uuid: string authorId: string + state: ARTICLE_STATE + revisionCount: number + sensitiveByAdmin: boolean + pinned: boolean + pinnedAt: Date | null + createdAt: Date + updatedAt: Date + remark: string | null + shortHash: string +} + +export interface ArticleVersion { + id: string + articleId: string title: string - slug: string - cover?: string + cover: string | null summary: string - wordCount: string + contentId: string + contentMdId: string | null + summaryCustomized: boolean + wordCount: number dataHash: string mediaHash: string - content: string - state: ARTICLE_STATE - public: boolean - live: boolean - createdAt: string - updatedAt: string - draftId: string - remark?: string + tags: string[] + connections: string[] + access: keyof typeof ARTICLE_ACCESS_TYPE + license: keyof typeof ARTICLE_LICENSE_TYPE + replyToDonator: string | null + requestForDonation: string | null + canComment: boolean + sensitiveByAuthor: boolean sticky: boolean - language?: LANGUAGES - revisionCount: string - iscnId?: string + language: string | null + iscnId: string | null + circleId: string | null + description: string | null + createdAt: Date + updatedAt: Date +} + +export interface ArticleContent { + id: string + content: string + hash: string +} + +export interface ArticleCircle { + id: string + articleId: string + circleId: string + access: keyof typeof ARTICLE_ACCESS_TYPE + secret?: string + createdAt: Date + updatedAt: Date +} + +export interface ArticleConnection { + id: string + entranceId: string + articleId: string + order: number + createdAt: Date +} + +export interface ArticleTag { + id: string + articleId: string + tagId: string + selected: boolean | null + creator: string | null + createdAt: Date + updatedAt: Date +} + +export interface ArticleRecommendSetting { + id: string + articleId: string + inHottest: boolean + inNewest: boolean +} + +export interface ArticleBoost { + id: string + articleId: string + boost: number + createdAt: Date + updatedAt: Date +} + +export interface ArticleTranslation { + id: string + articleId: string + articleVersionId: string + language: string + title: string + content: string + summary: string | null + createdAt: Date + updatedAt: Date +} + +export interface ArticleCountView { + id + commentsTotal + commenters7d + commenters1d + recentCommentSince + score +} + +export interface ArticleReadTimeMaterialized { + id: string + articleId: string + sumReadTime: string +} + +export interface RecommendedArticlesFromReadTagsMaterialized { + id: string + userId: string + articleId: string + tagsBased: string[] + score: number } diff --git a/src/definitions/asset.d.ts b/src/definitions/asset.d.ts index 511bc0e62..36e2825c3 100644 --- a/src/definitions/asset.d.ts +++ b/src/definitions/asset.d.ts @@ -1,4 +1,16 @@ +import { ASSET_TYPE } from 'common/enums' + export interface Asset { + id: string uuid: string path: string + type: ASSET_TYPE + authorId: string | null +} + +export interface AssetMap { + id: string + assetId: string + entityTypeId: string + entityId: string } diff --git a/src/definitions/auth.d.ts b/src/definitions/auth.d.ts new file mode 100644 index 000000000..37edb12cf --- /dev/null +++ b/src/definitions/auth.d.ts @@ -0,0 +1,16 @@ +import { VERIFICATION_CODE_STATUS, VERIFICATION_CODE_TYPE } from 'common/enums' + +export interface VerificationCode { + id: string + uuid: string + expiredAt: Date | null + verifiedAt: Date | null + usedAt: Date | null + code: string + type: keyof typeof VERIFICATION_CODE_TYPE + status: VERIFICATION_CODE_STATUS + userId: stiring | null + email: string | null + createdAt: Date + updatedAt: Date +} diff --git a/src/definitions/base.d.ts b/src/definitions/base.d.ts new file mode 100644 index 000000000..f1666b105 --- /dev/null +++ b/src/definitions/base.d.ts @@ -0,0 +1,3 @@ +export interface BaseDBSchema { + id: string +} diff --git a/src/definitions/circle.d.ts b/src/definitions/circle.d.ts index d191b038b..8a90573ff 100644 --- a/src/definitions/circle.d.ts +++ b/src/definitions/circle.d.ts @@ -1,5 +1,11 @@ -import { CIRCLE_STATE, INVITATION_STATE } from 'common/enums' +import { + CIRCLE_STATE, + INVITATION_STATE, + SUBSCRIPTION_STATE, + PAYMENT_PROVIDER, +} from 'common/enums' +import { valueof } from './generic' import { User } from './user' export interface Circle { @@ -13,18 +19,58 @@ export interface Circle { description?: string provider: string providerProductId: string - createdAt: string - updatedAt: string + createdAt: Date + updatedAt: Date +} +export interface CirclePrice { + id: string + amount: string + state: CIRCLE_STATE + currency: 'HKD' | 'LIKE' + circleId: string + provider: PAYMENT_PROVIDER + providerPriceId: string + createdAt: Date + updatedAt: Date +} + +export interface CircleSubscription { + id: string + userId: string + state: valueof + provider: string + providerSubscriptionId: string + canceledAt: Date | null + createdAt: Date + updatedAt: Date +} + +export interface CircleSubscriptionItem { + id: string + userId: string + subscriptionId + priceId: string + provider: 'stripe' | 'matters' + providerSubscriptionItemId: string + archived: bollean + remark: string | null + canceledAt: Date | null + createdAt: Date + updatedAt: Date } export interface CircleInvitation { id: string - userId?: string - circleId: string + userId: string | null + email: string | null inviter: string - email: string + circleId: string + sentAt: Date + acceptedAt: Date | null durationInDays: number + subscriptionItemId: string | null state: INVITATION_STATE + createdAt: Date } export type CircleMember = User & { circleId: string } diff --git a/src/definitions/collection.d.ts b/src/definitions/collection.d.ts index 9c6d1a924..7b915ae4c 100644 --- a/src/definitions/collection.d.ts +++ b/src/definitions/collection.d.ts @@ -5,13 +5,12 @@ export interface Collection { cover?: string description?: string pinned: boolean - pinnedAt: string - createdAt: string - updatedAt: string + pinnedAt: Date + createdAt: Date + updatedAt: Date } export interface CollectionArticle { articleId: string - draftId: string order: string } diff --git a/src/definitions/comment.d.ts b/src/definitions/comment.d.ts index c1221785c..2af38cb01 100644 --- a/src/definitions/comment.d.ts +++ b/src/definitions/comment.d.ts @@ -1,23 +1,52 @@ +import type { ValueOf } from './generic' + import { COMMENT_STATE, COMMENT_TYPE } from 'common/enums' export interface Comment { id: string uuid: string authorId: string - articleId?: string - parentCommentId?: string - content?: string - state: COMMENT_STATE + articleId: string | null + articleVersionId: string | null + parentCommentId: string | null + content: string | null + state: ValueOf + pinned: boolean + quotationStart: number | null + quotationEnd: number | null + quotationContent: string | null + replyTo: string | null + remark: string | null + targetId: string + targetTypeId: string + type: ValueOf + pinnedAt: Date | null + createdAt: Date + updatedAt: Date +} + +export interface FeaturedCommentMaterialized { + id: string + uuid: string + authorId: string + articleId: string + parentCommentId: string + content: string + state: string pinned: boolean - createdAt: string - updatedAt: string - quotationStart?: number - quotationEnd?: number - quotationContent?: string - replyTo?: string - remark?: string + quotationStart: number + quotationEnd: number + quotationContent: string + remark: string + replyTo: string targetId: string targetTypeId: string - type: COMMENT_STATE - pinnedAt: string + type: string + upvotedId: string + upvoteCount: string + downvotedId: string + downvoteCount: string + score: number + createdAt: Date + updatedAt: Date } diff --git a/src/definitions/draft.d.ts b/src/definitions/draft.d.ts index ef2193a5d..31628f832 100644 --- a/src/definitions/draft.d.ts +++ b/src/definitions/draft.d.ts @@ -1,32 +1,36 @@ -import { PIN_STATE, PUBLISH_STATE } from 'common/enums' +import { + PUBLISH_STATE, + ARTICLE_ACCESS_TYPE, + ARTICLE_LICENSE_TYPE, +} from 'common/enums' import { LANGUAGES } from './language' export interface Draft { id: string - uuid: string authorId: string title: string - cover?: string - summary?: string - summaryCustomized: boolean - wordCount?: string - dataHash: string - mediaHash: string + cover: string | null + summary: string | null content: string - contentMd?: string - createdAt: string - updatedAt: string - articleId: string - circleId?: string - collection?: string[] - tags?: string[] - remark?: string + contentMd: string | null + dataHash: string | null + mediaHash: string | null + circleId: string | null + collection: string[] | null + tags: string[] + language: LANGUAGES | null + access: keyof typeof ARTICLE_ACCESS_TYPE + license: keyof typeof ARTICLE_LICENSE_TYPE + replyToDonator: string | null + requestForDonation: string | null + canComment: boolean + iscnPublish: boolean | null + remark: string | null publishState: PUBLISH_STATE - pinState: PIN_STATE + articleId: string | null + sensitiveByAuthor: boolean archived: boolean - language?: LANGUAGES - replyToDonator?: string - requestForDonation?: string - iscnId?: string + createdAt: Date + updatedAt: Date } diff --git a/src/definitions/index.d.ts b/src/definitions/index.d.ts index c78250fc2..d7c20a88f 100644 --- a/src/definitions/index.d.ts +++ b/src/definitions/index.d.ts @@ -1,6 +1,5 @@ import type { BasedContext } from '@apollo/server' import type { - Alchemy, ArticleService, AtomService, CommentService, @@ -15,6 +14,7 @@ import type { CollectionService, LikeCoin, ExchangeRate, + RecommendationService, } from 'connectors' import type { PublicationQueue, @@ -26,19 +26,16 @@ import type { PayToByMattersQueue, PayoutQueue, UserQueue, -} from 'connectors/queues' +} from 'connectors/queue' import type { Request, Response } from 'express' import type { Redis } from 'ioredis' import type { Knex } from 'knex' -import { - PAYMENT_CURRENCY, - PAYMENT_PROVIDER, - TRANSACTION_STATE, - TRANSACTION_PURPOSE, - VERIFICATION_CODE_STATUS, -} from 'common/enums' - +export * from './base' +export * from './announcement' +export * from './auth' +export * from './action' +export * from './oauth' export * from './user' export * from './article' export * from './draft' @@ -47,13 +44,15 @@ export * from './circle' export * from './collection' export * from './comment' export * from './language' -export * from './schema' export * from './notification' export * from './generic' export * from './payment' export * from './appreciation' export * from './asset' -export * from './topic' +export * from './report' +export * from './wallet' +export * from './misc' +export * from './schema' export interface Context extends BasedContext { viewer: Viewer @@ -82,6 +81,7 @@ export interface DataSources { paymentService: PaymentService openseaService: OpenSeaService collectionService: CollectionService + recommendationService: RecommendationService likecoin: LikeCoin exchangeRate: ExchangeRate connections: Connections @@ -129,7 +129,6 @@ export type BasicTableName = | 'notice_entity' | 'push_device' | 'report' - | 'report_asset' | 'feedback' | 'feedback_asset' | 'invitation' @@ -166,10 +165,6 @@ export type BasicTableName = | 'seeding_user' | 'announcement' | 'announcement_translation' - | 'topic' - | 'article_topic' - | 'chapter' - | 'article_chapter' | 'crypto_wallet' | 'crypto_wallet_signature' | 'article_translation' @@ -182,6 +177,9 @@ export type BasicTableName = | 'collection' | 'collection_article' | 'social_account' + | 'article_content' + | 'article_version' + | 'matters_choice_topic' export type View = | 'tag_count_view' @@ -210,6 +208,11 @@ export type MaterializedView = export type TableName = BasicTableName | View | MaterializedView +export interface EntityType { + id: string + table: TableName +} + export interface ThirdPartyAccount { accountName: 'facebook' | 'wechat' | 'google' baseUrl: string @@ -243,50 +246,3 @@ export type TransactionTargetType = 'Article' | 'Transaction' export type Falsey = '' | 0 | false | null | undefined export type SkippedListItemType = 'agent_hash' | 'email' | 'domain' - -/** - * Payment - */ -export interface Customer { - id: string - userId: string - provider: string - customerId: string - cardLast4: string -} - -export interface CircleSubscription { - id: string - state: string - userId: string - provider: string - providerSubscriptionId: string -} - -export interface CirclePrice { - id: string - amount: number - currency: PAYMENT_CURRENCY - circleId: string - provider: PAYMENT_PROVIDER - providerPriceId: string -} - -export interface Transaction { - id: string - amount: string - currency: PAYMENT_CURRENCY - state: TRANSACTION_STATE - purpose: TRANSACTION_PURPOSE - provider: PAYMENT_PROVIDER - providerTxId: string - senderId: string - recipientId: string - targetId: string - targetType: string - fee: string - remark: string - parentId: string - createdAt: string - updatedAt: string -} diff --git a/src/definitions/misc.d.ts b/src/definitions/misc.d.ts new file mode 100644 index 000000000..fb1f641b5 --- /dev/null +++ b/src/definitions/misc.d.ts @@ -0,0 +1,67 @@ +import type { ValueOf } from './generic' + +import { + SKIPPED_LIST_ITEM_TYPES, + MATTERS_CHOICE_TOPIC_STATE, +} from 'common/enums' + +export interface PunishRecord { + id: string + userId: string + state: 'banned' + archived: boolean + expiredAt: Date + createdAt: Date + updatedAt: Date +} + +export interface MattersChoice { + id: string + articleId: string + createdAt: Date + updatedAt: Date +} + +export interface MattersChoiceTopic { + id: string + title: string + articles: string[] + pinAmount: number + note: string | null + state: KeyOf + publishedAt: Date | null + createdAt: Date + updatedAt: Date +} + +export interface Blocklist { + id: string + uuid: string + type: ValueOf + value: string + archived: boolean + note: string | null + createdAt: Date + updatedAt: Date +} + +export interface SearchHistory { + id: string + userId: string | null + searchKey: string + archived: boolean | null + createdAt: Date +} + +export interface BlockedSearchKeyword { + id: string + searchKey: string + createdAt: Date +} + +export interface LogRecord { + id: string + type: string + userId: string + readAt: Date +} diff --git a/src/definitions/notification.d.ts b/src/definitions/notification.d.ts index e355b0eb7..02b42f1aa 100644 --- a/src/definitions/notification.d.ts +++ b/src/definitions/notification.d.ts @@ -397,3 +397,24 @@ export interface PutNoticeParams { mergeData: boolean // used by circle bundled notice } } + +// DB schema + +export interface Notice { + id: string + uuid: string + unread: boolean + deleted: boolean + noticeDetailId: string + recipientId: string + createdAt: Date + updatedAt: Date +} + +export interface NoticeDetail { + id: string + noticeType: string + message: string + data: any + createdAt: Date +} diff --git a/src/definitions/oauth.d.ts b/src/definitions/oauth.d.ts new file mode 100644 index 000000000..30c9bd1db --- /dev/null +++ b/src/definitions/oauth.d.ts @@ -0,0 +1,107 @@ +import { User } from './user' + +export type UserOAuthLikeCoinAccountType = 'temporal' | 'general' + +export interface OAuthClientDB { + id: string + userId: string | null + avatar: string | null + clientId: string + redirectUri: string[] + grantTypes: string[] + scope: string[] +} + +export interface UserOauthLikecoinDB { + id: string + likerId: string + accountType: string + accessToken: string + refreshToken: string | null + expires: Date + scope: string[] + pendingLike + createdAt: Date + updatedAt: Date +} + +export interface OAuthAccessTokenDB { + id: string + token: string + clientId: string + userId: string + scope: string[] + expires: Date + createdAt: Date + updatedAt: Date +} + +export interface OAuthRefreshTokenDB { + id: string + token: string + clientId: string + userId: string + scope: string[] | null + expires: Date + createdAt: Date + updatedAt: Date +} + +export interface OAuthAuthorizationCodeDB { + id: string + code: string + clientId: string + userId: string + scope: string[] + expires: Date + redirectUri: string + createdAt: Date + updatedAt: Date +} + +export interface UserOAuthLikeCoin { + likerId: string + accountType: UserOAuthLikeCoinAccountType + accessToken: string + refreshToken: string + expires: Date + scope: string | string[] +} + +export interface OAuthClient { + id: string + redirectUris?: string | string[] + grants: string | string[] + scope: string[] + accessTokenLifetime?: number + refreshTokenLifetime?: number + rawClient: OAuthClientDB +} + +export interface OAuthAuthorizationCode { + authorizationCode: string + expiresAt: Date + redirectUri: string + scope?: string | string[] + client: OAuthClient + user: User +} + +export interface OAuthToken { + accessToken: string + accessTokenExpiresAt?: Date + refreshToken?: string + refreshTokenExpiresAt?: Date + scope: string | string[] + client: OAuthClient + user: User + id_token?: string +} + +export interface OAuthRefreshToken { + refreshToken: string + refreshTokenExpiresAt?: Date + scope?: string | string[] + client: OAuthClient + user: User +} diff --git a/src/definitions/payment.d.ts b/src/definitions/payment.d.ts index b687eaa3d..24adf37f4 100644 --- a/src/definitions/payment.d.ts +++ b/src/definitions/payment.d.ts @@ -1,5 +1,73 @@ +import { + PAYMENT_CURRENCY, + PAYMENT_PROVIDER, + TRANSACTION_STATE, + TRANSACTION_PURPOSE, +} from 'common/enums' + export interface PayoutAccount { id: string userId: string accountId: string + provider: 'stripe' + type: 'express' | 'standard' + archived: boolean + capabilitiesTransfers: boolean + country: string + currency: string + createdAt: Date + updatedAt: Date +} + +export interface Transaction { + id: string + amount: string + currency: PAYMENT_CURRENCY + state: TRANSACTION_STATE + purpose: TRANSACTION_PURPOSE + provider: PAYMENT_PROVIDER + providerTxId: string + senderId: string | null + recipientId: string + targetId: string + articleVersionId: string | null + targetType: string + fee: string + remark: string + parentId: string + createdAt: Date + updatedAt: Date +} + +export interface BlockchainTransaction { + id: string + transactionId: string | null + chainId: string + txHash: `0x${string}` + state: BLOCKCHAIN_TRANSACTION_STATE + from: `0x${string}` | null + to: `0x${string}` | null + blockNumber: string + createdAt: Date + updatedAt: Date +} + +export interface BlockchainSyncRecord { + id: string + chainId: string + contractAddress: string + blockNumber: string + createdAt: Date + updatedAt: Date +} + +export interface Customer { + id: string + userId: string + provider: string + customerId: string + cardLast4: string + // used as params only, `catLast4` will transform to `card_last4` in knex, not the `card_last_4` in actual + card_last_4: string + archived: boolean } diff --git a/src/definitions/report.d.ts b/src/definitions/report.d.ts new file mode 100644 index 000000000..98f4d19b3 --- /dev/null +++ b/src/definitions/report.d.ts @@ -0,0 +1,19 @@ +import { NODE_TYPES } from 'common/enums' + +export type ReportType = NODE_TYPES.Article | NODE_TYPES.Comment + +export type ReportReason = + | 'tort' + | 'illegal_advertising' + | 'discrimination_insult_hatred' + | 'pornography_involving_minors' + | 'other' + +export interface Report { + id: string + reporterId: string + articleId?: string + commentId?: string + reason: ReportReason + createdAt: Date +} diff --git a/src/definitions/schema.d.ts b/src/definitions/schema.d.ts index e15761f55..58aa401b5 100644 --- a/src/definitions/schema.d.ts +++ b/src/definitions/schema.d.ts @@ -11,6 +11,10 @@ import { import { Tag as TagModel } from './tag' import { Collection as CollectionModel } from './collection' import { Comment as CommentModel } from './comment' +import { + Article as ArticleModel, + ArticleVersion as ArticleVersionModel, +} from './article' import { Draft as DraftModel } from './draft' import { Circle as CircleModel, @@ -26,7 +30,8 @@ import { PayoutAccount as PayoutAccountModel } from './payment' import { Asset as AssetModel } from './asset' import { NoticeItem as NoticeItemModel } from './notification' import { Appreciation as AppreciationModel } from './appreciation' -import { Topic as TopicModel } from './topic' +import { Report as ReportModel } from './report' +import { MattersChoiceTopic as MattersChoiceTopicModel } from './misc' export type Maybe = T | null export type InputMaybe = T | undefined export type Exact = { @@ -197,6 +202,8 @@ export type GQLArticle = GQLNode & createdAt: Scalars['DateTime']['output'] /** IPFS hash of this article. */ dataHash: Scalars['String']['output'] + /** whether current viewer has donated to this article */ + donated: Scalars['Boolean']['output'] /** Total number of donation recieved of this article. */ donationCount: Scalars['Int']['output'] /** Donations of this article, grouped by sender */ @@ -257,6 +264,8 @@ export type GQLArticle = GQLNode & sensitiveByAdmin: Scalars['Boolean']['output'] /** whether content is marked as sensitive by author */ sensitiveByAuthor: Scalars['Boolean']['output'] + /** Short hash for shorter url addressing */ + shortHash: Scalars['String']['output'] /** Slugified article title. */ slug: Scalars['String']['output'] /** State of this article. */ @@ -284,6 +293,8 @@ export type GQLArticle = GQLNode & transactionsReceivedBy: GQLUserConnection /** Translation of article title and content. */ translation?: Maybe + /** history versions */ + versions: GQLArticleVersionsConnection /** Word count of this article. */ wordCount?: Maybe } @@ -384,6 +395,14 @@ export type GQLArticleTranslationArgs = { input?: InputMaybe } +/** + * This type contains metadata, content, hash and related data of an article. If you + * want information about article's comments. Please check Comment type. + */ +export type GQLArticleVersionsArgs = { + input: GQLArticleVersionsInput +} + export type GQLArticleAccess = { __typename?: 'ArticleAccess' circle?: Maybe @@ -452,7 +471,8 @@ export type GQLArticleEdge = { } export type GQLArticleInput = { - mediaHash: Scalars['String']['input'] + mediaHash?: InputMaybe + shortHash?: InputMaybe } /** Enums for types of article license */ @@ -517,6 +537,41 @@ export type GQLArticleTranslation = { title?: Maybe } +export type GQLArticleVersion = GQLNode & { + __typename?: 'ArticleVersion' + contents: GQLArticleContents + createdAt: Scalars['DateTime']['output'] + dataHash?: Maybe + description?: Maybe + id: Scalars['ID']['output'] + mediaHash?: Maybe + summary: Scalars['String']['output'] + title: Scalars['String']['output'] + translation?: Maybe +} + +export type GQLArticleVersionTranslationArgs = { + input?: InputMaybe +} + +export type GQLArticleVersionEdge = { + __typename?: 'ArticleVersionEdge' + cursor: Scalars['String']['output'] + node: GQLArticleVersion +} + +export type GQLArticleVersionsConnection = GQLConnection & { + __typename?: 'ArticleVersionsConnection' + edges: Array> + pageInfo: GQLPageInfo + totalCount: Scalars['Int']['output'] +} + +export type GQLArticleVersionsInput = { + after?: InputMaybe + first?: InputMaybe +} + /** This type contains type, link and related data of an asset. */ export type GQLAsset = { __typename?: 'Asset' @@ -545,7 +600,6 @@ export type GQLAssetType = | 'oauthClientAvatar' | 'profileCover' | 'tagCover' - | 'topicCover' export type GQLAuthResult = { __typename?: 'AuthResult' @@ -613,23 +667,6 @@ export type GQLChangeEmailInput = { oldEmailCodeId: Scalars['ID']['input'] } -/** This type contains metadata, content and related data of Chapter type, which is a container for Article type. A Chapter belong to a Topic. */ -export type GQLChapter = GQLNode & { - __typename?: 'Chapter' - /** Number articles included in this chapter. */ - articleCount: Scalars['Int']['output'] - /** Articles included in this Chapter */ - articles?: Maybe> - /** Description of this chapter. */ - description?: Maybe - /** Unique id of this chapter. */ - id: Scalars['ID']['output'] - /** Title of this chapter. */ - title: Scalars['String']['output'] - /** The topic that this Chapter belongs to. */ - topic: GQLTopic -} - export type GQLCircle = GQLNode & { __typename?: 'Circle' /** Analytics dashboard. */ @@ -1120,10 +1157,6 @@ export type GQLDeleteTagsInput = { ids: Array } -export type GQLDeleteTopicsInput = { - ids: Array -} - export type GQLDirectImageUploadInput = { draft?: InputMaybe entityId?: InputMaybe @@ -1216,6 +1249,8 @@ export type GQLEditArticleInput = { collection?: InputMaybe> content?: InputMaybe cover?: InputMaybe + /** revision description */ + description?: InputMaybe id: Scalars['ID']['input'] /** whether publish to ISCN */ iscnPublish?: InputMaybe @@ -1229,6 +1264,7 @@ export type GQLEditArticleInput = { sticky?: InputMaybe summary?: InputMaybe tags?: InputMaybe> + title?: InputMaybe } export type GQLEmailLoginInput = { @@ -1246,7 +1282,6 @@ export type GQLEntityType = | 'collection' | 'draft' | 'tag' - | 'topic' | 'user' export type GQLExchangeRate = { @@ -1297,8 +1332,6 @@ export type GQLFilterInput = { followed?: InputMaybe inRangeEnd?: InputMaybe inRangeStart?: InputMaybe - /** Used in User.topics */ - public?: InputMaybe /** index of list, min: 0, max: 49 */ random?: InputMaybe /** Used in User Articles filter, by tags or by time range, or both */ @@ -1358,6 +1391,33 @@ export type GQLGenerateSigningMessageInput = { export type GQLGrantType = 'authorization_code' | 'refresh_token' +export type GQLIcymiTopic = GQLNode & { + __typename?: 'IcymiTopic' + archivedAt?: Maybe + articles: Array + id: Scalars['ID']['output'] + note?: Maybe + pinAmount: Scalars['Int']['output'] + publishedAt?: Maybe + state: GQLIcymiTopicState + title: Scalars['String']['output'] +} + +export type GQLIcymiTopicConnection = GQLConnection & { + __typename?: 'IcymiTopicConnection' + edges: Array + pageInfo: GQLPageInfo + totalCount: Scalars['Int']['output'] +} + +export type GQLIcymiTopicEdge = { + __typename?: 'IcymiTopicEdge' + cursor: Scalars['String']['output'] + node: GQLIcymiTopic +} + +export type GQLIcymiTopicState = 'archived' | 'editing' | 'published' + export type GQLInvitation = { __typename?: 'Invitation' /** Accepted time. */ @@ -1543,8 +1603,6 @@ export type GQLMutation = { /** Remove a draft. */ deleteDraft?: Maybe deleteTags?: Maybe - /** Delete topics */ - deleteTopics: Scalars['Boolean']['output'] directImageUpload: GQLAsset /** Edit an article. */ editArticle: GQLArticle @@ -1574,8 +1632,6 @@ export type GQLMutation = { /** Publish an article onto IPFS. */ publishArticle: GQLDraft putAnnouncement: GQLAnnouncement - /** Create a Chapter when no id is given, update fields when id is given. Throw error if no id & no title, or no id & no topic. */ - putChapter: GQLChapter /** Create or update a Circle. */ putCircle: GQLCircle /** @@ -1590,6 +1646,7 @@ export type GQLMutation = { putDraft: GQLDraft /** update tags for showing on profile page */ putFeaturedTags?: Maybe> + putIcymiTopic?: Maybe /** Create or Update an OAuth Client, used in OSS. */ putOAuthClient?: Maybe putRemark?: Maybe @@ -1597,8 +1654,6 @@ export type GQLMutation = { putSkippedListItem?: Maybe> /** Create or update tag. */ putTag: GQLTag - /** Create a Topic when no id is given, update fields when id is given. Throw error if no id & no title. */ - putTopic: GQLTopic /** Read an article. */ readArticle: GQLArticle /** Update state of a user, used in OSS. */ @@ -1635,8 +1690,8 @@ export type GQLMutation = { singleFileUpload: GQLAsset /** Login/Signup via social accounts. */ socialLogin: GQLAuthResult - /** Sort topics */ - sortTopics: Array + /** Submit inappropriate content report */ + submitReport: GQLReport /** Subscribe a Circle. */ subscribeCircle: GQLSubscribeCircleResult toggleArticleRecommend: GQLArticle @@ -1785,10 +1840,6 @@ export type GQLMutationDeleteTagsArgs = { input: GQLDeleteTagsInput } -export type GQLMutationDeleteTopicsArgs = { - input: GQLDeleteTopicsInput -} - export type GQLMutationDirectImageUploadArgs = { input: GQLDirectImageUploadInput } @@ -1841,10 +1892,6 @@ export type GQLMutationPutAnnouncementArgs = { input: GQLPutAnnouncementInput } -export type GQLMutationPutChapterArgs = { - input: GQLPutChapterInput -} - export type GQLMutationPutCircleArgs = { input: GQLPutCircleInput } @@ -1869,6 +1916,10 @@ export type GQLMutationPutFeaturedTagsArgs = { input: GQLFeaturedTagsInput } +export type GQLMutationPutIcymiTopicArgs = { + input: GQLPutIcymiTopicInput +} + export type GQLMutationPutOAuthClientArgs = { input: GQLPutOAuthClientInput } @@ -1889,10 +1940,6 @@ export type GQLMutationPutTagArgs = { input: GQLPutTagInput } -export type GQLMutationPutTopicArgs = { - input: GQLPutTopicInput -} - export type GQLMutationReadArticleArgs = { input: GQLReadArticleInput } @@ -1961,8 +2008,8 @@ export type GQLMutationSocialLoginArgs = { input: GQLSocialLoginInput } -export type GQLMutationSortTopicsArgs = { - input: GQLSortTopicsInput +export type GQLMutationSubmitReportArgs = { + input: GQLSubmitReportInput } export type GQLMutationSubscribeCircleArgs = { @@ -2235,7 +2282,9 @@ export type GQLOss = { articles: GQLArticleConnection badgedUsers: GQLUserConnection comments: GQLCommentConnection + icymiTopics: GQLIcymiTopicConnection oauthClients: GQLOAuthClientConnection + reports: GQLReportConnection restrictedUsers: GQLUserConnection seedingUsers: GQLUserConnection skippedListItems: GQLSkippedListItemsConnection @@ -2255,10 +2304,18 @@ export type GQLOssCommentsArgs = { input: GQLConnectionArgs } +export type GQLOssIcymiTopicsArgs = { + input: GQLConnectionArgs +} + export type GQLOssOauthClientsArgs = { input: GQLConnectionArgs } +export type GQLOssReportsArgs = { + input: GQLConnectionArgs +} + export type GQLOssRestrictedUsersArgs = { input: GQLConnectionArgs } @@ -2410,14 +2467,6 @@ export type GQLPutAnnouncementInput = { visible?: InputMaybe } -export type GQLPutChapterInput = { - articles?: InputMaybe> - description?: InputMaybe - id?: InputMaybe - title?: InputMaybe - topic?: InputMaybe -} - export type GQLPutCircleArticlesInput = { /** Access Type, `public` or `paywall` only. */ accessType: GQLArticleAccessType @@ -2482,6 +2531,15 @@ export type GQLPutDraftInput = { title?: InputMaybe } +export type GQLPutIcymiTopicInput = { + articles?: InputMaybe> + id?: InputMaybe + note?: InputMaybe + pinAmount?: InputMaybe + state?: InputMaybe + title?: InputMaybe +} + export type GQLPutOAuthClientInput = { avatar?: InputMaybe description?: InputMaybe @@ -2520,16 +2578,6 @@ export type GQLPutTagInput = { id?: InputMaybe } -export type GQLPutTopicInput = { - articles?: InputMaybe> - chapters?: InputMaybe> - cover?: InputMaybe - description?: InputMaybe - id?: InputMaybe - public?: InputMaybe - title?: InputMaybe -} - export type GQLQuery = { __typename?: 'Query' article?: Maybe @@ -2646,6 +2694,8 @@ export type GQLRecommendation = { hottestTags: GQLTagConnection /** 'In case you missed it' recommendation. */ icymi: GQLArticleConnection + /** 'In case you missed it' topic. */ + icymiTopic?: Maybe /** Global articles sort by publish time. */ newest: GQLArticleConnection /** Global circles sort by created time. */ @@ -2747,6 +2797,35 @@ export type GQLReorderMoveInput = { newPosition: Scalars['Int']['input'] } +export type GQLReport = GQLNode & { + __typename?: 'Report' + createdAt: Scalars['DateTime']['output'] + id: Scalars['ID']['output'] + reason: GQLReportReason + reporter: GQLUser + target: GQLResponse +} + +export type GQLReportConnection = GQLConnection & { + __typename?: 'ReportConnection' + edges?: Maybe> + pageInfo: GQLPageInfo + totalCount: Scalars['Int']['output'] +} + +export type GQLReportEdge = { + __typename?: 'ReportEdge' + cursor: Scalars['String']['output'] + node: GQLReport +} + +export type GQLReportReason = + | 'discrimination_insult_hatred' + | 'illegal_advertising' + | 'other' + | 'pornography_involving_minors' + | 'tort' + export type GQLResetLikerIdInput = { id: Scalars['ID']['input'] } @@ -2960,10 +3039,6 @@ export type GQLSocialLoginInput = { type: GQLSocialAccountType } -export type GQLSortTopicsInput = { - ids: Array -} - export type GQLStripeAccount = { __typename?: 'StripeAccount' id: Scalars['ID']['output'] @@ -3004,6 +3079,11 @@ export type GQLStripeAccountCountry = | 'UnitedKingdom' | 'UnitedStates' +export type GQLSubmitReportInput = { + reason: GQLReportReason + targetId: Scalars['ID']['input'] +} + export type GQLSubscribeCircleInput = { /** Unique ID. */ id: Scalars['ID']['input'] @@ -3197,52 +3277,6 @@ export type GQLTopDonatorInput = { first?: InputMaybe } -/** This type contains metadata, content and related data of a topic, which is a container for Article and Chapter types. */ -export type GQLTopic = GQLNode & { - __typename?: 'Topic' - /** Number articles included in this topic. */ - articleCount: Scalars['Int']['output'] - /** List of articles included in this topic. */ - articles?: Maybe> - /** Author of this topic. */ - author: GQLUser - /** Number of chapters included in this topic. */ - chapterCount: Scalars['Int']['output'] - /** List of chapters included in this topic. */ - chapters?: Maybe> - /** Cover of this topic. */ - cover?: Maybe - /** Description of this topic. */ - description?: Maybe - /** Unique id of this topic. */ - id: Scalars['ID']['output'] - /** Latest published article on this topic */ - latestArticle?: Maybe - /** Whether this topic is public or not. */ - public: Scalars['Boolean']['output'] - /** Title of this topic. */ - title: Scalars['String']['output'] -} - -export type GQLTopicConnection = GQLConnection & { - __typename?: 'TopicConnection' - edges?: Maybe> - pageInfo: GQLPageInfo - totalCount: Scalars['Int']['output'] -} - -export type GQLTopicEdge = { - __typename?: 'TopicEdge' - cursor: Scalars['String']['output'] - node: GQLTopic -} - -export type GQLTopicInput = { - after?: InputMaybe - filter?: InputMaybe - first?: InputMaybe -} - export type GQLTransaction = { __typename?: 'Transaction' amount: Scalars['Float']['output'] @@ -3483,6 +3517,8 @@ export type GQLUser = GQLNode & { isFollowee: Scalars['Boolean']['output'] /** Whether current user is following viewer. */ isFollower: Scalars['Boolean']['output'] + /** user latest articles or collections */ + latestWorks: Array /** Liker info of current user */ liker: GQLLiker /** LikerID of LikeCoin, being used by LikeCoin OAuth */ @@ -3497,6 +3533,7 @@ export type GQLUser = GQLNode & { paymentPointer?: Maybe /** Tags pinned by current user. */ pinnedTags: GQLTagConnection + /** user pinned articles or collections */ pinnedWorks: Array /** Article recommendations for current user. */ recommendation: GQLRecommendation @@ -3511,8 +3548,6 @@ export type GQLUser = GQLNode & { subscriptions: GQLArticleConnection /** Tags by by usage order of current user. */ tags: GQLTagConnection - /** Topics created by current user. */ - topics: GQLTopicConnection /** Global unique user name of a user. */ userName?: Maybe /** User Wallet */ @@ -3567,10 +3602,6 @@ export type GQLUserTagsArgs = { input: GQLConnectionArgs } -export type GQLUserTopicsArgs = { - input: GQLTopicInput -} - export type GQLUserActivity = { __typename?: 'UserActivity' /** Appreciations current user received. */ @@ -4032,8 +4063,8 @@ export type GQLResolversUnionTypes> = nodes?: Maybe> }) Invitee: GQLPerson | UserModel - Response: DraftModel | CommentModel - TransactionTarget: DraftModel | CircleModel | TransactionModel + Response: ArticleModel | CommentModel + TransactionTarget: ArticleModel | CircleModel | TransactionModel }> /** Mapping of interface types */ @@ -4047,6 +4078,9 @@ export type GQLResolversInterfaceTypes< | (Omit & { edges?: Maybe> }) + | (Omit & { + edges: Array> + }) | (Omit & { edges?: Maybe> }) @@ -4060,6 +4094,9 @@ export type GQLResolversInterfaceTypes< edges?: Maybe> }) | GQLFollowingActivityConnection + | (Omit & { + edges: Array + }) | (Omit & { edges?: Maybe> }) @@ -4076,6 +4113,9 @@ export type GQLResolversInterfaceTypes< edges?: Maybe> }) | GQLRecentSearchConnection + | (Omit & { + edges?: Maybe> + }) | GQLResponseConnection | GQLSearchResultConnection | GQLSkippedListItemsConnection @@ -4085,9 +4125,6 @@ export type GQLResolversInterfaceTypes< | (Omit & { edges?: Maybe> }) - | (Omit & { - edges?: Maybe> - }) | (Omit & { edges?: Maybe> }) @@ -4095,17 +4132,15 @@ export type GQLResolversInterfaceTypes< edges?: Maybe> }) Node: - | DraftModel - | (Omit & { - articles?: Maybe> - topic: RefType['Topic'] - }) + | ArticleModel + | ArticleVersionModel | CircleModel | CollectionModel | CommentModel | DraftModel + | MattersChoiceTopicModel + | ReportModel | TagModel - | TopicModel | UserModel Notice: | NoticeItemModel @@ -4116,7 +4151,7 @@ export type GQLResolversInterfaceTypes< | NoticeItemModel | NoticeItemModel | NoticeItemModel - PinnableWork: DraftModel | CollectionModel + PinnableWork: ArticleModel | CollectionModel }> /** Mapping between all available schema types and the resolvers types */ @@ -4145,8 +4180,8 @@ export type GQLResolversTypes = ResolversObject<{ } > AppreciationPurpose: GQLAppreciationPurpose - Article: ResolverTypeWrapper - ArticleAccess: ResolverTypeWrapper + Article: ResolverTypeWrapper + ArticleAccess: ResolverTypeWrapper ArticleAccessType: GQLArticleAccessType ArticleArticleNotice: ResolverTypeWrapper ArticleArticleNoticeType: GQLArticleArticleNoticeType @@ -4155,7 +4190,7 @@ export type GQLResolversTypes = ResolversObject<{ edges?: Maybe> } > - ArticleContents: ResolverTypeWrapper + ArticleContents: ResolverTypeWrapper ArticleDonation: ResolverTypeWrapper< Omit & { sender?: Maybe @@ -4178,7 +4213,7 @@ export type GQLResolversTypes = ResolversObject<{ ArticleLicenseType: GQLArticleLicenseType ArticleNotice: ResolverTypeWrapper ArticleNoticeType: GQLArticleNoticeType - ArticleOSS: ResolverTypeWrapper + ArticleOSS: ResolverTypeWrapper ArticleRecommendationActivity: ResolverTypeWrapper< Omit & { nodes?: Maybe> @@ -4187,6 +4222,18 @@ export type GQLResolversTypes = ResolversObject<{ ArticleRecommendationActivitySource: GQLArticleRecommendationActivitySource ArticleState: GQLArticleState ArticleTranslation: ResolverTypeWrapper + ArticleVersion: ResolverTypeWrapper + ArticleVersionEdge: ResolverTypeWrapper< + Omit & { + node: GQLResolversTypes['ArticleVersion'] + } + > + ArticleVersionsConnection: ResolverTypeWrapper< + Omit & { + edges: Array> + } + > + ArticleVersionsInput: GQLArticleVersionsInput Asset: ResolverTypeWrapper AssetType: GQLAssetType AuthResult: ResolverTypeWrapper< @@ -4205,12 +4252,6 @@ export type GQLResolversTypes = ResolversObject<{ CacheControlScope: GQLCacheControlScope Chain: GQLChain ChangeEmailInput: GQLChangeEmailInput - Chapter: ResolverTypeWrapper< - Omit & { - articles?: Maybe> - topic: GQLResolversTypes['Topic'] - } - > Circle: ResolverTypeWrapper CircleAnalytics: ResolverTypeWrapper CircleConnection: ResolverTypeWrapper< @@ -4290,7 +4331,6 @@ export type GQLResolversTypes = ResolversObject<{ DeleteCommentInput: GQLDeleteCommentInput DeleteDraftInput: GQLDeleteDraftInput DeleteTagsInput: GQLDeleteTagsInput - DeleteTopicsInput: GQLDeleteTopicsInput DirectImageUploadInput: GQLDirectImageUploadInput Draft: ResolverTypeWrapper DraftAccess: ResolverTypeWrapper @@ -4328,6 +4368,16 @@ export type GQLResolversTypes = ResolversObject<{ GenerateSigningMessageInput: GQLGenerateSigningMessageInput GrantType: GQLGrantType ID: ResolverTypeWrapper + IcymiTopic: ResolverTypeWrapper + IcymiTopicConnection: ResolverTypeWrapper< + Omit & { + edges: Array + } + > + IcymiTopicEdge: ResolverTypeWrapper< + Omit & { node: GQLResolversTypes['IcymiTopic'] } + > + IcymiTopicState: GQLIcymiTopicState Int: ResolverTypeWrapper Invitation: ResolverTypeWrapper InvitationConnection: ResolverTypeWrapper< @@ -4399,7 +4449,9 @@ export type GQLResolversTypes = ResolversObject<{ | 'articles' | 'badgedUsers' | 'comments' + | 'icymiTopics' | 'oauthClients' + | 'reports' | 'restrictedUsers' | 'seedingUsers' | 'tags' @@ -4408,7 +4460,9 @@ export type GQLResolversTypes = ResolversObject<{ articles: GQLResolversTypes['ArticleConnection'] badgedUsers: GQLResolversTypes['UserConnection'] comments: GQLResolversTypes['CommentConnection'] + icymiTopics: GQLResolversTypes['IcymiTopicConnection'] oauthClients: GQLResolversTypes['OAuthClientConnection'] + reports: GQLResolversTypes['ReportConnection'] restrictedUsers: GQLResolversTypes['UserConnection'] seedingUsers: GQLResolversTypes['UserConnection'] tags: GQLResolversTypes['TagConnection'] @@ -4436,19 +4490,18 @@ export type GQLResolversTypes = ResolversObject<{ PublishArticleInput: GQLPublishArticleInput PublishState: GQLPublishState PutAnnouncementInput: GQLPutAnnouncementInput - PutChapterInput: GQLPutChapterInput PutCircleArticlesInput: GQLPutCircleArticlesInput PutCircleArticlesType: GQLPutCircleArticlesType PutCircleInput: GQLPutCircleInput PutCollectionInput: GQLPutCollectionInput PutCommentInput: GQLPutCommentInput PutDraftInput: GQLPutDraftInput + PutIcymiTopicInput: GQLPutIcymiTopicInput PutOAuthClientInput: GQLPutOAuthClientInput PutRemarkInput: GQLPutRemarkInput PutRestrictedUsersInput: GQLPutRestrictedUsersInput PutSkippedListItemInput: GQLPutSkippedListItemInput PutTagInput: GQLPutTagInput - PutTopicInput: GQLPutTopicInput Query: ResolverTypeWrapper<{}> QuoteCurrency: GQLQuoteCurrency ReadArticleInput: GQLReadArticleInput @@ -4477,6 +4530,16 @@ export type GQLResolversTypes = ResolversObject<{ RenameTagInput: GQLRenameTagInput ReorderCollectionArticlesInput: GQLReorderCollectionArticlesInput ReorderMoveInput: GQLReorderMoveInput + Report: ResolverTypeWrapper + ReportConnection: ResolverTypeWrapper< + Omit & { + edges?: Maybe> + } + > + ReportEdge: ResolverTypeWrapper< + Omit & { node: GQLResolversTypes['Report'] } + > + ReportReason: GQLReportReason ResetLikerIdInput: GQLResetLikerIdInput ResetPasswordInput: GQLResetPasswordInput ResetPasswordType: GQLResetPasswordType @@ -4516,10 +4579,10 @@ export type GQLResolversTypes = ResolversObject<{ SocialAccount: ResolverTypeWrapper SocialAccountType: GQLSocialAccountType SocialLoginInput: GQLSocialLoginInput - SortTopicsInput: GQLSortTopicsInput String: ResolverTypeWrapper StripeAccount: ResolverTypeWrapper StripeAccountCountry: GQLStripeAccountCountry + SubmitReportInput: GQLSubmitReportInput SubscribeCircleInput: GQLSubscribeCircleInput SubscribeCircleResult: ResolverTypeWrapper< Omit & { @@ -4557,16 +4620,6 @@ export type GQLResolversTypes = ResolversObject<{ > TopDonatorFilter: GQLTopDonatorFilter TopDonatorInput: GQLTopDonatorInput - Topic: ResolverTypeWrapper - TopicConnection: ResolverTypeWrapper< - Omit & { - edges?: Maybe> - } - > - TopicEdge: ResolverTypeWrapper< - Omit & { node: GQLResolversTypes['Topic'] } - > - TopicInput: GQLTopicInput Transaction: ResolverTypeWrapper TransactionConnection: ResolverTypeWrapper< Omit & { @@ -4696,13 +4749,13 @@ export type GQLResolversParentTypes = ResolversObject<{ AppreciationEdge: Omit & { node: GQLResolversParentTypes['Appreciation'] } - Article: DraftModel - ArticleAccess: DraftModel + Article: ArticleModel + ArticleAccess: ArticleModel ArticleArticleNotice: NoticeItemModel ArticleConnection: Omit & { edges?: Maybe> } - ArticleContents: DraftModel + ArticleContents: ArticleVersionModel ArticleDonation: Omit & { sender?: Maybe } @@ -4717,12 +4770,20 @@ export type GQLResolversParentTypes = ResolversObject<{ } ArticleInput: GQLArticleInput ArticleNotice: NoticeItemModel - ArticleOSS: DraftModel + ArticleOSS: ArticleModel ArticleRecommendationActivity: Omit< GQLArticleRecommendationActivity, 'nodes' > & { nodes?: Maybe> } ArticleTranslation: GQLArticleTranslation + ArticleVersion: ArticleVersionModel + ArticleVersionEdge: Omit & { + node: GQLResolversParentTypes['ArticleVersion'] + } + ArticleVersionsConnection: Omit & { + edges: Array> + } + ArticleVersionsInput: GQLArticleVersionsInput Asset: AssetModel AuthResult: Omit & { user?: Maybe @@ -4734,10 +4795,6 @@ export type GQLResolversParentTypes = ResolversObject<{ BlockedSearchKeyword: GQLBlockedSearchKeyword Boolean: Scalars['Boolean']['output'] ChangeEmailInput: GQLChangeEmailInput - Chapter: Omit & { - articles?: Maybe> - topic: GQLResolversParentTypes['Topic'] - } Circle: CircleModel CircleAnalytics: CircleModel CircleConnection: Omit & { @@ -4797,7 +4854,6 @@ export type GQLResolversParentTypes = ResolversObject<{ DeleteCommentInput: GQLDeleteCommentInput DeleteDraftInput: GQLDeleteDraftInput DeleteTagsInput: GQLDeleteTagsInput - DeleteTopicsInput: GQLDeleteTopicsInput DirectImageUploadInput: GQLDirectImageUploadInput Draft: DraftModel DraftAccess: DraftModel @@ -4825,6 +4881,13 @@ export type GQLResolversParentTypes = ResolversObject<{ FrequentSearchInput: GQLFrequentSearchInput GenerateSigningMessageInput: GQLGenerateSigningMessageInput ID: Scalars['ID']['output'] + IcymiTopic: MattersChoiceTopicModel + IcymiTopicConnection: Omit & { + edges: Array + } + IcymiTopicEdge: Omit & { + node: GQLResolversParentTypes['IcymiTopic'] + } Int: Scalars['Int']['output'] Invitation: CircleInvitationModel InvitationConnection: Omit & { @@ -4877,7 +4940,9 @@ export type GQLResolversParentTypes = ResolversObject<{ | 'articles' | 'badgedUsers' | 'comments' + | 'icymiTopics' | 'oauthClients' + | 'reports' | 'restrictedUsers' | 'seedingUsers' | 'tags' @@ -4886,7 +4951,9 @@ export type GQLResolversParentTypes = ResolversObject<{ articles: GQLResolversParentTypes['ArticleConnection'] badgedUsers: GQLResolversParentTypes['UserConnection'] comments: GQLResolversParentTypes['CommentConnection'] + icymiTopics: GQLResolversParentTypes['IcymiTopicConnection'] oauthClients: GQLResolversParentTypes['OAuthClientConnection'] + reports: GQLResolversParentTypes['ReportConnection'] restrictedUsers: GQLResolversParentTypes['UserConnection'] seedingUsers: GQLResolversParentTypes['UserConnection'] tags: GQLResolversParentTypes['TagConnection'] @@ -4907,18 +4974,17 @@ export type GQLResolversParentTypes = ResolversObject<{ Price: CirclePriceModel PublishArticleInput: GQLPublishArticleInput PutAnnouncementInput: GQLPutAnnouncementInput - PutChapterInput: GQLPutChapterInput PutCircleArticlesInput: GQLPutCircleArticlesInput PutCircleInput: GQLPutCircleInput PutCollectionInput: GQLPutCollectionInput PutCommentInput: GQLPutCommentInput PutDraftInput: GQLPutDraftInput + PutIcymiTopicInput: GQLPutIcymiTopicInput PutOAuthClientInput: GQLPutOAuthClientInput PutRemarkInput: GQLPutRemarkInput PutRestrictedUsersInput: GQLPutRestrictedUsersInput PutSkippedListItemInput: GQLPutSkippedListItemInput PutTagInput: GQLPutTagInput - PutTopicInput: GQLPutTopicInput Query: {} ReadArticleInput: GQLReadArticleInput ReadHistory: Omit & { @@ -4940,6 +5006,13 @@ export type GQLResolversParentTypes = ResolversObject<{ RenameTagInput: GQLRenameTagInput ReorderCollectionArticlesInput: GQLReorderCollectionArticlesInput ReorderMoveInput: GQLReorderMoveInput + Report: ReportModel + ReportConnection: Omit & { + edges?: Maybe> + } + ReportEdge: Omit & { + node: GQLResolversParentTypes['Report'] + } ResetLikerIdInput: GQLResetLikerIdInput ResetPasswordInput: GQLResetPasswordInput ResetWalletInput: GQLResetWalletInput @@ -4968,9 +5041,9 @@ export type GQLResolversParentTypes = ResolversObject<{ SkippedListItemsInput: GQLSkippedListItemsInput SocialAccount: GQLSocialAccount SocialLoginInput: GQLSocialLoginInput - SortTopicsInput: GQLSortTopicsInput String: Scalars['String']['output'] StripeAccount: PayoutAccountModel + SubmitReportInput: GQLSubmitReportInput SubscribeCircleInput: GQLSubscribeCircleInput SubscribeCircleResult: Omit & { circle: GQLResolversParentTypes['Circle'] @@ -4998,14 +5071,6 @@ export type GQLResolversParentTypes = ResolversObject<{ } TopDonatorFilter: GQLTopDonatorFilter TopDonatorInput: GQLTopDonatorInput - Topic: TopicModel - TopicConnection: Omit & { - edges?: Maybe> - } - TopicEdge: Omit & { - node: GQLResolversParentTypes['Topic'] - } - TopicInput: GQLTopicInput Transaction: TransactionModel TransactionConnection: Omit & { edges?: Maybe> @@ -5364,6 +5429,7 @@ export type GQLArticleResolvers< cover?: Resolver, ParentType, ContextType> createdAt?: Resolver dataHash?: Resolver + donated?: Resolver donationCount?: Resolver donations?: Resolver< GQLResolversTypes['ArticleDonationConnection'], @@ -5467,6 +5533,7 @@ export type GQLArticleResolvers< ParentType, ContextType > + shortHash?: Resolver slug?: Resolver state?: Resolver sticky?: Resolver @@ -5506,6 +5573,12 @@ export type GQLArticleResolvers< ContextType, Partial > + versions?: Resolver< + GQLResolversTypes['ArticleVersionsConnection'], + ParentType, + ContextType, + RequireFields + > wordCount?: Resolver, ParentType, ContextType> __isTypeOf?: IsTypeOfResolverFn }> @@ -5695,6 +5768,66 @@ export type GQLArticleTranslationResolvers< __isTypeOf?: IsTypeOfResolverFn }> +export type GQLArticleVersionResolvers< + ContextType = Context, + ParentType extends GQLResolversParentTypes['ArticleVersion'] = GQLResolversParentTypes['ArticleVersion'] +> = ResolversObject<{ + contents?: Resolver< + GQLResolversTypes['ArticleContents'], + ParentType, + ContextType + > + createdAt?: Resolver + dataHash?: Resolver< + Maybe, + ParentType, + ContextType + > + description?: Resolver< + Maybe, + ParentType, + ContextType + > + id?: Resolver + mediaHash?: Resolver< + Maybe, + ParentType, + ContextType + > + summary?: Resolver + title?: Resolver + translation?: Resolver< + Maybe, + ParentType, + ContextType, + Partial + > + __isTypeOf?: IsTypeOfResolverFn +}> + +export type GQLArticleVersionEdgeResolvers< + ContextType = Context, + ParentType extends GQLResolversParentTypes['ArticleVersionEdge'] = GQLResolversParentTypes['ArticleVersionEdge'] +> = ResolversObject<{ + cursor?: Resolver + node?: Resolver + __isTypeOf?: IsTypeOfResolverFn +}> + +export type GQLArticleVersionsConnectionResolvers< + ContextType = Context, + ParentType extends GQLResolversParentTypes['ArticleVersionsConnection'] = GQLResolversParentTypes['ArticleVersionsConnection'] +> = ResolversObject<{ + edges?: Resolver< + Array>, + ParentType, + ContextType + > + pageInfo?: Resolver + totalCount?: Resolver + __isTypeOf?: IsTypeOfResolverFn +}> + export type GQLAssetResolvers< ContextType = Context, ParentType extends GQLResolversParentTypes['Asset'] = GQLResolversParentTypes['Asset'] @@ -5758,27 +5891,6 @@ export type GQLBlockedSearchKeywordResolvers< __isTypeOf?: IsTypeOfResolverFn }> -export type GQLChapterResolvers< - ContextType = Context, - ParentType extends GQLResolversParentTypes['Chapter'] = GQLResolversParentTypes['Chapter'] -> = ResolversObject<{ - articleCount?: Resolver - articles?: Resolver< - Maybe>, - ParentType, - ContextType - > - description?: Resolver< - Maybe, - ParentType, - ContextType - > - id?: Resolver - title?: Resolver - topic?: Resolver - __isTypeOf?: IsTypeOfResolverFn -}> - export type GQLCircleResolvers< ContextType = Context, ParentType extends GQLResolversParentTypes['Circle'] = GQLResolversParentTypes['Circle'] @@ -6227,23 +6339,25 @@ export type GQLConnectionResolvers< __resolveType: TypeResolveFn< | 'AppreciationConnection' | 'ArticleConnection' + | 'ArticleVersionsConnection' | 'CircleConnection' | 'CollectionConnection' | 'CommentConnection' | 'DraftConnection' | 'FollowingActivityConnection' + | 'IcymiTopicConnection' | 'InvitationConnection' | 'MemberConnection' | 'NoticeConnection' | 'OAuthClientConnection' | 'ReadHistoryConnection' | 'RecentSearchConnection' + | 'ReportConnection' | 'ResponseConnection' | 'SearchResultConnection' | 'SkippedListItemsConnection' | 'TagConnection' | 'TopDonatorConnection' - | 'TopicConnection' | 'TransactionConnection' | 'UserConnection', ParentType, @@ -6483,6 +6597,60 @@ export type GQLFollowingActivityEdgeResolvers< __isTypeOf?: IsTypeOfResolverFn }> +export type GQLIcymiTopicResolvers< + ContextType = Context, + ParentType extends GQLResolversParentTypes['IcymiTopic'] = GQLResolversParentTypes['IcymiTopic'] +> = ResolversObject<{ + archivedAt?: Resolver< + Maybe, + ParentType, + ContextType + > + articles?: Resolver< + Array, + ParentType, + ContextType + > + id?: Resolver + note?: Resolver, ParentType, ContextType> + pinAmount?: Resolver + publishedAt?: Resolver< + Maybe, + ParentType, + ContextType + > + state?: Resolver< + GQLResolversTypes['IcymiTopicState'], + ParentType, + ContextType + > + title?: Resolver + __isTypeOf?: IsTypeOfResolverFn +}> + +export type GQLIcymiTopicConnectionResolvers< + ContextType = Context, + ParentType extends GQLResolversParentTypes['IcymiTopicConnection'] = GQLResolversParentTypes['IcymiTopicConnection'] +> = ResolversObject<{ + edges?: Resolver< + Array, + ParentType, + ContextType + > + pageInfo?: Resolver + totalCount?: Resolver + __isTypeOf?: IsTypeOfResolverFn +}> + +export type GQLIcymiTopicEdgeResolvers< + ContextType = Context, + ParentType extends GQLResolversParentTypes['IcymiTopicEdge'] = GQLResolversParentTypes['IcymiTopicEdge'] +> = ResolversObject<{ + cursor?: Resolver + node?: Resolver + __isTypeOf?: IsTypeOfResolverFn +}> + export type GQLInvitationResolvers< ContextType = Context, ParentType extends GQLResolversParentTypes['Invitation'] = GQLResolversParentTypes['Invitation'] @@ -6741,12 +6909,6 @@ export type GQLMutationResolvers< ContextType, RequireFields > - deleteTopics?: Resolver< - GQLResolversTypes['Boolean'], - ParentType, - ContextType, - RequireFields - > directImageUpload?: Resolver< GQLResolversTypes['Asset'], ParentType, @@ -6831,12 +6993,6 @@ export type GQLMutationResolvers< ContextType, RequireFields > - putChapter?: Resolver< - GQLResolversTypes['Chapter'], - ParentType, - ContextType, - RequireFields - > putCircle?: Resolver< GQLResolversTypes['Circle'], ParentType, @@ -6873,6 +7029,12 @@ export type GQLMutationResolvers< ContextType, RequireFields > + putIcymiTopic?: Resolver< + Maybe, + ParentType, + ContextType, + RequireFields + > putOAuthClient?: Resolver< Maybe, ParentType, @@ -6903,12 +7065,6 @@ export type GQLMutationResolvers< ContextType, RequireFields > - putTopic?: Resolver< - GQLResolversTypes['Topic'], - ParentType, - ContextType, - RequireFields - > readArticle?: Resolver< GQLResolversTypes['Article'], ParentType, @@ -7016,11 +7172,11 @@ export type GQLMutationResolvers< ContextType, RequireFields > - sortTopics?: Resolver< - Array, + submitReport?: Resolver< + GQLResolversTypes['Report'], ParentType, ContextType, - RequireFields + RequireFields > subscribeCircle?: Resolver< GQLResolversTypes['SubscribeCircleResult'], @@ -7247,13 +7403,14 @@ export type GQLNodeResolvers< > = ResolversObject<{ __resolveType: TypeResolveFn< | 'Article' - | 'Chapter' + | 'ArticleVersion' | 'Circle' | 'Collection' | 'Comment' | 'Draft' + | 'IcymiTopic' + | 'Report' | 'Tag' - | 'Topic' | 'User', ParentType, ContextType @@ -7478,12 +7635,24 @@ export type GQLOssResolvers< ContextType, RequireFields > + icymiTopics?: Resolver< + GQLResolversTypes['IcymiTopicConnection'], + ParentType, + ContextType, + RequireFields + > oauthClients?: Resolver< GQLResolversTypes['OAuthClientConnection'], ParentType, ContextType, RequireFields > + reports?: Resolver< + GQLResolversTypes['ReportConnection'], + ParentType, + ContextType, + RequireFields + > restrictedUsers?: Resolver< GQLResolversTypes['UserConnection'], ParentType, @@ -7787,6 +7956,11 @@ export type GQLRecommendationResolvers< ContextType, RequireFields > + icymiTopic?: Resolver< + Maybe, + ParentType, + ContextType + > newest?: Resolver< GQLResolversTypes['ArticleConnection'], ParentType, @@ -7820,6 +7994,41 @@ export type GQLRecommendationResolvers< __isTypeOf?: IsTypeOfResolverFn }> +export type GQLReportResolvers< + ContextType = Context, + ParentType extends GQLResolversParentTypes['Report'] = GQLResolversParentTypes['Report'] +> = ResolversObject<{ + createdAt?: Resolver + id?: Resolver + reason?: Resolver + reporter?: Resolver + target?: Resolver + __isTypeOf?: IsTypeOfResolverFn +}> + +export type GQLReportConnectionResolvers< + ContextType = Context, + ParentType extends GQLResolversParentTypes['ReportConnection'] = GQLResolversParentTypes['ReportConnection'] +> = ResolversObject<{ + edges?: Resolver< + Maybe>, + ParentType, + ContextType + > + pageInfo?: Resolver + totalCount?: Resolver + __isTypeOf?: IsTypeOfResolverFn +}> + +export type GQLReportEdgeResolvers< + ContextType = Context, + ParentType extends GQLResolversParentTypes['ReportEdge'] = GQLResolversParentTypes['ReportEdge'] +> = ResolversObject<{ + cursor?: Resolver + node?: Resolver + __isTypeOf?: IsTypeOfResolverFn +}> + export type GQLResponseResolvers< ContextType = Context, ParentType extends GQLResolversParentTypes['Response'] = GQLResolversParentTypes['Response'] @@ -8109,63 +8318,6 @@ export type GQLTopDonatorEdgeResolvers< __isTypeOf?: IsTypeOfResolverFn }> -export type GQLTopicResolvers< - ContextType = Context, - ParentType extends GQLResolversParentTypes['Topic'] = GQLResolversParentTypes['Topic'] -> = ResolversObject<{ - articleCount?: Resolver - articles?: Resolver< - Maybe>, - ParentType, - ContextType - > - author?: Resolver - chapterCount?: Resolver - chapters?: Resolver< - Maybe>, - ParentType, - ContextType - > - cover?: Resolver, ParentType, ContextType> - description?: Resolver< - Maybe, - ParentType, - ContextType - > - id?: Resolver - latestArticle?: Resolver< - Maybe, - ParentType, - ContextType - > - public?: Resolver - title?: Resolver - __isTypeOf?: IsTypeOfResolverFn -}> - -export type GQLTopicConnectionResolvers< - ContextType = Context, - ParentType extends GQLResolversParentTypes['TopicConnection'] = GQLResolversParentTypes['TopicConnection'] -> = ResolversObject<{ - edges?: Resolver< - Maybe>, - ParentType, - ContextType - > - pageInfo?: Resolver - totalCount?: Resolver - __isTypeOf?: IsTypeOfResolverFn -}> - -export type GQLTopicEdgeResolvers< - ContextType = Context, - ParentType extends GQLResolversParentTypes['TopicEdge'] = GQLResolversParentTypes['TopicEdge'] -> = ResolversObject<{ - cursor?: Resolver - node?: Resolver - __isTypeOf?: IsTypeOfResolverFn -}> - export type GQLTransactionResolvers< ContextType = Context, ParentType extends GQLResolversParentTypes['Transaction'] = GQLResolversParentTypes['Transaction'] @@ -8356,6 +8508,11 @@ export type GQLUserResolvers< isBlocking?: Resolver isFollowee?: Resolver isFollower?: Resolver + latestWorks?: Resolver< + Array, + ParentType, + ContextType + > liker?: Resolver likerId?: Resolver< Maybe, @@ -8430,12 +8587,6 @@ export type GQLUserResolvers< ContextType, RequireFields > - topics?: Resolver< - GQLResolversTypes['TopicConnection'], - ParentType, - ContextType, - RequireFields - > userName?: Resolver< Maybe, ParentType, @@ -8817,13 +8968,15 @@ export type GQLResolvers = ResolversObject<{ ArticleOSS?: GQLArticleOssResolvers ArticleRecommendationActivity?: GQLArticleRecommendationActivityResolvers ArticleTranslation?: GQLArticleTranslationResolvers + ArticleVersion?: GQLArticleVersionResolvers + ArticleVersionEdge?: GQLArticleVersionEdgeResolvers + ArticleVersionsConnection?: GQLArticleVersionsConnectionResolvers Asset?: GQLAssetResolvers AuthResult?: GQLAuthResultResolvers Badge?: GQLBadgeResolvers Balance?: GQLBalanceResolvers BlockchainTransaction?: GQLBlockchainTransactionResolvers BlockedSearchKeyword?: GQLBlockedSearchKeywordResolvers - Chapter?: GQLChapterResolvers Circle?: GQLCircleResolvers CircleAnalytics?: GQLCircleAnalyticsResolvers CircleConnection?: GQLCircleConnectionResolvers @@ -8858,6 +9011,9 @@ export type GQLResolvers = ResolversObject<{ FollowingActivity?: GQLFollowingActivityResolvers FollowingActivityConnection?: GQLFollowingActivityConnectionResolvers FollowingActivityEdge?: GQLFollowingActivityEdgeResolvers + IcymiTopic?: GQLIcymiTopicResolvers + IcymiTopicConnection?: GQLIcymiTopicConnectionResolvers + IcymiTopicEdge?: GQLIcymiTopicEdgeResolvers Invitation?: GQLInvitationResolvers InvitationConnection?: GQLInvitationConnectionResolvers InvitationEdge?: GQLInvitationEdgeResolvers @@ -8893,6 +9049,9 @@ export type GQLResolvers = ResolversObject<{ RecentSearchConnection?: GQLRecentSearchConnectionResolvers RecentSearchEdge?: GQLRecentSearchEdgeResolvers Recommendation?: GQLRecommendationResolvers + Report?: GQLReportResolvers + ReportConnection?: GQLReportConnectionResolvers + ReportEdge?: GQLReportEdgeResolvers Response?: GQLResponseResolvers ResponseConnection?: GQLResponseConnectionResolvers ResponseEdge?: GQLResponseEdgeResolvers @@ -8911,9 +9070,6 @@ export type GQLResolvers = ResolversObject<{ TagOSS?: GQLTagOssResolvers TopDonatorConnection?: GQLTopDonatorConnectionResolvers TopDonatorEdge?: GQLTopDonatorEdgeResolvers - Topic?: GQLTopicResolvers - TopicConnection?: GQLTopicConnectionResolvers - TopicEdge?: GQLTopicEdgeResolvers Transaction?: GQLTransactionResolvers TransactionConnection?: GQLTransactionConnectionResolvers TransactionEdge?: GQLTransactionEdgeResolvers diff --git a/src/definitions/tag.d.ts b/src/definitions/tag.d.ts index f9c65c634..fadfc1cb7 100644 --- a/src/definitions/tag.d.ts +++ b/src/definitions/tag.d.ts @@ -3,13 +3,38 @@ export interface Tag { content: string createdAt: string updatedAt: string - remark?: string + remark: string | null delete: boolean - cover?: string - description?: string - editors?: string[] + cover: string | null + description: string | null + editors: string[] | null creator: string - owner: string - majorTagId?: string + owner: string | null + majorTagId: string | null slug: string } + +export interface TagBoost { + id: string + tagId: string + boost: number + createdAt: Date + updatedAt: Date +} + +export interface UserTagsOrder { + id: string + userId: string + tagIds: string[] + createdAt: Date + updatedAt: Date +} + +export interface TagTranslation { + id: string + tagId: string + language: string + content: string + createdAt: Date + updatedAt: Date +} diff --git a/src/definitions/topic.d.ts b/src/definitions/topic.d.ts deleted file mode 100644 index fe690c978..000000000 --- a/src/definitions/topic.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface Topic { - id: string - title: string - description?: string - cover?: string - userId: string - public: boolean -} diff --git a/src/definitions/user.d.ts b/src/definitions/user.d.ts index 211c47ef5..1ead785d2 100644 --- a/src/definitions/user.d.ts +++ b/src/definitions/user.d.ts @@ -1,4 +1,8 @@ -import { SOCIAL_LOGIN_TYPE, USER_STATE } from 'common/enums' +import { + SOCIAL_LOGIN_TYPE, + USER_STATE, + USER_RESTRICTION_TYPE, +} from 'common/enums' import { LANGUAGES } from './language' @@ -11,23 +15,25 @@ interface UserBase { avatar: string email: string | null emailVerified: boolean - likerId?: string - passwordHash: string - paymentPointer?: string - paymentPasswordHash?: string + likerId: string | null + passwordHash: string | null + paymentPointer: string | null + paymentPasswordHash: string | null baseGravity: number currGravity: number language: LANGUAGES // oauthType: any role: UserRole state: UserState - createdAt: string - updatedAt: string agreeOn: string - ethAddress: string - currency?: 'HKD' | 'TWD' | 'USD' + ethAddress: string | null + currency: 'HKD' | 'TWD' | 'USD' | null profileCover?: string - extra?: any // jsonb saved here + // eslint-disable-next-line @typescript-eslint/no-explicit-any + extra: any | null // jsonb saved here + remark: string | null + createdAt: Date + updatedAt: Date } export type UserNoUsername = UserBase & { userName: null; displayName: null } @@ -68,82 +74,100 @@ export type Viewer = (User & ViewerBase) | ViewerBase export type AuthMode = 'visitor' | 'oauth' | 'user' | 'admin' -export type UserOAuthLikeCoinAccountType = 'temporal' | 'general' - -export interface UserOAuthLikeCoin { - likerId: string - accountType: UserOAuthLikeCoinAccountType - accessToken: string - refreshToken: string - expires: Date - scope: string | string[] +export interface Wallet { + id: string + userId: string + address: string } -export interface OAuthClientDB { - id: sring +export interface SocialAccount { userId: string - avatar: string + type: keyof typeof SOCIAL_LOGIN_TYPE + providerAccountId: string + userName?: string + email?: string } -export interface OAuthClient { - [key: string]: any +export interface UserIpnsKeys { id: string - redirectUris?: string | string[] - grants: string | string[] - accessTokenLifetime?: number - refreshTokenLifetime?: number + userId: string | null + ipnsKey: string + privKeyPem: string + privKeyName: string + createdAt: Date + updatedAt: Date + lastDataHash: string | null + lastPublished: Date | null + // eslint-disable-next-line @typescript-eslint/no-explicit-any + stats: any } -export interface OAuthAuthorizationCode { - [key: string]: any - authorizationCode: string - expiresAt: Date - redirectUri: string - scope?: string | string[] - client: OAuthClient - user: User +export interface UserNotifySetting { + id: string + userId: string + enable: boolean + mention: boolean + userNewFollower: boolean + articleNewComment: boolean + articleNewAppreciation: boolean + articleNewSubscription: boolean + articleSubscribedNewComment: boolean + articleCommentPinned: boolean + reportFeedback: boolean + email: boolean + tag: boolean + circleNewFollower: boolean + circleNewDiscussion: boolean + circleNewSubscriber: boolean + circleNewUnsubscriber: boolean + circleMemberBroadcast: boolean + circleMemberNewDiscussion: boolean + circleMemberNewDiscussionReply: boolean + inCircleNewArticle: boolean + inCircleNewBroadcast: boolean + inCircleNewBroadcastReply: boolean + inCircleNewDiscussion: boolean + inCircleNewDiscussionReply: boolean + articleNewCollected: boolean + circleMemberNewBroadcastReply: boolean + createdAt: Date + updatedAt: Date } -export interface OAuthToken { - [key: string]: any - accessToken: string - accessTokenExpiresAt?: Date - refreshToken?: string - refreshTokenExpiresAt?: Date - scope?: string | string[] - client: OAuthClient - user: User +export interface UsernameEditHistory { + id: string + userId: string + previous: string + createdAt: Date } -export interface OAuthRefreshToken { - [key: string]: any - refreshToken: string - refreshTokenExpiresAt?: Date - scope?: string | string[] - client: OAuthClient - user: User +export interface UserRestriction { + id: string + userId: string + type: keyof typeof USER_RESTRICTION_TYPE + createdAt: Date } -export interface VerficationCode { +export interface UserBoost { id: string - uuid: string - expiredAt: Date - code: string - type: GQLVerificationCodeType - status: VERIFICATION_CODE_STATUS - email: string + userId: string + boost: number + createdAt: Date + updatedAt: Date } -export interface Wallet { +export interface SeedingUser { id: string userId: string - address: string + createdAt: Date + updatedAt: Date } -export interface SocialAccount { +export interface UserBadge { + id: string userId: string - type: keyof typeof SOCIAL_LOGIN_TYPE - providerAccountId: string - userName?: string - email?: string + type: string + enabled: boolean + extra: any + createdAt: Date } diff --git a/src/definitions/wallet.d.ts b/src/definitions/wallet.d.ts new file mode 100644 index 000000000..5f14f304f --- /dev/null +++ b/src/definitions/wallet.d.ts @@ -0,0 +1,23 @@ +import { GQLSigningMessagePurpose } from './schema' + +export interface CryptoWallet { + id: string + userId: string | null + address: string + archived: boolean + createdAt: Date + updatedAt: Date +} + +export interface CryptoWalletSignature { + id: string + address: string + signedMessage: string | null + signature: string | null + purpose: GQLSigningMessagePurpose + userId: string | null + nonce: string | null + expiredAt: Date | null + createdAt: Date + updatedAt: Date +} diff --git a/src/mutations/article/appreciateArticle.ts b/src/mutations/article/appreciateArticle.ts index 95b2bc6a5..04d90d899 100644 --- a/src/mutations/article/appreciateArticle.ts +++ b/src/mutations/article/appreciateArticle.ts @@ -1,16 +1,6 @@ -// import slugify from '@matters/slugify' import type { GQLMutationResolvers } from 'definitions' -import { v4 } from 'uuid' - -import { - APPRECIATION_PURPOSE, - APPRECIATION_TYPES, - ARTICLE_ACCESS_TYPE, - ARTICLE_STATE, - USER_STATE, -} from 'common/enums' -import { environment } from 'common/environment' +import { ARTICLE_STATE, USER_STATE } from 'common/enums' import { ActionLimitExceededError, ArticleNotFoundError, @@ -19,12 +9,11 @@ import { ForbiddenError, UserInputError, } from 'common/errors' -import { fromGlobalId, verifyCaptchaToken } from 'common/utils' -// import { GCP, cfsvc } from 'connectors' +import { fromGlobalId } from 'common/utils' const resolver: GQLMutationResolvers['appreciateArticle'] = async ( _, - { input: { id, amount, token, superLike } }, + { input: { id, amount } }, context ) => { const { @@ -33,9 +22,6 @@ const resolver: GQLMutationResolvers['appreciateArticle'] = async ( atomService, userService, articleService, - draftService, - paymentService, - systemService, queues: { appreciationQueue }, }, } = context @@ -67,42 +53,15 @@ const resolver: GQLMutationResolvers['appreciateArticle'] = async ( if (!article) { throw new ArticleNotFoundError('target article does not exists') } - const node = await draftService.baseFindById(article.draftId) - if (!node) { - throw new ArticleNotFoundError( - 'target article linked draft does not exists' - ) - } // check author - const isAuthor = article.authorId === viewer.id - if (isAuthor && !superLike) { - throw new ForbiddenError('cannot appreciate your own article') - } - - const author = await userService.loadById(article.authorId) - + const author = await atomService.userIdLoader.load(article.authorId) if (author.state === USER_STATE.frozen) { throw new ForbiddenByTargetStateError( `cannot appreciate ${author.state} user` ) } - // check access - const articleCircle = await articleService.findArticleCircle(article.id) - - if (articleCircle && !isAuthor) { - const isCircleMember = await paymentService.isCircleMember({ - userId: viewer.id, - circleId: articleCircle.circleId, - }) - const isPaywall = articleCircle.access === ARTICLE_ACCESS_TYPE.paywall - - if (isPaywall && !isCircleMember) { - throw new ForbiddenError('only circle members have the permission') - } - } - // check if viewer is blocked by article owner const isBlocked = await userService.blocked({ userId: article.authorId, @@ -112,54 +71,6 @@ const resolver: GQLMutationResolvers['appreciateArticle'] = async ( throw new ForbiddenError('viewer is blocked by target author') } - /** - * Super Like - */ - if (superLike) { - const liker = await userService.findLiker({ userId: viewer.id }) - - if (liker?.likerId && author.likerId) { - // const slug = slugify(node.title) - const superLikeData = { - liker, - iscn_id: article.iscn_id, - url: `https://${environment.siteDomain}/@${author.userName}/${article.id}`, - likerIp: viewer.ip, - userAgent: viewer.userAgent, - } - const canSuperLike = await userService.likecoin.canSuperLike( - superLikeData - ) - - if (!canSuperLike) { - throw new ForbiddenError('cannot super like') - } - - await userService.likecoin.superlike({ - ...superLikeData, - authorLikerId: author.likerId, - }) - - // insert record - const appreciation = { - senderId: viewer.id, - recipientId: article.authorId, - referenceId: article.id, - purpose: APPRECIATION_PURPOSE.superlike, - type: APPRECIATION_TYPES.like, - } - await atomService.create({ - table: 'appreciation', - data: { ...appreciation, uuid: v4(), amount }, - }) - - return node - } - } - - /** - * Like - */ const appreciateLeft = await articleService.appreciateLeftByUser({ articleId: dbId, userId: viewer.id, @@ -171,31 +82,16 @@ const resolver: GQLMutationResolvers['appreciateArticle'] = async ( // Check if amount exceeded limit. if yes, then use the left amount. const validAmount = Math.min(amount, appreciateLeft) - // protect from scripting - const feature = await systemService.getFeatureFlag('verify_appreciate') - - if (feature && (await systemService.isFeatureEnabled(feature.flag, viewer))) { - // for a transition period, we may check both, and pass if any one pass siteverify - // after the transition period, can turn off the one no longer in use - const isHuman = await verifyCaptchaToken(token!, viewer.ip) - if (!isHuman) { - throw new ForbiddenError('appreciate via script is not allowed') - } - } - // insert appreciation job - appreciationQueue.appreciate( - { - amount: validAmount, - articleId: article.id, - senderId: viewer.id, - senderIP: viewer.ip, - userAgent: viewer.userAgent, - }, - context - ) + appreciationQueue.appreciate({ + amount: validAmount, + articleId: article.id, + senderId: viewer.id, + senderIP: viewer.ip, + userAgent: viewer.userAgent, + }) - return node + return article } export default resolver diff --git a/src/mutations/article/deleteTopics.ts b/src/mutations/article/deleteTopics.ts deleted file mode 100644 index 10cbf799b..000000000 --- a/src/mutations/article/deleteTopics.ts +++ /dev/null @@ -1,74 +0,0 @@ -import type { GQLMutationResolvers } from 'definitions' - -import { invalidateFQC } from '@matters/apollo-response-cache' -import _uniq from 'lodash/uniq' - -import { NODE_TYPES } from 'common/enums' -import { ForbiddenError } from 'common/errors' -import { fromGlobalId } from 'common/utils' - -const resolver: GQLMutationResolvers['deleteTopics'] = async ( - _, - { input: { ids } }, - { - viewer, - dataSources: { - atomService, - connections: { redis }, - }, - } -) => { - const topicIds = _uniq(ids.map((id) => fromGlobalId(id).id)) - - // check permission - const topics = await atomService.findMany({ - table: 'topic', - whereIn: ['id', topicIds], - }) - topics.forEach((topic) => { - if (topic.userId !== viewer.id) { - throw new ForbiddenError('viewer has no permission') - } - }) - - // delete chapters - const chapters = await atomService.findMany({ - table: 'chapter', - whereIn: ['topic_id', topicIds], - }) - const chapterIds = chapters.map((c) => c.id) - await atomService.deleteMany({ - table: 'article_chapter', - whereIn: ['chapter_id', chapterIds], - }) - await atomService.deleteMany({ - table: 'chapter', - whereIn: ['id', chapterIds], - }) - - // delete articles - await atomService.deleteMany({ - table: 'article_topic', - whereIn: ['topic_id', topicIds], - }) - - // delete topics - await atomService.deleteMany({ - table: 'topic', - whereIn: ['id', topicIds], - }) - - // manually invalidate cache since it returns nothing - await Promise.all( - topicIds.map((id) => - invalidateFQC({ - node: { type: NODE_TYPES.Topic, id }, - redis, - }) - ) - ) - - return true -} - -export default resolver diff --git a/src/mutations/article/editArticle.ts b/src/mutations/article/editArticle.ts index b09782deb..99a3abd61 100644 --- a/src/mutations/article/editArticle.ts +++ b/src/mutations/article/editArticle.ts @@ -1,14 +1,10 @@ -import type { Article, DataSources, GQLMutationResolvers } from 'definitions' -import type { Knex } from 'knex' +import type { Article, Draft, Circle, GQLMutationResolvers } from 'definitions' import { stripHtml } from '@matters/ipns-site-generator' import { - html2md, normalizeArticleHTML, sanitizeHTML, } from '@matters/matters-editor/transformers' -import lodash, { difference, isEqual, uniq } from 'lodash' -import { v4 } from 'uuid' import { ARTICLE_LICENSE_TYPE, @@ -16,34 +12,23 @@ import { ASSET_TYPE, CACHE_KEYWORD, CIRCLE_STATE, - DB_NOTICE_TYPE, + MAX_ARTICLE_TITLE_LENGTH, + MAX_ARTICLE_SUMMARY_LENGTH, MAX_ARTICLE_CONTENT_LENGTH, - MAX_ARTICLE_CONTENT_REVISION_LENGTH, MAX_ARTICLE_REVISION_COUNT, - MAX_ARTICLES_PER_CONNECTION_LIMIT, - MAX_TAGS_PER_ARTICLE_LIMIT, NODE_TYPES, - PUBLISH_STATE, USER_STATE, } from 'common/enums' -import { environment } from 'common/environment' import { - ArticleCollectionReachLimitError, ArticleNotFoundError, - ArticleRevisionContentInvalidError, ArticleRevisionReachLimitError, AssetNotFoundError, CircleNotFoundError, - DraftNotFoundError, ForbiddenByStateError, ForbiddenError, - TooManyTagsForArticleError, UserInputError, } from 'common/errors' -import { getLogger } from 'common/logger' -import { fromGlobalId, measureDiffs, normalizeTagInput } from 'common/utils' - -const logger = getLogger('mutation-edit-article') +import { fromGlobalId } from 'common/utils' const resolver: GQLMutationResolvers['editArticle'] = async ( _, @@ -54,6 +39,7 @@ const resolver: GQLMutationResolvers['editArticle'] = async ( sticky, pinned, tags, + title, content, summary, cover, @@ -66,6 +52,7 @@ const resolver: GQLMutationResolvers['editArticle'] = async ( replyToDonator, iscnPublish, canComment, + description, }, }, { @@ -73,13 +60,8 @@ const resolver: GQLMutationResolvers['editArticle'] = async ( dataSources: { articleService, atomService, - draftService, - notificationService, systemService, - tagService, - userService, - connections: { knex }, - queues: { publicationQueue, revisionQueue }, + queues: { revisionQueue }, }, } ) => { @@ -97,15 +79,17 @@ const resolver: GQLMutationResolvers['editArticle'] = async ( // checks const { id: dbId } = fromGlobalId(id) - const article = await articleService.baseFindById(dbId) + let article = await atomService.articleIdLoader.load(dbId) + const articleVersion = await articleService.loadLatestArticleVersion( + article.id + ) if (!article) { throw new ArticleNotFoundError('article does not exist') } - const draft = await draftService.baseFindById(article.draftId) - if (!draft) { - throw new DraftNotFoundError('article linked draft does not exist') + if (!articleVersion) { + throw new ArticleNotFoundError('article version does not exist') } - if (draft.authorId !== viewer.id) { + if (article.authorId !== viewer.id) { throw new ForbiddenError('viewer has no permission') } if (article.state !== ARTICLE_STATE.active) { @@ -121,11 +105,7 @@ const resolver: GQLMutationResolvers['editArticle'] = async ( ) } if (state === ARTICLE_STATE.archived) { - await articleService.archive(dbId) - - // refresh after archived any article - const author = await userService.baseFindById(article.authorId) - publicationQueue.refreshIPNSFeed({ userName: author.userName }) + return articleService.archive(dbId) } /** @@ -133,65 +113,105 @@ const resolver: GQLMutationResolvers['editArticle'] = async ( */ const isPinned = pinned ?? sticky if (typeof isPinned === 'boolean') { - await articleService.updatePinned(article.id, viewer.id, isPinned) + article = await articleService.updatePinned(article.id, viewer.id, isPinned) + } + + // collect new article version data + let data: Partial = {} + let updateRevisionCount = false + const checkRevisionCount = (newRevisionCount: number) => { + if (newRevisionCount > MAX_ARTICLE_REVISION_COUNT) { + throw new ArticleRevisionReachLimitError( + 'number of revisions reach limit' + ) + } + } + + /** + * title + */ + if (title !== undefined) { + const _title = (title ?? '').trim() + if (_title.length > MAX_ARTICLE_TITLE_LENGTH) { + throw new UserInputError('title reach length limit') + } + if (_title.length === 0) { + throw new UserInputError('title cannot be empty') + } + if (_title !== articleVersion.title) { + checkRevisionCount(article.revisionCount + 1) + updateRevisionCount = true + data = { ...data, title: _title } + } + } + + /** + * Summary + */ + if (summary !== undefined && summary !== articleVersion.summary) { + if (summary?.length > MAX_ARTICLE_SUMMARY_LENGTH) { + throw new UserInputError('summary reach length limit') + } + checkRevisionCount(article.revisionCount + 1) + updateRevisionCount = true + data = { ...data, summary: summary ? summary.trim() : null } } /** * Tags */ - if (tags !== undefined) { - await handleTags({ - viewerId: viewer.id, - tags, - article, - dataSources: { - tagService, - }, - }) + if ( + tags !== undefined && + (tags ?? []).toString() !== articleVersion.tags.toString() + ) { + checkRevisionCount(article.revisionCount + 1) + updateRevisionCount = true + data = { ...data, tags } } /** * Cover */ - const resetCover = cover === null - if (cover) { - const asset = await systemService.findAssetByUUID(cover) + if (cover !== undefined && cover !== articleVersion.cover) { + checkRevisionCount(article.revisionCount + 1) + updateRevisionCount = true - if ( - !asset || - [ASSET_TYPE.embed, ASSET_TYPE.cover].indexOf(asset.type) < 0 || - asset.authorId !== viewer.id - ) { - throw new AssetNotFoundError('article cover does not exists') - } + const resetCover = cover === null - await articleService.baseUpdate(dbId, { - cover: asset.id, - updatedAt: knex.fn.now(), - }) - } else if (resetCover) { - await articleService.baseUpdate(dbId, { - cover: null, - updatedAt: knex.fn.now(), - }) + if (resetCover) { + data = { ...data, cover: null } + } else { + const asset = await systemService.findAssetByUUID(cover) + + if ( + !asset || + [ASSET_TYPE.embed, ASSET_TYPE.cover].indexOf(asset.type) < 0 || + asset.authorId !== viewer.id + ) { + throw new AssetNotFoundError('article cover does not exists') + } + + data = { ...data, cover: asset.id } + } } /** * Connection */ if (collection !== undefined) { - await handleConnection({ - viewerId: viewer.id, - collection, - article, - dataSources: { - atomService, - userService, - articleService, - notificationService, - }, - knex, - }) + const connections = (collection ?? []).map( + (globalId) => fromGlobalId(globalId as unknown as string).id + ) + + if (connections.toString() !== articleVersion.connections.toString()) { + checkRevisionCount(article.revisionCount + 1) + updateRevisionCount = true + } + + data = { + ...data, + collection: connections, + } } /** @@ -202,8 +222,7 @@ const resolver: GQLMutationResolvers['editArticle'] = async ( where: { articleId: article.id }, }) const resetCircle = currAccess && circleGlobalId === null - let isUpdatingAccess = false - let circle: any + let circle: Circle if (circleGlobalId) { const { id: circleId } = fromGlobalId(circleGlobalId) @@ -230,103 +249,40 @@ const resolver: GQLMutationResolvers['editArticle'] = async ( circle.id !== currAccess?.circleId || (circle.id === currAccess?.circleId && accessType !== currAccess?.access) ) { - isUpdatingAccess = true + data = { ...data, circleId, access: accessType } } - - // insert to db - const data = { articleId: article.id, circleId: circle.id } - await atomService.upsert({ - table: 'article_circle', - where: data, - create: { ...data, access: accessType }, - update: { ...data, access: accessType, updatedAt: knex.fn.now() }, - }) } else if (resetCircle) { - await atomService.deleteMany({ - table: 'article_circle', - where: { articleId: article.id }, - }) - } - - /** - * Summary - */ - const resetSummary = summary === null || summary === '' - if (summary || resetSummary) { - await atomService.update({ - table: 'draft', - where: { id: article.draftId }, - data: { - summary: summary || null, - summaryCustomized: !!summary, - updatedAt: knex.fn.now(), - }, - }) - } - - /** - * Revision Count - */ - const isUpdatingCircleOrAccess = isUpdatingAccess || resetCircle - const checkRevisionCount = () => { - const revisionCount = article.revisionCount || 0 - if (revisionCount >= MAX_ARTICLE_REVISION_COUNT) { - throw new ArticleRevisionReachLimitError( - 'number of revisions reach limit' - ) - } + data = { ...data, circleId: null } } /** * License */ - // cc_by_nc_nd_2 license not longer in use if (license === ARTICLE_LICENSE_TYPE.cc_by_nc_nd_2) { throw new UserInputError( `${ARTICLE_LICENSE_TYPE.cc_by_nc_nd_2} is not longer in use` ) } - if (license !== draft.license) { - await atomService.update({ - table: 'draft', - where: { id: article.draftId }, - data: { - license: license || ARTICLE_LICENSE_TYPE.cc_by_nc_nd_4, - updatedAt: knex.fn.now(), - }, - }) + if (license && license !== articleVersion.license) { + data = { ...data, license } } /** * Support settings */ - const isUpdatingRequestForDonation = requestForDonation !== undefined - const isUpdatingReplyToDonator = replyToDonator !== undefined - if (isUpdatingRequestForDonation || isUpdatingReplyToDonator) { - await atomService.update({ - table: 'draft', - where: { id: article.draftId }, - data: { - requestForDonation, - replyToDonator, - updatedAt: knex.fn.now(), - }, - }) + if (requestForDonation !== undefined) { + data = { ...data, requestForDonation } + } + if (replyToDonator !== undefined) { + data = { ...data, replyToDonator } } /** * Comment settings */ - if (canComment !== undefined && canComment !== draft.canComment) { + if (canComment !== undefined && canComment !== articleVersion.canComment) { if (canComment === true) { - await atomService.update({ - table: 'draft', - where: { id: article.draftId }, - data: { - canComment, - updatedAt: knex.fn.now(), - }, - }) + data = { ...data, canComment } } else { throw new ForbiddenError(`canComment can not be turned off`) } @@ -335,120 +291,74 @@ const resolver: GQLMutationResolvers['editArticle'] = async ( /** * Sensitive settings */ - if (sensitive !== undefined && sensitive !== draft.sensitiveByAuthor) { - await atomService.update({ - table: 'draft', - where: { id: article.draftId }, - data: { - sensitiveByAuthor: sensitive, - updatedAt: knex.fn.now(), - }, - }) + if ( + sensitive !== undefined && + sensitive !== articleVersion.sensitiveByAuthor + ) { + data = { ...data, sensitiveByAuthor: sensitive } } /** * Republish article if content or access is changed */ - const republish = async (newContent?: string) => { - checkRevisionCount() - - // fetch updated data before create draft - const [ - currDraft, - currArticle, - currCollections, - currTags, - currArticleCircle, - ] = await Promise.all([ - draftService.baseFindById(article.draftId), // fetch latest draft - articleService.baseFindById(dbId), // fetch latest article - articleService.findConnections({ entranceId: article.id }), - tagService.findByArticleId({ articleId: article.id }), - articleService.findArticleCircle(article.id), - ]) - const currTagContents = currTags.map((currTag) => currTag.content) - const currCollectionIds = currCollections.map( - ({ articleId }: { articleId: string }) => articleId - ) - - // create draft linked to this article - const _content = normalizeArticleHTML( - sanitizeHTML(newContent || currDraft.content) - ) - let contentMd = '' - try { - contentMd = html2md(_content) - } catch (e) { - logger.warn('draft %s failed to convert HTML to Markdown', draft.id) - } - const data: Record = lodash.omitBy( - { - uuid: v4(), - authorId: currDraft.authorId, - articleId: currArticle.id, - title: currDraft.title, - summary: currDraft.summary, - summaryCustomized: currDraft.summaryCustomized, - content: _content, - contentMd, - tags: currTagContents, - cover: currArticle.cover, - collection: currCollectionIds, - archived: false, - publishState: PUBLISH_STATE.pending, - circleId: currArticleCircle?.circleId, - access: currArticleCircle?.access, - sensitiveByAuthor: currDraft?.sensitiveByAuthor, - license: currDraft?.license, - requestForDonation: currDraft?.requestForDonation, - replyToDonator: currDraft?.replyToDonator, - canComment: currDraft?.canComment, - // iscnPublish, - }, - lodash.isUndefined // to drop only undefined // _.isNil - ) - const revisedDraft = await draftService.baseCreate(data) - - // add job to publish queue - revisionQueue.publishRevisedArticle({ - draftId: revisedDraft.id, - iscnPublish, - }) - } - if (content) { - // check for content length limit - if (content.length > MAX_ARTICLE_CONTENT_LENGTH) { + if (stripHtml(content).length > MAX_ARTICLE_CONTENT_LENGTH) { throw new UserInputError('content reach length limit') } // check diff distances reaches limit or not - const diffs = measureDiffs( - stripHtml(normalizeArticleHTML(draft.content)), - stripHtml(normalizeArticleHTML(content)) + const { content: lastContent } = + await atomService.articleContentIdLoader.load(articleVersion.contentId) + const processed = normalizeArticleHTML( + sanitizeHTML(content, { maxEmptyParagraphs: -1 }) ) - if (diffs > MAX_ARTICLE_CONTENT_REVISION_LENGTH) { - throw new ArticleRevisionContentInvalidError('revised content invalid') + const changed = processed !== lastContent + + if (changed) { + checkRevisionCount(article.revisionCount + 1) + updateRevisionCount = true + data = { ...data, content: processed } } + } - if (diffs > 0) { - // only republish when have changes - await republish(content) + if (Object.keys(data).length > 0) { + const newArticleVersion = await articleService.createNewArticleVersion( + article.id, + viewer.id, + data, + description + ) + if (updateRevisionCount) { + await atomService.update({ + table: 'article', + where: { id: article.id }, + data: { revisionCount: article.revisionCount + 1 }, + }) } - } else if (isUpdatingCircleOrAccess) { - await republish() + revisionQueue.publishRevisedArticle({ + articleId: article.id, + newArticleVersionId: newArticleVersion.id, + oldArticleVersionId: articleVersion.id, + iscnPublish, + }) } - /** - * Result - */ - const node = await draftService.baseFindById(article.draftId) + // fetch latest article data + const node = await atomService.findUnique({ + table: 'article', + where: { id: dbId }, + }) + articleService.latestArticleVersionLoader.clearAll() // invalidate circle - if (circle) { - node[CACHE_KEYWORD] = [ + if (circleGlobalId) { + ;( + node as Article & { + [CACHE_KEYWORD]: Array<{ id: string; type: string }> + } + )[CACHE_KEYWORD] = [ { - id: circle.id, + id: fromGlobalId(circleGlobalId).id, type: NODE_TYPES.Circle, }, ] @@ -457,219 +367,4 @@ const resolver: GQLMutationResolvers['editArticle'] = async ( return node } -const handleTags = async ({ - viewerId, - tags, - article, - dataSources: { tagService }, -}: { - viewerId: string - tags: string[] | null - article: Article - dataSources: Pick -}) => { - // validate - const oldIds = ( - await tagService.findByArticleId({ articleId: article.id }) - ).map(({ id: tagId }: { id: string }) => tagId) - - if ( - tags && - tags.length > MAX_TAGS_PER_ARTICLE_LIMIT && - tags.length > oldIds.length - ) { - throw new TooManyTagsForArticleError( - `Not allow more than ${MAX_TAGS_PER_ARTICLE_LIMIT} tags on an article` - ) - } - - // create tag records - const tagEditors = environment.mattyId - ? [environment.mattyId, article.authorId] - : [article.authorId] - const dbTags = - tags === null - ? [] - : ( - await Promise.all( - tags.filter(Boolean).map(async (content: string) => - tagService.create( - { - content, - creator: article.authorId, - editors: tagEditors, - owner: article.authorId, - }, - { - columns: ['id', 'content'], - skipCreate: normalizeTagInput(content) !== content, // || content.length > MAX_TAG_CONTENT_LENGTH, - } - ) - ) - ) - ).map(({ id, content }) => ({ id: `${id}`, content })) - - const newIds = dbTags.map(({ id: tagId }) => tagId) - - // check if add tags include matty's tag - const mattyTagId = environment.mattyChoiceTagId || '' - const isMatty = environment.mattyId === viewerId - const addIds = difference(newIds, oldIds) - if (addIds.includes(mattyTagId) && !isMatty) { - throw new ForbiddenError('not allow to add official tag') - } - - // add - await tagService.createArticleTags({ - articleIds: [article.id], - creator: article.authorId, - tagIds: addIds, - }) - - // delete unwanted - await tagService.deleteArticleTagsByTagIds({ - articleId: article.id, - tagIds: difference(oldIds, newIds), - }) -} - -const handleConnection = async ({ - viewerId, - collection, - article, - dataSources: { - atomService, - userService, - articleService, - notificationService, - }, - knex, -}: { - viewerId: string - collection: string[] | null - article: Article - dataSources: Pick< - DataSources, - 'atomService' | 'userService' | 'articleService' | 'notificationService' - > - knex: Knex -}) => { - const oldIds = ( - await articleService.findConnections({ - entranceId: article.id, - }) - ).map(({ articleId }: { articleId: string }) => articleId) - const newIds = - collection === null - ? [] - : uniq(collection.map((articleId) => fromGlobalId(articleId).id)).filter( - (id) => !!id - ) - const newIdsToAdd = difference(newIds, oldIds) - const oldIdsToDelete = difference(oldIds, newIds) - - // do nothing if no change - if (isEqual(oldIds, newIds)) { - return - } - // only validate new-added articles - if (newIdsToAdd.length) { - if ( - newIds.length > MAX_ARTICLES_PER_CONNECTION_LIMIT && - newIds.length >= oldIds.length - ) { - throw new ArticleCollectionReachLimitError( - `Not allow more than ${MAX_ARTICLES_PER_CONNECTION_LIMIT} articles in connection` - ) - } - await Promise.all( - newIdsToAdd.map(async (articleId) => { - const collectedArticle = await atomService.findUnique({ - table: 'article', - where: { id: articleId }, - }) - - if (!collectedArticle) { - throw new ArticleNotFoundError(`Cannot find article ${articleId}`) - } - - if (collectedArticle.state !== ARTICLE_STATE.active) { - throw new ForbiddenError(`Article ${articleId} cannot be collected.`) - } - - const isBlocked = await userService.blocked({ - userId: collectedArticle.authorId, - targetId: viewerId, - }) - - if (isBlocked) { - throw new ForbiddenError('viewer has no permission') - } - }) - ) - } - - interface Item { - entranceId: string - articleId: string - order: number - } - const addItems: Item[] = [] - const updateItems: Item[] = [] - - // gather data - newIds.forEach((articleId: string, index: number) => { - const isNew = newIdsToAdd.includes(articleId) - if (isNew) { - addItems.push({ entranceId: article.id, articleId, order: index }) - } - if (!isNew && index !== oldIds.indexOf(articleId)) { - updateItems.push({ entranceId: article.id, articleId, order: index }) - } - }) - - await Promise.all([ - ...addItems.map((item) => - atomService.create({ - table: 'article_connection', - data: { - ...item, - }, - }) - ), - ...updateItems.map((item) => - atomService.update({ - table: 'article_connection', - where: { entranceId: item.entranceId, articleId: item.articleId }, - data: { order: item.order, updatedAt: knex.fn.now() }, - }) - ), - ]) - - // delete unwanted - await atomService.deleteMany({ - table: 'article_connection', - where: { entranceId: article.id }, - whereIn: ['article_id', oldIdsToDelete], - }) - - // trigger notifications - newIdsToAdd.forEach(async (articleId) => { - const targetCollection = await articleService.baseFindById(articleId) - notificationService.trigger({ - event: DB_NOTICE_TYPE.article_new_collected, - recipientId: targetCollection.authorId, - actorId: article.authorId, - entities: [ - { type: 'target', entityTable: 'article', entity: targetCollection }, - { - type: 'collection', - entityTable: 'article', - entity: article, - }, - ], - }) - }) -} - export default resolver diff --git a/src/mutations/article/index.ts b/src/mutations/article/index.ts index b44ea0c3d..ce45be2b3 100644 --- a/src/mutations/article/index.ts +++ b/src/mutations/article/index.ts @@ -2,16 +2,12 @@ import addArticlesTags from './addArticlesTags' import appreciateArticle from './appreciateArticle' import deleteArticlesTags from './deleteArticlesTags' import deleteTags from './deleteTags' -import deleteTopics from './deleteTopics' import editArticle from './editArticle' import mergeTags from './mergeTags' import publishArticle from './publishArticle' -import putChapter from './putChapter' import putTag from './putTag' -import putTopic from './putTopic' import readArticle from './readArticle' import renameTag from './renameTag' -import sortTopics from './sortTopics' import toggleArticleRecommend from './toggleArticleRecommend' import toggleSubscribeArticle from './toggleSubscribeArticle' import toggleTagRecommend from './toggleTagRecommend' @@ -39,9 +35,5 @@ export default { updateArticlesTags, updateTagSetting, toggleTagRecommend, - putChapter, - putTopic, - deleteTopics, - sortTopics, }, } diff --git a/src/mutations/article/mergeTags.ts b/src/mutations/article/mergeTags.ts index 844d160ba..a36d3f2e8 100644 --- a/src/mutations/article/mergeTags.ts +++ b/src/mutations/article/mergeTags.ts @@ -1,4 +1,4 @@ -import type { GQLMutationResolvers } from 'definitions' +import type { GQLMutationResolvers, Tag } from 'definitions' import { CACHE_KEYWORD, NODE_TYPES } from 'common/enums' import { environment } from 'common/environment' @@ -24,7 +24,9 @@ const resolver: GQLMutationResolvers['mergeTags'] = async ( }) // invalidate extra nodes - newTag[CACHE_KEYWORD] = tagIds.map((id) => ({ id, type: NODE_TYPES.Tag })) + ;(newTag as Tag & { [CACHE_KEYWORD]: any })[CACHE_KEYWORD] = tagIds.map( + (id) => ({ id, type: NODE_TYPES.Tag }) + ) return newTag } diff --git a/src/mutations/article/publishArticle.ts b/src/mutations/article/publishArticle.ts index e5b9a7802..f695c9782 100644 --- a/src/mutations/article/publishArticle.ts +++ b/src/mutations/article/publishArticle.ts @@ -16,6 +16,7 @@ const resolver: GQLMutationResolvers['publishArticle'] = async ( viewer, dataSources: { draftService, + atomService, queues: { publicationQueue }, }, } @@ -34,7 +35,7 @@ const resolver: GQLMutationResolvers['publishArticle'] = async ( // retrive data from draft const { id: draftDBId } = fromGlobalId(id) - const draft = await draftService.loadById(draftDBId) + const draft = await atomService.draftIdLoader.load(draftDBId) const isPublished = draft.publishState === PUBLISH_STATE.published if (draft.authorId !== viewer.id || (draft.archived && !isPublished)) { diff --git a/src/mutations/article/putChapter.ts b/src/mutations/article/putChapter.ts deleted file mode 100644 index 966e1d650..000000000 --- a/src/mutations/article/putChapter.ts +++ /dev/null @@ -1,255 +0,0 @@ -import type { GQLMutationResolvers } from 'definitions' - -import _difference from 'lodash/difference' -import _inter from 'lodash/intersection' -import _uniq from 'lodash/uniq' - -import { USER_STATE } from 'common/enums' -import { UserInputError } from 'common/errors' -import { - AuthenticationError, - ForbiddenByStateError, - ForbiddenError, -} from 'common/errors' -import { fromGlobalId } from 'common/utils' - -const resolver: GQLMutationResolvers['putChapter'] = async ( - _, - { input: { id, articles, topic: topicGlobalId, ...rest } }, - { viewer, dataSources: { atomService } } -) => { - // access control - if (!viewer.userName) { - throw new ForbiddenError('user has no username') - } - - if (viewer.state === USER_STATE.frozen) { - throw new ForbiddenByStateError(`${viewer.state} user has no permission`) - } - - // prepare data - const properties = { updatedAt: new Date(), ...rest } as typeof rest & { - updatedAt: Date - topicId: string - } - - // helper function to check article ids validity - const checkArticleIds = async (ids: string[]) => { - if (ids.length > 0) { - const articlesObjs = await atomService.findMany({ - table: 'article', - whereIn: ['id', ids], - }) - - if (articlesObjs.length !== ids.length) { - throw new UserInputError('some articles cannot be found.') - } - - articlesObjs.map((articleObj) => { - if (articleObj.authorId !== viewer.id) { - throw new AuthenticationError( - 'users can only update their own articles.' - ) - } - }) - } - } - - /** - * update - * when id is provided - */ - if (id) { - const { id: chapterDbId } = fromGlobalId(id) - - let chapter = await atomService.findUnique({ - table: 'chapter', - where: { id: chapterDbId }, - }) - - if (!chapter) { - throw new UserInputError('cannot find chapter.') - } - - const topic = await atomService.findUnique({ - table: 'topic', - where: { id: chapter.topicId }, - }) - - if (!topic) { - throw new UserInputError('cannot find topic of chapter.') - } - - // check topic ownership - if (topic.userId !== viewer.id) { - throw new AuthenticationError( - 'users can only update chapters in their own topics' - ) - } - - // if a new topic id is provided, we are moving to a new topic - if (topicGlobalId && fromGlobalId(topicGlobalId).id !== chapter.topicId) { - const newTopic = await atomService.findUnique({ - table: 'topic', - where: { id: fromGlobalId(topicGlobalId).id }, - }) - - // check new topic ownership - if (newTopic.userId !== viewer.id) { - throw new AuthenticationError( - 'users can only update chapters in their own topics' - ) - } - - // add to properties - properties.topicId = newTopic.id - } - - // update properties in chapterDbId table - if (Object.keys(properties).length > 0) { - chapter = await atomService.update({ - table: 'chapter', - where: { id: chapterDbId }, - data: properties, - }) - } - - // update article order or insert new articles in article_topic table - if (articles && articles.length > 0) { - // get unique ids from input - const newIds = _uniq(articles).map( - (globalId) => fromGlobalId(globalId).id - ) - - // get existing articles - const oldIds = ( - await atomService.findMany({ - table: 'article_chapter', - where: { chapterId: chapterDbId }, - }) - ).map((record) => record.articleId) - - // determine articles to be removed, added and updated - const addIds = _difference(newIds, oldIds) as string[] - const removeIds = _difference(oldIds, newIds) - const updateIds = _inter(newIds, oldIds) - - // check validity of added ids - await checkArticleIds(addIds) - - // updated - await Promise.all( - updateIds.map((articleId) => - atomService.update({ - table: 'article_chapter', - where: { chapterId: chapterDbId, articleId }, - data: { order: newIds.indexOf(articleId), updatedAt: new Date() }, - }) - ) - ) - - // create - await Promise.all( - addIds.map((articleId) => - atomService.create({ - table: 'article_chapter', - data: { - chapterId: chapterDbId, - articleId, - order: newIds.indexOf(articleId), - }, - }) - ) - ) - - // remove - await Promise.all( - removeIds.map((articleId) => - atomService.deleteMany({ - table: 'article_chapter', - where: { - chapterId: chapterDbId, - articleId, - }, - }) - ) - ) - } - - return chapter - } - - /** - * create - * when id is provided - */ - if (!id) { - // check input validity - if (!rest.title || !topicGlobalId) { - throw new UserInputError( - 'Title and topic is required for creating chapter.' - ) - } - - // check topic - const { id: topicDbId } = fromGlobalId(topicGlobalId) - - const topic = await atomService.findUnique({ - table: 'topic', - where: { id: topicDbId }, - }) - - if (!topic) { - throw new UserInputError('Topic not found') - } - - if (topic.userId !== viewer.id) { - throw new AuthenticationError( - 'Users can only create chapter in their own topics' - ) - } - - properties.topicId = topic.id - - // get default order - const order = - ((await atomService.max({ - table: 'chapter', - column: 'order', - where: { topicId: topicDbId }, - })) || 0) + 1 - - // create record in chapter table - const chapter = await atomService.create({ - table: 'chapter', - data: { order, ...properties }, - }) - - // create references to articles in article_chapter - if (articles && articles.length > 0) { - // get unique ids from input - const ids = _uniq(articles).map( - (globalId) => fromGlobalId(globalId).id - ) as string[] - - // check validity of article ids - await checkArticleIds(ids) - - await Promise.all( - ids.map((articleId, index) => - atomService.create({ - table: 'article_chapter', - data: { - chapterId: chapter.id, - articleId, - order: index, - }, - }) - ) - ) - } - return chapter - } -} - -export default resolver diff --git a/src/mutations/article/putTag.ts b/src/mutations/article/putTag.ts index cf3102326..1882ba705 100644 --- a/src/mutations/article/putTag.ts +++ b/src/mutations/article/putTag.ts @@ -24,7 +24,7 @@ import { const resolver: GQLMutationResolvers['putTag'] = async ( _, { input: { id, content, cover, description } }, - { viewer, dataSources: { systemService, tagService } } + { viewer, dataSources: { systemService, tagService, atomService } } ) => { if (!viewer.userName) { throw new ForbiddenError('user has no username') @@ -131,7 +131,10 @@ const resolver: GQLMutationResolvers['putTag'] = async ( // delete unused tag cover if (tag.cover && tag.cover !== updateTag.cover) { - const coverAsset = await tagService.baseFindById(tag.cover, 'asset') + const coverAsset = await atomService.findUnique({ + where: { id: tag.cover }, + table: 'asset', + }) if (coverAsset) { await systemService.deleteAssetAndAssetMap({ [`${coverAsset.id}`]: coverAsset.path, diff --git a/src/mutations/article/putTopic.ts b/src/mutations/article/putTopic.ts deleted file mode 100644 index 75d836dfe..000000000 --- a/src/mutations/article/putTopic.ts +++ /dev/null @@ -1,254 +0,0 @@ -import type { GQLMutationResolvers } from 'definitions' - -import _difference from 'lodash/difference' -import _inter from 'lodash/intersection' -import _uniq from 'lodash/uniq' - -import { ASSET_TYPE, USER_STATE } from 'common/enums' -import { - AssetNotFoundError, - AuthenticationError, - ForbiddenByStateError, - ForbiddenError, - UserInputError, -} from 'common/errors' -import { fromGlobalId } from 'common/utils' - -const resolver: GQLMutationResolvers['putTopic'] = async ( - _, - { input: { id, chapters, articles, cover, ...rest } }, - { viewer, dataSources: { atomService, systemService } } -) => { - // access control - if (!viewer.userName) { - throw new ForbiddenError('user has no username') - } - - if (viewer.state === USER_STATE.frozen) { - throw new ForbiddenByStateError(`${viewer.state} user has no permission`) - } - - // prepare data - const properties = { updatedAt: new Date(), ...rest } as typeof rest & { - cover?: string - } - - // map cover to cover id - if (cover) { - const asset = await systemService.findAssetByUUID(cover) - if ( - !asset || - asset.type !== ASSET_TYPE.topicCover || - asset.authorId !== viewer.id - ) { - throw new AssetNotFoundError('topic cover asset does not exists') - } - properties.cover = asset.id - } - - /** - * update - * when id is provided - */ - if (id) { - const { id: topicDbId } = fromGlobalId(id) - let topic = await atomService.findUnique({ - table: 'topic', - where: { id: topicDbId }, - }) - - if (!topic) { - throw new UserInputError('cannot find topic.') - } - - if (topic.userId !== viewer.id) { - throw new AuthenticationError('users can only update their own topics.') - } - - // update properties in topic table - if (Object.keys(properties).length > 0) { - topic = await atomService.update({ - table: 'topic', - where: { id: topicDbId }, - data: properties, - }) - } - - // update chapter order in chapter table - if (chapters && chapters.length > 0) { - // get chapter db ids - const chapterDbIds = _uniq(chapters).map( - (chapterGlobalId) => fromGlobalId(chapterGlobalId).id - ) - - // join to get topic owner - const chapterObjs = await atomService.knex - .select('chapter.id', 'topic.user_id') - .from('chapter') - .join('topic', 'chapter.topic_id', 'topic.id') - .whereIn('chapter.id', chapterDbIds) - - // check validity - if (chapterObjs.length !== chapterDbIds.length) { - throw new UserInputError('some chapters cannot be found.') - } - - chapterObjs.map((chapter) => { - if (chapter.userId !== viewer.id) { - throw new AuthenticationError( - 'users can only update their own chapters.' - ) - } - }) - - // update record - await Promise.all( - chapterDbIds.map((chapterDbId, index) => - atomService.update({ - table: 'chapter', - where: { id: chapterDbId, topicId: topicDbId }, - data: { order: index, updatedAt: new Date() }, - }) - ) - ) - } - - // update article order or insert new articles in article_topic table - if (articles && articles.length > 0) { - // get unique ids from input - const newIds = _uniq(articles).map( - (globalId) => fromGlobalId(globalId).id - ) - - // get existing articles - const oldIds = ( - await atomService.findMany({ - table: 'article_topic', - where: { topicId: topicDbId }, - }) - ).map((record) => record.articleId) - - // determine articles to be removed, added and updated - const addIds = _difference(newIds, oldIds) - const removeIds = _difference(oldIds, newIds) - const updateIds = _inter(newIds, oldIds) - - // check validity of added articles - const articlesObjs = await atomService.findMany({ - table: 'article', - whereIn: ['id', addIds], - }) - - if (articlesObjs.length !== addIds.length) { - throw new UserInputError('some articles cannot be found.') - } - - articlesObjs.map((articleObj) => { - if (articleObj.authorId !== viewer.id) { - throw new AuthenticationError( - 'users can only update their own articles.' - ) - } - }) - - // updated - await Promise.all( - updateIds.map((articleId) => - atomService.update({ - table: 'article_topic', - where: { topicId: topicDbId, articleId }, - data: { order: newIds.indexOf(articleId), updatedAt: new Date() }, - }) - ) - ) - - // create - await Promise.all( - addIds.map((articleId) => - atomService.create({ - table: 'article_topic', - data: { - topicId: topicDbId, - articleId, - order: newIds.indexOf(articleId), - }, - }) - ) - ) - - // remove - await Promise.all( - removeIds.map((articleId) => - atomService.deleteMany({ - table: 'article_topic', - where: { - topicId: topicDbId, - articleId, - }, - }) - ) - ) - } - return topic - } - - /** - * create - * when id is provided - */ - if (!id) { - // check input validity - if (!rest.title) { - throw new UserInputError('Title is required for creating topic.') - } - - if (chapters && chapters.length > 0) { - throw new UserInputError( - 'Cannot add chapter when creating topic. Use `putChapter` after creating topic.' - ) - } - - // update orders - const userTopics = await atomService.findMany({ - table: 'topic', - where: { user_id: viewer.id }, - }) - await Promise.all( - userTopics.map((topic) => - atomService.update({ - table: 'topic', - where: { id: topic.id }, - data: { - order: topic.order + 1, - updatedAt: new Date(), - }, - }) - ) - ) - - // create record in topic table - const newTopic = await atomService.create({ - table: 'topic', - data: { userId: viewer.id, order: 0, ...properties }, - }) - - // create references to articles in article_topic - if (articles && articles.length > 0) { - await Promise.all( - articles.map((article, index) => - atomService.create({ - table: 'article_topic', - data: { - topicId: newTopic.id, - articleId: fromGlobalId(article).id, - order: index, - }, - }) - ) - ) - } - return newTopic - } -} - -export default resolver diff --git a/src/mutations/article/readArticle.ts b/src/mutations/article/readArticle.ts index 19b0ec042..7408e4208 100644 --- a/src/mutations/article/readArticle.ts +++ b/src/mutations/article/readArticle.ts @@ -10,7 +10,7 @@ const logger = getLogger('mutation-read-article') const resolver: GQLMutationResolvers['readArticle'] = async ( _, { input: { id } }, - { viewer, dataSources: { atomService, articleService, draftService } } + { viewer, dataSources: { atomService, articleService } } ) => { const { id: dbId } = fromGlobalId(id) @@ -19,22 +19,10 @@ const resolver: GQLMutationResolvers['readArticle'] = async ( where: { id: dbId, state: ARTICLE_STATE.active }, }) if (!article) { - logger.warn('target article %s does not exists', article.id) + logger.warn('target article %s does not exists', id) throw new ArticleNotFoundError('target article does not exists') } - const node = await draftService.baseFindById(article.draftId) - if (!node) { - logger.warn( - 'target article %s linked draft %s does not exists', - article.id, - article.draftId - ) - throw new ArticleNotFoundError( - 'target article linked draft does not exists' - ) - } - // only record if viewer read others articles if (viewer.id !== article.authorId) { await articleService.read({ @@ -44,7 +32,7 @@ const resolver: GQLMutationResolvers['readArticle'] = async ( }) } - return node + return article } export default resolver diff --git a/src/mutations/article/sortTopics.ts b/src/mutations/article/sortTopics.ts deleted file mode 100644 index d18863e32..000000000 --- a/src/mutations/article/sortTopics.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { GQLMutationResolvers } from 'definitions' - -import _difference from 'lodash/difference' -import _inter from 'lodash/intersection' -import _uniq from 'lodash/uniq' - -import { ForbiddenError } from 'common/errors' -import { fromGlobalId } from 'common/utils' - -const resolver: GQLMutationResolvers['sortTopics'] = async ( - _, - { input: { ids } }, - { viewer, dataSources: { atomService } } -) => { - const topicIds = _uniq(ids.map((id) => fromGlobalId(id).id)) - - // check permission - const topics = await atomService.findMany({ - table: 'topic', - whereIn: ['id', topicIds], - }) - topics.forEach((topic) => { - if (topic.userId !== viewer.id) { - throw new ForbiddenError('viewer has no permission') - } - }) - - // remake orders - const userTopics = await atomService.findMany({ - table: 'topic', - where: { user_id: viewer.id }, - orderBy: [{ column: 'order', order: 'asc' }], - }) - const userTopicIds = userTopics.map((topic) => topic.id) - const restIds = _difference(userTopicIds, topicIds) - const newOrderedTopicIds = [...topicIds, ...restIds] - - // update orders - await Promise.all( - newOrderedTopicIds.map((topicId, index) => - atomService.update({ - table: 'topic', - where: { id: topicId }, - data: { - order: index, - updatedAt: new Date(), - }, - }) - ) - ) - - return atomService.findMany({ - table: 'topic', - where: { user_id: viewer.id }, - orderBy: [{ column: 'order', order: 'asc' }], - }) -} - -export default resolver diff --git a/src/mutations/article/toggleArticleRecommend.ts b/src/mutations/article/toggleArticleRecommend.ts index 3e8493c51..98dba7f53 100644 --- a/src/mutations/article/toggleArticleRecommend.ts +++ b/src/mutations/article/toggleArticleRecommend.ts @@ -4,25 +4,25 @@ import { ArticleNotFoundError } from 'common/errors' import { fromGlobalId } from 'common/utils' const resolver: GQLMutationResolvers['toggleArticleRecommend'] = async ( - root, + _, { input: { id, enabled, type = 'icymi' } }, - { viewer, dataSources: { atomService, articleService, draftService } } + { dataSources: { atomService } } ) => { const { id: dbId } = fromGlobalId(id) - const article = await articleService.dataloader.load(dbId) + const article = await atomService.articleIdLoader.load(dbId) if (!article) { throw new ArticleNotFoundError('target article does not exists') } switch (type) { - case 'icymi': + case 'icymi': { if (enabled) { const data = { articleId: dbId } await atomService.upsert({ table: 'matters_choice', where: data, create: data, - update: { ...data, updatedAt: new Date() }, + update: data, }) } else { await atomService.deleteMany({ @@ -31,7 +31,8 @@ const resolver: GQLMutationResolvers['toggleArticleRecommend'] = async ( }) } break - case 'hottest': + } + case 'hottest': { await atomService.upsert({ table: 'article_recommend_setting', where: { articleId: dbId }, @@ -39,7 +40,8 @@ const resolver: GQLMutationResolvers['toggleArticleRecommend'] = async ( update: { inHottest: enabled }, }) break - case 'newest': + } + case 'newest': { await atomService.upsert({ table: 'article_recommend_setting', where: { articleId: dbId }, @@ -47,10 +49,9 @@ const resolver: GQLMutationResolvers['toggleArticleRecommend'] = async ( update: { inNewest: enabled }, }) break + } } - - const node = await draftService.baseFindById(article.draftId) - return node + return article } export default resolver diff --git a/src/mutations/article/toggleSubscribeArticle.ts b/src/mutations/article/toggleSubscribeArticle.ts index 4d099df71..635c1c56c 100644 --- a/src/mutations/article/toggleSubscribeArticle.ts +++ b/src/mutations/article/toggleSubscribeArticle.ts @@ -5,6 +5,7 @@ import { DB_NOTICE_TYPE, USER_ACTION, USER_STATE, + ARTICLE_ACTION, } from 'common/enums' import { ArticleNotFoundError, @@ -16,7 +17,7 @@ import { fromGlobalId } from 'common/utils' const resolver: GQLMutationResolvers['toggleSubscribeArticle'] = async ( _, { input: { id, enabled } }, - { viewer, dataSources: { atomService, draftService, notificationService } } + { viewer, dataSources: { atomService, articleService, notificationService } } ) => { // checks if (!viewer.userName) { @@ -28,7 +29,7 @@ const resolver: GQLMutationResolvers['toggleSubscribeArticle'] = async ( } const { id: dbId } = fromGlobalId(id) - // banned and archived articles shall still be abled to be unsubscribed + // banned and archived articles shall still be able to be unsubscribed const article = enabled === false ? await atomService.findFirst({ @@ -50,6 +51,8 @@ const resolver: GQLMutationResolvers['toggleSubscribeArticle'] = async ( if (!article) { throw new ArticleNotFoundError('target article does not exists') } + const { id: articleVersionId } = + await articleService.loadLatestArticleVersion(article.id) // determine action let action: 'subscribe' | 'unsubscribe' @@ -59,7 +62,7 @@ const resolver: GQLMutationResolvers['toggleSubscribeArticle'] = async ( where: { targetId: article.id, userId: viewer.id, - action: USER_ACTION.subscribe, + action: ARTICLE_ACTION.subscribe, }, }) action = userSubscribe ? 'unsubscribe' : 'subscribe' @@ -72,13 +75,13 @@ const resolver: GQLMutationResolvers['toggleSubscribeArticle'] = async ( const data = { targetId: article.id, userId: viewer.id, - action: USER_ACTION.subscribe, + action: ARTICLE_ACTION.subscribe, } await atomService.upsert({ table: 'action_article', where: data, - create: data, - update: { ...data, updatedAt: new Date() }, + create: { ...data, articleVersionId }, + update: { ...data, articleVersionId }, }) // trigger notifications @@ -99,8 +102,7 @@ const resolver: GQLMutationResolvers['toggleSubscribeArticle'] = async ( }) } - const node = await draftService.baseFindById(article.draftId) - return node + return article } export default resolver diff --git a/src/mutations/article/toggleTagRecommend.ts b/src/mutations/article/toggleTagRecommend.ts index 11bcfb301..af29d00e1 100644 --- a/src/mutations/article/toggleTagRecommend.ts +++ b/src/mutations/article/toggleTagRecommend.ts @@ -6,10 +6,10 @@ import { fromGlobalId } from 'common/utils' const resolver: GQLMutationResolvers['toggleTagRecommend'] = async ( _, { input: { id, enabled } }, - { dataSources: { tagService } } + { dataSources: { tagService, atomService } } ) => { const { id: dbId } = fromGlobalId(id) - const tag = await tagService.loadById(dbId) + const tag = await atomService.tagIdLoader.load(dbId) if (!tag) { throw new TagNotFoundError('target tag does not exists') } diff --git a/src/mutations/article/updateArticleSensitive.ts b/src/mutations/article/updateArticleSensitive.ts index 4d64aaf85..826cfabcd 100644 --- a/src/mutations/article/updateArticleSensitive.ts +++ b/src/mutations/article/updateArticleSensitive.ts @@ -6,21 +6,27 @@ import { fromGlobalId } from 'common/utils' const resolver: GQLMutationResolvers['updateArticleSensitive'] = async ( _, { input: { id, sensitive } }, - { dataSources: { articleService, draftService } } + { dataSources: { atomService } } ) => { const { id: dbId } = fromGlobalId(id) - const article = await articleService.baseFindById(dbId) + const article = await atomService.findUnique({ + table: 'article', + where: { id: dbId }, + }) if (!article) { throw new ArticleNotFoundError('article does not exist') } - const draft = await draftService.baseUpdate(article.draftId, { - sensitiveByAdmin: sensitive, - updatedAt: new Date(), + const updated = await atomService.update({ + data: { + sensitiveByAdmin: sensitive, + }, + table: 'article', + where: { id: dbId }, }) - return draft + return updated } export default resolver diff --git a/src/mutations/article/updateArticleState.ts b/src/mutations/article/updateArticleState.ts index e5c7e2ebe..2b5db0ed8 100644 --- a/src/mutations/article/updateArticleState.ts +++ b/src/mutations/article/updateArticleState.ts @@ -1,45 +1,32 @@ -import type { GQLMutationResolvers, UserHasUsername } from 'definitions' +import type { GQLMutationResolvers } from 'definitions' import { ARTICLE_STATE, OFFICIAL_NOTICE_EXTEND_TYPE } from 'common/enums' -// import { environment } from 'common/environment' import { fromGlobalId } from 'common/utils' const resolver: GQLMutationResolvers['updateArticleState'] = async ( _, { input: { id, state } }, - { - dataSources: { - userService, - articleService, - draftService, - notificationService, - queues: { publicationQueue }, - }, - } + { dataSources: { atomService, notificationService } } ) => { const { id: dbId } = fromGlobalId(id) - const article = await articleService.baseUpdate(dbId, { - state, - updatedAt: new Date(), + const article = await atomService.update({ + table: 'article', + where: { id: dbId }, + data: { + state, + }, }) - const user = (await userService.loadById(article.authorId)) as UserHasUsername - // trigger notification if (state === ARTICLE_STATE.banned) { notificationService.trigger({ event: OFFICIAL_NOTICE_EXTEND_TYPE.article_banned, entities: [{ type: 'target', entityTable: 'article', entity: article }], - recipientId: user.id, + recipientId: article.authorId, }) } - - const { userName } = user - publicationQueue.refreshIPNSFeed({ userName }) - - const node = await draftService.baseFindById(article.draftId) - return node + return article } export default resolver diff --git a/src/mutations/article/updateTagSetting.ts b/src/mutations/article/updateTagSetting.ts index b18cdae92..4f8ae9dc9 100644 --- a/src/mutations/article/updateTagSetting.ts +++ b/src/mutations/article/updateTagSetting.ts @@ -1,4 +1,4 @@ -import type { GQLMutationResolvers } from 'definitions' +import type { GQLMutationResolvers, Tag } from 'definitions' import _difference from 'lodash/difference' import _some from 'lodash/some' @@ -66,7 +66,7 @@ const resolver: GQLMutationResolvers['updateTagSetting'] = async ( // update updatedTag = await tagService.baseUpdate(tagId, { owner: viewer.id, - editors: _uniq([...tag.editors, viewer.id]), + editors: _uniq([...(tag.editors ?? []), viewer.id]), }) break @@ -104,14 +104,14 @@ const resolver: GQLMutationResolvers['updateTagSetting'] = async ( (editors .map((editor) => { const { id: editorId } = fromGlobalId(editor) - if (!tag.editors.includes(editorId)) { + if (!(tag.editors ?? []).includes(editorId)) { return editorId } }) .filter((editorId) => editorId !== undefined) as string[]) || [] // editors composed by 4 editors, matty and owner - const dedupedEditors = _uniq([...tag.editors, ...newEditors]) + const dedupedEditors = _uniq([...(tag.editors ?? []), ...newEditors]) if (dedupedEditors.length > 6) { throw new TagEditorsReachLimitError('number of editors reaches limit') } @@ -173,7 +173,9 @@ const resolver: GQLMutationResolvers['updateTagSetting'] = async ( if (updatedTag) { // invalidate extra nodes - updatedTag[CACHE_KEYWORD] = [{ id: viewer.id, type: NODE_TYPES.User }] + ;(updatedTag as Tag & { [CACHE_KEYWORD]: any })[CACHE_KEYWORD] = [ + { id: viewer.id, type: NODE_TYPES.User }, + ] } return updatedTag } diff --git a/src/mutations/circle/invite.ts b/src/mutations/circle/invite.ts index af3da814a..0b8b1ee42 100644 --- a/src/mutations/circle/invite.ts +++ b/src/mutations/circle/invite.ts @@ -220,7 +220,7 @@ const resolver: GQLMutationResolvers['invite'] = async ( sender: { displayName: viewer.displayName, }, - to: recipient?.email || email, + to: (recipient?.email || email) as string, }) } } diff --git a/src/mutations/circle/putCircleArticles.ts b/src/mutations/circle/putCircleArticles.ts index f5c29cba1..dc93282a8 100644 --- a/src/mutations/circle/putCircleArticles.ts +++ b/src/mutations/circle/putCircleArticles.ts @@ -1,11 +1,6 @@ -import type { GQLMutationResolvers } from 'definitions' +import type { GQLMutationResolvers, Article, Circle } from 'definitions' -import { - normalizeArticleHTML, - sanitizeHTML, -} from '@matters/matters-editor/transformers' import { uniq } from 'lodash' -import { v4 } from 'uuid' import { ARTICLE_LICENSE_TYPE, @@ -17,9 +12,8 @@ import { MAX_ARTICLE_REVISION_COUNT, NODE_TYPES, PRICE_STATE, - PUBLISH_STATE, - SUBSCRIPTION_STATE, USER_STATE, + SUBSCRIPTION_STATE, } from 'common/enums' import { ArticleNotFoundError, @@ -40,8 +34,6 @@ const resolver: GQLMutationResolvers['putCircleArticles'] = async ( dataSources: { atomService, systemService, - draftService, - tagService, articleService, notificationService, connections: { knex }, @@ -94,7 +86,7 @@ const resolver: GQLMutationResolvers['putCircleArticles'] = async ( throw new ArticleNotFoundError('articles not found') } - const republish = async (article: any) => { + const republish = async (article: Article) => { const revisionCount = article.revisionCount || 0 if (revisionCount >= MAX_ARTICLE_REVISION_COUNT) { throw new ArticleRevisionReachLimitError( @@ -103,63 +95,20 @@ const resolver: GQLMutationResolvers['putCircleArticles'] = async ( } // fetch updated data before create draft - const [ - currDraft, - currArticle, - currCollections, - currTags, - currArticleCircle, - ] = await Promise.all([ - draftService.baseFindById(article.draftId), // fetch latest draft - articleService.baseFindById(article.id), // fetch latest article + const [oldArticleVersion] = await Promise.all([ + articleService.loadLatestArticleVersion(article.id), articleService.findConnections({ entranceId: article.id }), - tagService.findByArticleId({ articleId: article.id }), - articleService.findArticleCircle(article.id), ]) - const currTagContents = currTags.map((currTag) => currTag.content) - const currCollectionIds = currCollections.map( - ({ articleId }: { articleId: string }) => articleId - ) - - // create draft linked to this article - const data: Record = { - uuid: v4(), - authorId: currDraft.authorId, - articleId: currArticle.id, - title: currDraft.title, - summary: currDraft.summary, - summaryCustomized: currDraft.summaryCustomized, - content: normalizeArticleHTML(sanitizeHTML(currDraft.content)), - tags: currTagContents, - cover: currArticle.cover, - collection: currCollectionIds, - archived: false, - publishState: PUBLISH_STATE.pending, - circleId: currArticleCircle?.circleId, - access: currArticleCircle?.access, - sensitiveByAuthor: currDraft?.sensitiveByAuthor, - license: currDraft.license, - requestForDonation: currDraft?.requestForDonation, - replyToDonator: currDraft?.replyToDonator, - canComment: currDraft?.canComment, - iscnPublish: currDraft.iscnPublish, - } - const revisedDraft = await draftService.baseCreate(data) - // add job to publish queue + const newArticleVersion = await articleService.createNewArticleVersion( + article.id, + viewer.id, + { license: license || ARTICLE_LICENSE_TYPE.cc_by_nc_nd_4 } + ) revisionQueue.publishRevisedArticle({ - draftId: revisedDraft.id, - }) - } - - const editLicense = async (draftId: string) => { - await atomService.update({ - table: 'draft', - where: { id: draftId }, - data: { - license: license || ARTICLE_LICENSE_TYPE.cc_by_nc_nd_4, - updatedAt: knex.fn.now(), // new Date(), - }, + articleId: article.id, + oldArticleVersionId: oldArticleVersion.id, + newArticleVersionId: newArticleVersion.id, }) } @@ -184,7 +133,7 @@ const resolver: GQLMutationResolvers['putCircleArticles'] = async ( ]) const followers = await atomService.findMany({ table: 'action_circle', - select: ['user_id'], + select: ['userId'], where: { targetId: circleId, action: CIRCLE_ACTION.follow }, }) const recipients = uniq([ @@ -201,16 +150,13 @@ const resolver: GQLMutationResolvers['putCircleArticles'] = async ( update: { ...data, access: accessType, - updatedAt: knex.fn.now(), - // new Date(), }, }) - await editLicense(article.draftId) await republish(article) // notify - recipients.forEach((recipientId: any) => { + recipients.forEach((recipientId: string) => { notificationService.trigger({ event: DB_NOTICE_TYPE.circle_new_article, recipientId, @@ -230,16 +176,17 @@ const resolver: GQLMutationResolvers['putCircleArticles'] = async ( }) for (const article of targetArticles) { - await editLicense(article.draftId) await republish(article) } } // invalidate articles - circle[CACHE_KEYWORD] = targetArticles.map((article) => ({ - id: article.id, - type: NODE_TYPES.Article, - })) + articleService.latestArticleVersionLoader.clearAll() + ;(circle as Circle & { [CACHE_KEYWORD]: any })[CACHE_KEYWORD] = + targetArticles.map((article) => ({ + id: article.id, + type: NODE_TYPES.Article, + })) return circle } diff --git a/src/mutations/circle/subscribeCircle.ts b/src/mutations/circle/subscribeCircle.ts index b14fa6f4b..48eddff65 100644 --- a/src/mutations/circle/subscribeCircle.ts +++ b/src/mutations/circle/subscribeCircle.ts @@ -1,4 +1,4 @@ -import type { Customer, GQLMutationResolvers } from 'definitions' +import type { GQLMutationResolvers } from 'definitions' import { invalidateFQC } from '@matters/apollo-response-cache' import { compare } from 'bcrypt' @@ -99,20 +99,19 @@ const resolver: GQLMutationResolvers['subscribeCircle'] = async ( } // retrieve or create a Customer - let customer = (await atomService.findFirst({ - table: 'customer', - where: { - userId: viewer.id, - provider: PAYMENT_PROVIDER.stripe, - archived: false, - }, - })) as Customer - if (!customer) { - customer = (await paymentService.createCustomer({ + const customer = + (await atomService.findFirst({ + table: 'customer', + where: { + userId: viewer.id, + provider: PAYMENT_PROVIDER.stripe, + archived: false, + }, + })) || + (await paymentService.createCustomer({ user: viewer, provider: PAYMENT_PROVIDER.stripe, - })) as Customer - } + })) // check subscription const subscriptions = await paymentService.findActiveSubscriptions({ diff --git a/src/mutations/circle/toggleFollowCircle.ts b/src/mutations/circle/toggleFollowCircle.ts index 2147401d3..c12ec6e0f 100644 --- a/src/mutations/circle/toggleFollowCircle.ts +++ b/src/mutations/circle/toggleFollowCircle.ts @@ -1,4 +1,4 @@ -import type { GQLMutationResolvers } from 'definitions' +import type { GQLMutationResolvers, Circle } from 'definitions' import { CACHE_KEYWORD, @@ -98,7 +98,7 @@ const resolver: GQLMutationResolvers['toggleFollowCircle'] = async ( } // invalidate cache - circle[CACHE_KEYWORD] = [ + ;(circle as Circle & { [CACHE_KEYWORD]: any })[CACHE_KEYWORD] = [ { id: viewer.id, type: NODE_TYPES.User, diff --git a/src/mutations/circle/unsubscribeCircle.ts b/src/mutations/circle/unsubscribeCircle.ts index 5cb911706..012fa895d 100644 --- a/src/mutations/circle/unsubscribeCircle.ts +++ b/src/mutations/circle/unsubscribeCircle.ts @@ -1,4 +1,4 @@ -import type { GQLMutationResolvers } from 'definitions' +import type { GQLMutationResolvers, Circle } from 'definitions' import { CACHE_KEYWORD, @@ -153,7 +153,7 @@ const resolver: GQLMutationResolvers['unsubscribeCircle'] = async ( }) // invalidate cache - circle[CACHE_KEYWORD] = [ + ;(circle as Circle & { [CACHE_KEYWORD]: any })[CACHE_KEYWORD] = [ { id: viewer.id, type: NODE_TYPES.User, diff --git a/src/mutations/collection/addCollectionsArticles.ts b/src/mutations/collection/addCollectionsArticles.ts index 09c730c31..ff9d7cd2c 100644 --- a/src/mutations/collection/addCollectionsArticles.ts +++ b/src/mutations/collection/addCollectionsArticles.ts @@ -20,7 +20,7 @@ import { fromGlobalId } from 'common/utils' const resolver: GQLMutationResolvers['addCollectionsArticles'] = async ( _, { input: { collections: rawCollections, articles: rawArticles } }, - { dataSources: { collectionService, articleService }, viewer } + { dataSources: { collectionService, articleService, atomService }, viewer } ) => { if (!viewer.id) { throw new ForbiddenError('Viewer has no permission') @@ -111,7 +111,7 @@ const resolver: GQLMutationResolvers['addCollectionsArticles'] = async ( } } - return await collectionService.loadByIds(collectionIds) + return await atomService.collectionIdLoader.loadMany(collectionIds) } export default resolver diff --git a/src/mutations/collection/deleteCollectionArticles.ts b/src/mutations/collection/deleteCollectionArticles.ts index 7432a8df4..2754aef9f 100644 --- a/src/mutations/collection/deleteCollectionArticles.ts +++ b/src/mutations/collection/deleteCollectionArticles.ts @@ -17,7 +17,7 @@ import { fromGlobalId } from 'common/utils' const resolver: GQLMutationResolvers['deleteCollectionArticles'] = async ( _, { input: { collection: globalId, articles } }, - { dataSources: { collectionService }, viewer } + { dataSources: { collectionService, atomService }, viewer } ) => { if (!viewer.id) { throw new ForbiddenError('Viewer has no permission') @@ -35,7 +35,7 @@ const resolver: GQLMutationResolvers['deleteCollectionArticles'] = async ( throw new UserInputError('Invalid Article ids') } - const collection = await collectionService.loadById(collectionId) + const collection = await atomService.collectionIdLoader.load(collectionId) if (!collection) { throw new UserInputError('Collection not found') diff --git a/src/mutations/collection/reorderCollectionArticles.ts b/src/mutations/collection/reorderCollectionArticles.ts index 890ba4526..ae2474562 100644 --- a/src/mutations/collection/reorderCollectionArticles.ts +++ b/src/mutations/collection/reorderCollectionArticles.ts @@ -11,7 +11,7 @@ import { fromGlobalId } from 'common/utils' const resolver: GQLMutationResolvers['reorderCollectionArticles'] = async ( _, { input: { collection: globalId, moves: rawMoves } }, - { dataSources: { collectionService }, viewer } + { dataSources: { collectionService, atomService }, viewer } ) => { if (!viewer.id) { throw new ForbiddenError('Viewer has no permission') @@ -26,7 +26,7 @@ const resolver: GQLMutationResolvers['reorderCollectionArticles'] = async ( throw new UserInputError('Invalid Collection id') } - const collection = await collectionService.loadById(collectionId) + const collection = await atomService.collectionIdLoader.load(collectionId) if (!collection) { throw new UserInputError('Collection not found') diff --git a/src/mutations/comment/deleteComment.ts b/src/mutations/comment/deleteComment.ts index cff841458..f8c5f858c 100644 --- a/src/mutations/comment/deleteComment.ts +++ b/src/mutations/comment/deleteComment.ts @@ -1,4 +1,4 @@ -import type { GQLMutationResolvers } from 'definitions' +import type { GQLMutationResolvers, Comment } from 'definitions' import { CACHE_KEYWORD, @@ -17,7 +17,7 @@ import { fromGlobalId } from 'common/utils' const resolver: GQLMutationResolvers['deleteComment'] = async ( _, { input: { id } }, - { viewer, dataSources: { atomService, commentService, articleService } } + { viewer, dataSources: { atomService, commentService } } ) => { if (!viewer.id) { throw new AuthenticationError('visitor has no permission') @@ -28,16 +28,13 @@ const resolver: GQLMutationResolvers['deleteComment'] = async ( } const { id: dbId } = fromGlobalId(id) - const comment = await commentService.loadById(dbId) + const comment = await atomService.commentIdLoader.load(dbId) // check target - let article: any - let circle: any - if (comment.type === COMMENT_TYPE.article) { - article = await articleService.dataloader.load(comment.targetId) - } else { - circle = await atomService.circleIdLoader.load(comment.targetId) - } + const node = + comment.type === COMMENT_TYPE.article + ? await atomService.articleIdLoader.load(comment.targetId) + : await atomService.circleIdLoader.load(comment.targetId) // check permission if (comment.authorId !== viewer.id) { @@ -47,14 +44,16 @@ const resolver: GQLMutationResolvers['deleteComment'] = async ( // archive comment const newComment = await commentService.baseUpdate(dbId, { state: COMMENT_STATE.archived, - updatedAt: new Date(), }) // invalidate extra nodes - newComment[CACHE_KEYWORD] = [ + ;(newComment as Comment & { [CACHE_KEYWORD]: any })[CACHE_KEYWORD] = [ { - id: article ? article.id : circle.id, - type: article ? NODE_TYPES.Article : NODE_TYPES.Circle, + id: node.id, + type: + comment.type === COMMENT_TYPE.article + ? NODE_TYPES.Article + : NODE_TYPES.Circle, }, ] diff --git a/src/mutations/comment/putComment.ts b/src/mutations/comment/putComment.ts index d5edc84f5..943629d59 100644 --- a/src/mutations/comment/putComment.ts +++ b/src/mutations/comment/putComment.ts @@ -2,8 +2,12 @@ import type { GQLMutationResolvers, NoticeCircleNewBroadcastCommentsParams, NoticeCircleNewDiscussionCommentsParams, + Article, + Circle, + Comment, } from 'definitions' +import { stripHtml } from '@matters/ipns-site-generator' import { normalizeCommentHTML, sanitizeHTML, @@ -18,6 +22,8 @@ import { CACHE_KEYWORD, COMMENT_TYPE, DB_NOTICE_TYPE, + MAX_ARTICLE_COMMENT_LENGTH, + MAX_COMMENT_EMPTY_PARAGRAPHS, NODE_TYPES, USER_STATE, } from 'common/enums' @@ -52,11 +58,9 @@ const resolver: GQLMutationResolvers['putComment'] = async ( dataSources: { atomService, paymentService, - commentService, articleService, notificationService, userService, - connections: { knex }, }, } ) => { @@ -69,17 +73,21 @@ const resolver: GQLMutationResolvers['putComment'] = async ( ) } - const data: { [key: string]: any } = { - content: normalizeCommentHTML(sanitizeHTML(content)), + const data: Partial & { mentionedUserIds?: any } = { + content: normalizeCommentHTML( + sanitizeHTML(content, { + maxEmptyParagraphs: MAX_COMMENT_EMPTY_PARAGRAPHS, + }) + ), authorId: viewer.id, } /** * check target */ - let article: any - let circle: any - let targetAuthor: any + let article: Article | undefined + let circle: Circle | undefined + let targetAuthor: string | undefined if (articleId) { const { id: articleDbId } = fromGlobalId(articleId) article = await atomService.findFirst({ @@ -89,6 +97,8 @@ const resolver: GQLMutationResolvers['putComment'] = async ( if (!article) { throw new ArticleNotFoundError('target article does not exists') } + const { id: articleVersionId } = + await articleService.loadLatestArticleVersion(article.id) const { id: typeId } = await atomService.findFirst({ table: 'entity_type', @@ -96,11 +106,12 @@ const resolver: GQLMutationResolvers['putComment'] = async ( }) data.targetTypeId = typeId data.targetId = article.id + data.articleVersionId = articleVersionId targetAuthor = article.authorId } else if (circleId) { const { id: circleDbId } = fromGlobalId(circleId) - circle = await atomService.circleIdLoader.load(circleDbId) + circle = (await atomService.circleIdLoader.load(circleDbId)) as Circle if (!circle) { throw new CircleNotFoundError('target circle does not exists') @@ -134,13 +145,17 @@ const resolver: GQLMutationResolvers['putComment'] = async ( data.type = COMMENT_TYPE[type] } + if (isArticleType && stripHtml(content).length > MAX_ARTICLE_COMMENT_LENGTH) { + throw new UserInputError('content reach length limit') + } + /** * check parentComment */ - let parentComment: any + let parentComment: Comment | undefined = undefined if (parentId) { const { id: parentDbId } = fromGlobalId(parentId) - parentComment = await commentService.loadById(parentDbId) + parentComment = await atomService.commentIdLoader.load(parentDbId) if (!parentComment) { throw new CommentNotFoundError('target parentComment does not exists') } @@ -160,10 +175,10 @@ const resolver: GQLMutationResolvers['putComment'] = async ( /** * check reply to */ - let replyToComment: any + let replyToComment: Comment | undefined = undefined if (replyTo) { const { id: replyToDBId } = fromGlobalId(replyTo) - replyToComment = await commentService.loadById(replyToDBId) + replyToComment = await atomService.commentIdLoader.load(replyToDBId) if (!replyToComment) { throw new CommentNotFoundError('target replyToComment does not exists') } @@ -287,12 +302,12 @@ const resolver: GQLMutationResolvers['putComment'] = async ( /** * Update */ - let newComment: any + let newComment: Comment if (id) { const { id: commentDbId } = fromGlobalId(id) // check permission - const comment = await commentService.loadById(commentDbId) + const comment = await atomService.commentIdLoader.load(commentDbId) if (comment.authorId !== viewer.id) { throw new ForbiddenError('viewer has no permission') } @@ -305,7 +320,6 @@ const resolver: GQLMutationResolvers['putComment'] = async ( authorId: data.authorId, parentCommentId: data.parentCommentId, replyTo: data.replyTo, - updatedAt: knex.fn.now(), // new Date(), }, }) } else { @@ -320,6 +334,7 @@ const resolver: GQLMutationResolvers['putComment'] = async ( authorId: data.authorId, targetId: data.targetId, targetTypeId: data.targetTypeId, + articleVersionId: data.articleVersionId, parentCommentId: data.parentCommentId, replyTo: data.replyTo, type: data.type, @@ -504,11 +519,11 @@ const resolver: GQLMutationResolvers['putComment'] = async ( }) // invalidate extra nodes - newComment[CACHE_KEYWORD] = [ + ;(newComment as Comment & { [CACHE_KEYWORD]: any })[CACHE_KEYWORD] = [ parentComment ? { id: parentComment.id, type: NODE_TYPES.Comment } : {}, replyToComment ? { id: replyToComment.id, type: NODE_TYPES.Comment } : {}, { - id: article ? article.id : circle.id, + id: article ? article.id : circle?.id, type: article ? NODE_TYPES.Article : NODE_TYPES.Circle, }, ] diff --git a/src/mutations/comment/togglePinComment.ts b/src/mutations/comment/togglePinComment.ts index 52835115f..d381295dc 100644 --- a/src/mutations/comment/togglePinComment.ts +++ b/src/mutations/comment/togglePinComment.ts @@ -1,4 +1,4 @@ -import type { GQLMutationResolvers } from 'definitions' +import type { GQLMutationResolvers, Article, Circle } from 'definitions' import { CACHE_KEYWORD, @@ -19,29 +19,21 @@ const resolver: Exclude< > = async ( _, { input: { id, enabled } }, - { - viewer, - dataSources: { - atomService, - commentService, - articleService, - notificationService, - }, - } + { viewer, dataSources: { atomService, commentService, notificationService } } ) => { if (!viewer.id) { throw new AuthenticationError('visitor has no permission') } const { id: dbId } = fromGlobalId(id) - const comment = await commentService.loadById(dbId) + const comment = await atomService.commentIdLoader.load(dbId) // check target - let article: any - let circle: any - let targetAuthor: any + let article: Article | undefined = undefined + let circle: Circle | undefined = undefined + let targetAuthor: string if (comment.type === COMMENT_TYPE.article) { - article = await articleService.dataloader.load(comment.targetId) + article = await atomService.articleIdLoader.load(comment.targetId) targetAuthor = article.authorId } else { circle = await atomService.circleIdLoader.load(comment.targetId) @@ -90,7 +82,6 @@ const resolver: Exclude< }, data: { pinned: false, - updatedAt: new Date(), pinnedAt: null, }, }) @@ -102,7 +93,6 @@ const resolver: Exclude< where: { id: dbId }, data: { pinned: true, - updatedAt: new Date(), pinnedAt: new Date(), }, }) @@ -127,9 +117,11 @@ const resolver: Exclude< } // invalidate extra nodes - pinnedComment[CACHE_KEYWORD] = [ + ;(pinnedComment as unknown as Comment & { [CACHE_KEYWORD]: any })[ + CACHE_KEYWORD + ] = [ { - id: article ? article.id : circle.id, + id: article ? article.id : circle?.id, type: article ? NODE_TYPES.Article : NODE_TYPES.Circle, }, ] diff --git a/src/mutations/comment/unvoteComment.ts b/src/mutations/comment/unvoteComment.ts index 6a8c222db..df851cbec 100644 --- a/src/mutations/comment/unvoteComment.ts +++ b/src/mutations/comment/unvoteComment.ts @@ -1,4 +1,4 @@ -import type { GQLMutationResolvers } from 'definitions' +import type { GQLMutationResolvers, Article, Circle } from 'definitions' import { COMMENT_TYPE, USER_STATE } from 'common/enums' import { ForbiddenByStateError, ForbiddenError } from 'common/errors' @@ -7,29 +7,21 @@ import { fromGlobalId } from 'common/utils' const resolver: GQLMutationResolvers['unvoteComment'] = async ( _, { input: { id } }, - { - viewer, - dataSources: { - atomService, - articleService, - paymentService, - commentService, - }, - } + { viewer, dataSources: { atomService, paymentService, commentService } } ) => { if (!viewer.userName) { throw new ForbiddenError('user has no username') } const { id: dbId } = fromGlobalId(id) - const comment = await commentService.loadById(dbId) + const comment = await atomService.commentIdLoader.load(dbId) // check target - let article: any - let circle: any - let targetAuthor: any + let article: Article + let circle: Circle | undefined = undefined + let targetAuthor: string if (comment.type === COMMENT_TYPE.article) { - article = await articleService.loadById(comment.targetId) + article = await atomService.articleIdLoader.load(comment.targetId) targetAuthor = article.authorId } else { circle = await atomService.circleIdLoader.load(comment.targetId) diff --git a/src/mutations/comment/updateCommentsState.ts b/src/mutations/comment/updateCommentsState.ts index 504713026..506fc9940 100644 --- a/src/mutations/comment/updateCommentsState.ts +++ b/src/mutations/comment/updateCommentsState.ts @@ -1,4 +1,11 @@ -import type { GQLMutationResolvers } from 'definitions' +import type { + GQLMutationResolvers, + Circle, + Article, + ValueOf, +} from 'definitions' + +import { invalidateFQC } from '@matters/apollo-response-cache' import { COMMENT_STATE, @@ -16,24 +23,23 @@ const resolver: GQLMutationResolvers['updateCommentsState'] = async ( viewer, dataSources: { atomService, - userService, - articleService, commentService, notificationService, + connections, }, } ) => { const dbIds = (ids || []).map((id) => fromGlobalId(id).id) const updateCommentState = async (id: string) => { - const comment = await commentService.loadById(id) + const comment = await atomService.commentIdLoader.load(id) // check target - let article: any - let circle: any - let targetAuthor: any + let article: Article + let circle: Circle + let targetAuthor: string if (comment.type === COMMENT_TYPE.article) { - article = await articleService.dataloader.load(comment.targetId) + article = await atomService.articleIdLoader.load(comment.targetId) targetAuthor = article.authorId } else { circle = await atomService.circleIdLoader.load(comment.targetId) @@ -42,14 +48,16 @@ const resolver: GQLMutationResolvers['updateCommentsState'] = async ( // check permission const isTargetAuthor = targetAuthor === viewer.id - const isValidFromState = [ - COMMENT_STATE.active, - COMMENT_STATE.collapsed, - ].includes(comment.state) - const isValidToState = [ - COMMENT_STATE.active, - COMMENT_STATE.collapsed, - ].includes(state as any) + const isValidFromState = ( + [COMMENT_STATE.active, COMMENT_STATE.collapsed] as Array< + ValueOf + > + ).includes(comment.state) + const isValidToState = ( + [COMMENT_STATE.active, COMMENT_STATE.collapsed] as Array< + ValueOf + > + ).includes(state) if (!isTargetAuthor || !isValidFromState || !isValidToState) { throw new ForbiddenError( @@ -65,6 +73,13 @@ const resolver: GQLMutationResolvers['updateCommentsState'] = async ( updatedAt: new Date(), }) + if (comment.type === COMMENT_TYPE.article) { + invalidateFQC({ + node: { type: NODE_TYPES.Article, id: comment.targetId }, + redis: connections.redis, + }) + } + return newComment } @@ -83,22 +98,27 @@ const resolver: GQLMutationResolvers['updateCommentsState'] = async ( updatedAt: new Date(), }) - // trigger notification - if (state === COMMENT_STATE.banned) { - await Promise.all( - comments.map(async (comment) => { - const user = await userService.loadById(comment.authorId) - + await Promise.all( + comments.map(async (comment) => { + // trigger notification + if (state === COMMENT_STATE.banned) { notificationService.trigger({ event: OFFICIAL_NOTICE_EXTEND_TYPE.comment_banned, entities: [ { type: 'target', entityTable: 'comment', entity: comment }, ], - recipientId: user.id, + recipientId: comment.authorId, }) - }) - ) - } + } + // invalidate cache + if (comment.type === COMMENT_TYPE.article) { + invalidateFQC({ + node: { type: NODE_TYPES.Article, id: comment.targetId }, + redis: connections.redis, + }) + } + }) + ) return comments } diff --git a/src/mutations/comment/voteComment.ts b/src/mutations/comment/voteComment.ts index 6340b5324..9c125a827 100644 --- a/src/mutations/comment/voteComment.ts +++ b/src/mutations/comment/voteComment.ts @@ -1,4 +1,4 @@ -import type { GQLMutationResolvers } from 'definitions' +import type { GQLMutationResolvers, Article, Circle } from 'definitions' import { COMMENT_TYPE, USER_STATE } from 'common/enums' import { ForbiddenByStateError, ForbiddenError } from 'common/errors' @@ -7,29 +7,21 @@ import { fromGlobalId } from 'common/utils' const resolver: GQLMutationResolvers['voteComment'] = async ( _, { input: { id, vote } }, - { - viewer, - dataSources: { - atomService, - articleService, - paymentService, - commentService, - }, - } + { viewer, dataSources: { atomService, paymentService, commentService } } ) => { if (!viewer.userName) { throw new ForbiddenError('user has no username') } const { id: dbId } = fromGlobalId(id) - const comment = await commentService.loadById(dbId) + const comment = await atomService.commentIdLoader.load(dbId) // check target - let article: any - let circle: any - let targetAuthor: any + let article: Article + let circle: Circle | undefined = undefined + let targetAuthor: string if (comment.type === COMMENT_TYPE.article) { - article = await articleService.dataloader.load(comment.targetId) + article = await atomService.articleIdLoader.load(comment.targetId) targetAuthor = article.authorId } else { circle = await atomService.circleIdLoader.load(comment.targetId) diff --git a/src/mutations/draft/putDraft.ts b/src/mutations/draft/putDraft.ts index 1c9537a3c..e4ffb8340 100644 --- a/src/mutations/draft/putDraft.ts +++ b/src/mutations/draft/putDraft.ts @@ -1,11 +1,17 @@ -import type { DataSources, ItemData, GQLMutationResolvers } from 'definitions' - +import type { AtomService } from 'connectors' +import type { + DataSources, + ItemData, + GQLMutationResolvers, + Draft, +} from 'definitions' + +import { stripHtml } from '@matters/ipns-site-generator' import { normalizeArticleHTML, sanitizeHTML, } from '@matters/matters-editor/transformers' import { isUndefined, omitBy, isString, uniq } from 'lodash' -import { v4 } from 'uuid' import { ARTICLE_LICENSE_TYPE, @@ -13,8 +19,8 @@ import { ASSET_TYPE, CACHE_KEYWORD, CIRCLE_STATE, - MAX_ARTICE_SUMMARY_LENGTH, - MAX_ARTICE_TITLE_LENGTH, + MAX_ARTICLE_SUMMARY_LENGTH, + MAX_ARTICLE_TITLE_LENGTH, MAX_ARTICLE_CONTENT_LENGTH, MAX_ARTICLES_PER_CONNECTION_LIMIT, MAX_TAGS_PER_ARTICLE_LIMIT, @@ -40,17 +46,7 @@ import { extractAssetDataFromHtml, fromGlobalId } from 'common/utils' const resolver: GQLMutationResolvers['putDraft'] = async ( _, { input }, - { - viewer, - dataSources: { - articleService, - atomService, - draftService, - systemService, - userService, - connections: { knex }, - }, - } + { viewer, dataSources: { atomService, draftService, systemService } } ) => { const { id, @@ -122,10 +118,9 @@ const resolver: GQLMutationResolvers['putDraft'] = async ( ).filter((articleId) => !!articleId) : collectionGlobalId // do not convert null or undefined if (collection) { - await validateCollection({ - viewerId: viewer.id, - collection, - dataSources: { userService, articleService }, + await validateConnections({ + connections: collection, + atomService, }) } @@ -156,7 +151,6 @@ const resolver: GQLMutationResolvers['putDraft'] = async ( } // assemble data - const resetSummary = summary === null || summary === '' const resetCover = cover === null const resetCircle = circleGlobalId === null @@ -164,9 +158,10 @@ const resolver: GQLMutationResolvers['putDraft'] = async ( { authorId: id ? undefined : viewer.id, title: title?.trim(), - summary: summary?.trim(), - summaryCustomized: summary === undefined ? undefined : !resetSummary, - content: content && normalizeArticleHTML(sanitizeHTML(content)), + summary: summary === null ? null : summary?.trim(), + content: + content && + normalizeArticleHTML(sanitizeHTML(content, { maxEmptyParagraphs: -1 })), tags: tags?.length === 0 ? null : tags, cover: coverId, collection: collection?.length === 0 ? null : collection, @@ -183,20 +178,20 @@ const resolver: GQLMutationResolvers['putDraft'] = async ( ) // check for title, summary and content length limit - if (data?.title?.length > MAX_ARTICE_TITLE_LENGTH) { + if (data?.title?.length > MAX_ARTICLE_TITLE_LENGTH) { throw new UserInputError('title reach length limit') } - if (data?.summary?.length > MAX_ARTICE_SUMMARY_LENGTH) { + if (data?.summary?.length > MAX_ARTICLE_SUMMARY_LENGTH) { throw new UserInputError('summary reach length limit') } - if (data?.content?.length > MAX_ARTICLE_CONTENT_LENGTH) { + if (stripHtml(data?.content || '').length > MAX_ARTICLE_CONTENT_LENGTH) { throw new UserInputError('content reach length limit') } // Update if (id) { const { id: dbId } = fromGlobalId(id) - const draft = await draftService.loadById(dbId) + const draft = await atomService.draftIdLoader.load(dbId) // check for draft existence if (!draft) { @@ -233,11 +228,11 @@ const resolver: GQLMutationResolvers['putDraft'] = async ( // check for collection limit if (collection) { - const oldCollectionLength = + const oldConnectionLength = draft.collection == null ? 0 : draft.collection.length if ( collection.length > MAX_ARTICLES_PER_CONNECTION_LIMIT && - collection.length > oldCollectionLength + collection.length > oldConnectionLength ) { throw new ArticleCollectionReachLimitError( `Not allow more than ${MAX_ARTICLES_PER_CONNECTION_LIMIT} articles in collection` @@ -279,9 +274,7 @@ const resolver: GQLMutationResolvers['putDraft'] = async ( return draftService.baseUpdate(dbId, { ...data, // reset fields - summary: resetSummary ? null : data.summary, circleId: resetCircle ? null : data.circleId, - updatedAt: knex.fn.now(), }) } @@ -298,7 +291,9 @@ const resolver: GQLMutationResolvers['putDraft'] = async ( ) } - const draft = await draftService.baseCreate({ uuid: v4(), ...data }) + const draft = (await draftService.baseCreate(data)) as Draft & { + [CACHE_KEYWORD]: Array<{ id: string; type: NODE_TYPES.User }> + } draft[CACHE_KEYWORD] = [ { id: viewer.id, @@ -332,18 +327,19 @@ const validateTags = async ({ } } -const validateCollection = async ({ - viewerId, - collection, - dataSources: { userService, articleService }, +const validateConnections = async ({ + connections, + atomService, }: { - viewerId: string - collection: string[] - dataSources: Pick + connections: string[] + atomService: AtomService }) => { await Promise.all( - collection.map(async (articleId) => { - const article = await articleService.baseFindById(articleId) + connections.map(async (articleId) => { + const article = await atomService.findUnique({ + table: 'article', + where: { id: articleId }, + }) if (!article) { throw new ArticleNotFoundError(`Cannot find article ${articleId}`) @@ -352,14 +348,6 @@ const validateCollection = async ({ if (article.state !== ARTICLE_STATE.active) { throw new ForbiddenError(`Article ${articleId} cannot be collected.`) } - - const isBlocked = await userService.blocked({ - userId: article.authorId, - targetId: viewerId, - }) - if (isBlocked) { - throw new ForbiddenError('viewer has no permission') - } }) ) } diff --git a/src/mutations/system/addBlockedSearchKeyword.ts b/src/mutations/system/addBlockedSearchKeyword.ts index 83ca217d0..2a6a4b5be 100644 --- a/src/mutations/system/addBlockedSearchKeyword.ts +++ b/src/mutations/system/addBlockedSearchKeyword.ts @@ -3,15 +3,15 @@ import type { GQLMutationResolvers } from 'definitions' import { UserInputError } from 'common/errors' const resolver: GQLMutationResolvers['addBlockedSearchKeyword'] = async ( - root, + _, { input: { keyword } }, - { dataSources: { atomService, systemService }, viewer } + { dataSources: { atomService } } ) => { const table = 'blocked_search_keyword' const search_key = await atomService.findFirst({ table, - where: { search_key: keyword }, + where: { searchKey: keyword }, }) if (search_key) { @@ -20,7 +20,7 @@ const resolver: GQLMutationResolvers['addBlockedSearchKeyword'] = async ( const newItem = await atomService.create({ table, - data: { search_key: keyword }, + data: { searchKey: keyword }, }) const newAddedKeyword = { diff --git a/src/mutations/system/deleteAnnouncements.ts b/src/mutations/system/deleteAnnouncements.ts index fb85d7dab..3fa01e5cc 100644 --- a/src/mutations/system/deleteAnnouncements.ts +++ b/src/mutations/system/deleteAnnouncements.ts @@ -4,7 +4,7 @@ import { UserInputError } from 'common/errors' import { fromGlobalId } from 'common/utils' const resolver: GQLMutationResolvers['deleteAnnouncements'] = async ( - root, + _, { input: { ids } }, { dataSources: { atomService } } ) => { diff --git a/src/mutations/system/deleteBlockedSearchKeywords.ts b/src/mutations/system/deleteBlockedSearchKeywords.ts index b947682dd..5ce829a0b 100644 --- a/src/mutations/system/deleteBlockedSearchKeywords.ts +++ b/src/mutations/system/deleteBlockedSearchKeywords.ts @@ -3,7 +3,7 @@ import type { GQLMutationResolvers } from 'definitions' import { UserInputError } from 'common/errors' const resolver: GQLMutationResolvers['deleteBlockedSearchKeywords'] = async ( - root, + _, { input: { keywords } }, { dataSources: { atomService } } ) => { diff --git a/src/mutations/system/index.ts b/src/mutations/system/index.ts index ad76feccb..acdc1cead 100644 --- a/src/mutations/system/index.ts +++ b/src/mutations/system/index.ts @@ -4,12 +4,14 @@ import deleteBlockedSearchKeywords from './deleteBlockedSearchKeywords' import directImageUpload from './directImageUpload' import logRecord from './logRecord' import putAnnouncement from './putAnnouncement' +import putIcymiTopic from './putIcymiTopic' import putRemark from './putRemark' import putRestrictedUsers from './putRestrictedUsers' import putSkippedListItem from './putSkippedListItem' import setBoost from './setBoost' import setFeature from './setFeature' import singleFileUpload from './singleFileUpload' +import submitReport from './submitReport' import toggleSeedingUsers from './toggleSeedingUsers' export default { @@ -27,5 +29,7 @@ export default { addBlockedSearchKeyword, deleteBlockedSearchKeywords, putRestrictedUsers, + submitReport, + putIcymiTopic, }, } diff --git a/src/mutations/system/putAnnouncement.ts b/src/mutations/system/putAnnouncement.ts index 4631b0841..75d733bff 100644 --- a/src/mutations/system/putAnnouncement.ts +++ b/src/mutations/system/putAnnouncement.ts @@ -11,7 +11,7 @@ import { import { fromGlobalId, toGlobalId } from 'common/utils' const resolver: GQLMutationResolvers['putAnnouncement'] = async ( - root, + _, { input }, { dataSources: { atomService, systemService }, viewer } ) => { @@ -27,7 +27,7 @@ const resolver: GQLMutationResolvers['putAnnouncement'] = async ( const toAnnouncementId = (dbId: string) => toGlobalId({ type: NODE_TYPES.Announcement, id: dbId }) - const toCoverURL = async (coverId: any) => + const toCoverURL = async (coverId: string | null) => coverId ? systemService.findAssetUrl(coverId) : null // preparation @@ -138,7 +138,7 @@ const resolver: GQLMutationResolvers['putAnnouncement'] = async ( return { ...ret, id: toAnnouncementId(ret.id), - cover: toCoverURL(ret.cover), + cover: (await toCoverURL(ret.cover)) ?? '', translations: translations && transResults.map((tr: any) => ({ diff --git a/src/mutations/system/putIcymiTopic.ts b/src/mutations/system/putIcymiTopic.ts new file mode 100644 index 000000000..d28fe5759 --- /dev/null +++ b/src/mutations/system/putIcymiTopic.ts @@ -0,0 +1,57 @@ +import type { GQLMutationResolvers } from 'definitions' + +import { MATTERS_CHOICE_TOPIC_STATE } from 'common/enums' +import { UserInputError } from 'common/errors' +import { fromGlobalId } from 'common/utils' + +const resolver: GQLMutationResolvers['putIcymiTopic'] = async ( + _, + { input: { id: globalId, title, articles, pinAmount, note, state } }, + { dataSources: { recommendationService } } +) => { + if (!globalId) { + // create + if (!title) { + throw new UserInputError('title is required') + } + if (!pinAmount) { + throw new UserInputError('pinAmount is required') + } + const topic = await recommendationService.createIcymiTopic({ + title, + articleIds: (articles ?? []).map((article) => fromGlobalId(article).id), + pinAmount, + note, + }) + + if (state === MATTERS_CHOICE_TOPIC_STATE.published) { + return recommendationService.publishIcymiTopic(topic.id) + } else { + // state === MATTERS_CHOICE_TOPIC_STATE.editing or undefined + return topic + } + } else { + // update + const id = fromGlobalId(globalId).id + if (state === MATTERS_CHOICE_TOPIC_STATE.archived) { + return recommendationService.archiveIcymiTopic(id) + } + const topic = await recommendationService.updateIcymiTopic(id, { + title, + articleIds: articles + ? articles.map((article) => fromGlobalId(article).id) + : undefined, + pinAmount, + note, + }) + + if (state === MATTERS_CHOICE_TOPIC_STATE.published) { + return recommendationService.publishIcymiTopic(topic.id) + } else { + // state === MATTERS_CHOICE_TOPIC_STATE.editing or undefined + return topic + } + } +} + +export default resolver diff --git a/src/mutations/system/putRemark.ts b/src/mutations/system/putRemark.ts index c251679de..eaf110ac7 100644 --- a/src/mutations/system/putRemark.ts +++ b/src/mutations/system/putRemark.ts @@ -20,11 +20,7 @@ const resolver: GQLMutationResolvers['putRemark'] = async ( : id const table = tableMap[type] - const entity = await systemService.baseUpdate( - dbId, - { remark, updatedAt: new Date() }, - table - ) + const entity = await systemService.baseUpdate(dbId, { remark }, table) return entity.remark } diff --git a/src/mutations/system/putSkippedListItem.ts b/src/mutations/system/putSkippedListItem.ts index d89ac9e4d..878fc5367 100644 --- a/src/mutations/system/putSkippedListItem.ts +++ b/src/mutations/system/putSkippedListItem.ts @@ -7,12 +7,15 @@ import { fromGlobalId, toGlobalId } from 'common/utils' const resolver: GQLMutationResolvers['putSkippedListItem'] = async ( _, { input: { id, type, value, archived } }, - { dataSources: { systemService } } + { dataSources: { systemService, atomService } } ) => { // Update if (id) { const { id: dbId } = fromGlobalId(id) - const item = await systemService.baseFindById(dbId, 'blocklist') + const item = await atomService.findUnique({ + where: { id: dbId }, + table: 'blocklist', + }) if (!item) { throw new EntityNotFoundError(`target ${dbId} does not exists`) diff --git a/src/mutations/system/setBoost.ts b/src/mutations/system/setBoost.ts index 5929f3343..c3ef571b0 100644 --- a/src/mutations/system/setBoost.ts +++ b/src/mutations/system/setBoost.ts @@ -15,7 +15,7 @@ const resolver: GQLMutationResolvers['setBoost'] = async ( } const { id: dbId } = fromGlobalId(id) - const entity = await serviceMap[type].loadById(dbId) + const entity = await serviceMap[type].baseFindById(dbId) if (!entity) { throw new EntityNotFoundError(`target ${type} does not exists`) } diff --git a/src/mutations/system/singleFileUpload.ts b/src/mutations/system/singleFileUpload.ts index 1bbdb279f..8b999dde6 100644 --- a/src/mutations/system/singleFileUpload.ts +++ b/src/mutations/system/singleFileUpload.ts @@ -1,4 +1,4 @@ -import type { ItemData, GQLMutationResolvers } from 'definitions' +import type { Asset, GQLMutationResolvers } from 'definitions' import axios from 'axios' import { FileUpload } from 'graphql-upload' @@ -124,10 +124,8 @@ const resolver: GQLMutationResolvers['singleFileUpload'] = async ( // make sure both settled try { key = isImageType - ? // @ts-ignore - await systemService.cfsvc.baseUploadFile(type, upload, uuid) - : // @ts-ignore - await systemService.aws.baseUploadFile(type, upload, uuid) + ? await systemService.cfsvc.baseUploadFile(type, upload, uuid) + : await systemService.aws.baseUploadFile(type, upload, uuid) } catch (err) { logger.error('cloudflare upload image ERROR:', err) throw err @@ -135,7 +133,7 @@ const resolver: GQLMutationResolvers['singleFileUpload'] = async ( // assert both "fulfilled" ? - const asset: ItemData = { + const asset: Partial = { uuid, authorId: viewer.id, type, diff --git a/src/mutations/system/submitReport.ts b/src/mutations/system/submitReport.ts new file mode 100644 index 000000000..c855ee5e3 --- /dev/null +++ b/src/mutations/system/submitReport.ts @@ -0,0 +1,24 @@ +import type { GQLMutationResolvers, ReportType } from 'definitions' + +import { NODE_TYPES } from 'common/enums' +import { UserInputError } from 'common/errors' +import { fromGlobalId } from 'common/utils' + +const resolver: GQLMutationResolvers['submitReport'] = async ( + _, + { input: { targetId: globalId, reason } }, + { dataSources: { systemService }, viewer } +) => { + const { type, id: targetId } = fromGlobalId(globalId) + if (![NODE_TYPES.Article, NODE_TYPES.Comment].includes(type)) { + throw new UserInputError('invalid type') + } + return systemService.submitReport({ + targetType: type as ReportType, + targetId, + reporterId: viewer.id, + reason, + }) +} + +export default resolver diff --git a/src/mutations/system/toggleSeedingUsers.ts b/src/mutations/system/toggleSeedingUsers.ts index ec274c32c..1913e2dbc 100644 --- a/src/mutations/system/toggleSeedingUsers.ts +++ b/src/mutations/system/toggleSeedingUsers.ts @@ -4,9 +4,9 @@ import { UserInputError } from 'common/errors' import { fromGlobalId } from 'common/utils' const resolver: GQLMutationResolvers['toggleSeedingUsers'] = async ( - root, + _, { input: { ids, enabled } }, - { dataSources: { atomService }, viewer } + { dataSources: { atomService } } ) => { if (!ids || ids.length === 0) { throw new UserInputError('"ids" is required') diff --git a/src/mutations/user/claimLogbooks.ts b/src/mutations/user/claimLogbooks.ts index a92718941..320123103 100644 --- a/src/mutations/user/claimLogbooks.ts +++ b/src/mutations/user/claimLogbooks.ts @@ -14,7 +14,7 @@ import { recoverMessageAddress, } from 'viem' import { privateKeyToAccount } from 'viem/accounts' -import { polygon, polygonMumbai } from 'viem/chains' +import { polygon } from 'viem/chains' import { BLOCKCHAIN_RPC, SIGNING_MESSAGE_PURPOSE } from 'common/enums' import { environment, isProd, contract } from 'common/environment' @@ -77,12 +77,16 @@ const resolver: GQLMutationResolvers['claimLogbooks'] = async ( throw new EntityNotFoundError('no logbooks to claim') } + // FIXME: pause support for the Polygon testnet + // @see {src/common/enums/payment.ts:L59} + if (!isProd) { + throw new UserInputError('Polygon Mumbai is deprecated') + } + // filter unclaimed token ids const client = createPublicClient({ - chain: isProd ? polygon : polygonMumbai, - transport: http( - isProd ? BLOCKCHAIN_RPC[polygon.id] : BLOCKCHAIN_RPC[polygonMumbai.id] - ), + chain: polygon, + transport: http(BLOCKCHAIN_RPC[polygon.id]), }) const abi = [ 'function ownerOf(uint256 tokenId) view returns (address)', @@ -122,9 +126,7 @@ const resolver: GQLMutationResolvers['claimLogbooks'] = async ( try { const { data } = await axios({ method: 'get', - url: isProd - ? 'https://gasstation-mainnet.matic.network/v2' - : 'https://gasstation-mumbai.matic.today/v2', + url: 'https://gasstation-mainnet.matic.network/v2', }) maxFeePerGas = parseGwei(Math.ceil(data.fast.maxFee) + '') maxPriorityFeePerGas = parseGwei(Math.ceil(data.fast.maxPriorityFee) + '') diff --git a/src/mutations/user/payTo.ts b/src/mutations/user/payTo.ts index b0858be45..410c90cb4 100644 --- a/src/mutations/user/payTo.ts +++ b/src/mutations/user/payTo.ts @@ -211,6 +211,9 @@ const resolver: GQLMutationResolvers['payTo'] = async ( if (!chain) { throw new UserInputError('`chain` is required if `currency` is `USDT`') } + if (chain === 'Polygon') { + throw new UserInputError('Polygon is deprecated') + } if (!txHash) { throw new UserInputError('`txHash` is required if `currency` is `USDT`') } diff --git a/src/mutations/user/putFeaturedTags.ts b/src/mutations/user/putFeaturedTags.ts index a625511c1..8319e6cae 100644 --- a/src/mutations/user/putFeaturedTags.ts +++ b/src/mutations/user/putFeaturedTags.ts @@ -1,4 +1,4 @@ -import type { GQLMutationResolvers } from 'definitions' +import type { GQLMutationResolvers, UserTagsOrder } from 'definitions' import { AuthenticationError } from 'common/errors' import { fromGlobalId } from 'common/utils' @@ -6,7 +6,7 @@ import { fromGlobalId } from 'common/utils' const resolver: GQLMutationResolvers['putFeaturedTags'] = async ( _, { input: { ids } }, - { viewer, dataSources: { systemService, tagService } } + { viewer, dataSources: { systemService, atomService } } ) => { // checks if (!viewer.id) { @@ -15,14 +15,13 @@ const resolver: GQLMutationResolvers['putFeaturedTags'] = async ( const dbIds = ids.filter(Boolean).map((id: string) => fromGlobalId(id).id) - const entry = await systemService.baseUpdateOrCreate({ + const entry = await systemService.baseUpdateOrCreate({ table: 'user_tags_order', where: { userId: viewer.id }, data: { userId: viewer.id, tagIds: dbIds }, - updateUpdatedAt: true, }) - return tagService.loadByIds(entry.tagIds) + return atomService.tagIdLoader.loadMany(entry.tagIds) } export default resolver diff --git a/src/mutations/user/resetLikerId.ts b/src/mutations/user/resetLikerId.ts index c1eb2248d..4b61c1055 100644 --- a/src/mutations/user/resetLikerId.ts +++ b/src/mutations/user/resetLikerId.ts @@ -4,12 +4,12 @@ import { ForbiddenError } from 'common/errors' import { fromGlobalId } from 'common/utils' const resolver: GQLMutationResolvers['resetLikerId'] = async ( - root, + _, { input: { id } }, - { viewer, dataSources: { atomService, userService } } + { dataSources: { atomService } } ) => { const { id: dbId } = fromGlobalId(id) - const user = await userService.loadById(dbId) + const user = await atomService.userIdLoader.load(dbId) if (!user || !user.likerId) { throw new ForbiddenError("user doesn't exist or have a liker id") diff --git a/src/mutations/user/resetWallet.ts b/src/mutations/user/resetWallet.ts index 970cb63bb..ad8f4819b 100644 --- a/src/mutations/user/resetWallet.ts +++ b/src/mutations/user/resetWallet.ts @@ -9,7 +9,7 @@ const resolver: GQLMutationResolvers['resetWallet'] = async ( { dataSources: { atomService, userService } } ) => { const { id: dbId } = fromGlobalId(id) - const user = await userService.loadById(dbId) + const user = await atomService.userIdLoader.load(dbId) if (!user || !user.ethAddress) { throw new ForbiddenError("user doesn't exist or have a crypto wallet") diff --git a/src/mutations/user/toggleBlockUser.ts b/src/mutations/user/toggleBlockUser.ts index 65f3d7acf..c5d7697dd 100644 --- a/src/mutations/user/toggleBlockUser.ts +++ b/src/mutations/user/toggleBlockUser.ts @@ -11,7 +11,7 @@ import { fromGlobalId } from 'common/utils' const resolver: GQLMutationResolvers['toggleBlockUser'] = async ( _, { input: { id, enabled } }, - { viewer, dataSources: { userService } } + { viewer, dataSources: { userService, atomService } } ) => { // checks if (!viewer.id) { @@ -24,7 +24,7 @@ const resolver: GQLMutationResolvers['toggleBlockUser'] = async ( throw new ActionFailedError('cannot block yourself') } - const user = await userService.loadById(dbId) + const user = await atomService.userIdLoader.load(dbId) if (!user) { throw new UserNotFoundError('target user does not exists') } diff --git a/src/mutations/user/toggleFollowTag.ts b/src/mutations/user/toggleFollowTag.ts index 4d03c1f8d..d71608f29 100644 --- a/src/mutations/user/toggleFollowTag.ts +++ b/src/mutations/user/toggleFollowTag.ts @@ -7,7 +7,7 @@ import { fromGlobalId } from 'common/utils' const resolver: GQLMutationResolvers['toggleFollowTag'] = async ( _, { input: { id, enabled } }, - { viewer, dataSources: { tagService } } + { viewer, dataSources: { tagService, atomService } } ) => { // checks if (!viewer.userName) { @@ -15,7 +15,7 @@ const resolver: GQLMutationResolvers['toggleFollowTag'] = async ( } const { id: dbId } = fromGlobalId(id) - const tag = await tagService.loadById(dbId) + const tag = await atomService.tagIdLoader.load(dbId) if (!tag) { throw new TagNotFoundError('target user does not exists') diff --git a/src/mutations/user/toggleFollowUser.ts b/src/mutations/user/toggleFollowUser.ts index dc8d74c66..d075a7e05 100644 --- a/src/mutations/user/toggleFollowUser.ts +++ b/src/mutations/user/toggleFollowUser.ts @@ -18,7 +18,7 @@ import { fromGlobalId } from 'common/utils' const resolver: GQLMutationResolvers['toggleFollowUser'] = async ( _, { input: { id, enabled } }, - { viewer, dataSources: { userService, notificationService } } + { viewer, dataSources: { userService, notificationService, atomService } } ) => { // checks if (!viewer.userName) { @@ -30,7 +30,7 @@ const resolver: GQLMutationResolvers['toggleFollowUser'] = async ( } const { id: dbId } = fromGlobalId(id) - const user = await userService.loadById(dbId) + const user = await atomService.userIdLoader.load(dbId) if (!user) { throw new UserNotFoundError('target user does not exists') diff --git a/src/mutations/user/togglePinTag.ts b/src/mutations/user/togglePinTag.ts index a89f42a2d..763aaad4e 100644 --- a/src/mutations/user/togglePinTag.ts +++ b/src/mutations/user/togglePinTag.ts @@ -7,7 +7,7 @@ import { fromGlobalId } from 'common/utils' const resolver: GQLMutationResolvers['togglePinTag'] = async ( _, { input: { id, enabled } }, - { viewer, dataSources: { tagService } } + { viewer, dataSources: { tagService, atomService } } ) => { // checks if (!viewer.id) { @@ -15,7 +15,7 @@ const resolver: GQLMutationResolvers['togglePinTag'] = async ( } const { id: dbId } = fromGlobalId(id) - const tag = await tagService.loadById(dbId) + const tag = await atomService.tagIdLoader.load(dbId) if (!tag) { throw new TagNotFoundError('target user does not exists') diff --git a/src/mutations/user/toggleUsersBadge.ts b/src/mutations/user/toggleUsersBadge.ts index 34349c8ea..2da8a20fd 100644 --- a/src/mutations/user/toggleUsersBadge.ts +++ b/src/mutations/user/toggleUsersBadge.ts @@ -1,8 +1,7 @@ -// import { isNil, omitBy } from 'lodash' +import type { GQLMutationResolvers } from 'definitions' import { UserInputError } from 'common/errors' import { fromGlobalId } from 'common/utils' -import { GQLMutationResolvers } from 'definitions' const resolver: GQLMutationResolvers['toggleUsersBadge'] = async ( _, @@ -33,8 +32,6 @@ const resolver: GQLMutationResolvers['toggleUsersBadge'] = async ( case 'nomad4': level = 4 break - // level = Number.parseInt(type.charAt(5)) // only 1, 2, 3, 4 - // type = 'nomad' } const dbType = ( type.startsWith('nomad') && level >= 1 ? 'nomad' : type @@ -43,9 +40,7 @@ const resolver: GQLMutationResolvers['toggleUsersBadge'] = async ( await // enabled Promise.all( userIds.map((id) => { - const dataUpdate = - // omitBy( - { enabled, ...(level ? { extra: { level } } : null) } + const dataUpdate = { enabled, ...(level ? { extra: { level } } : null) } const dataCreate = { userId: id, type: dbType, ...dataUpdate } return atomService.upsert({ diff --git a/src/mutations/user/unbindLikerId.ts b/src/mutations/user/unbindLikerId.ts index 89d69a436..5b9714792 100644 --- a/src/mutations/user/unbindLikerId.ts +++ b/src/mutations/user/unbindLikerId.ts @@ -8,12 +8,13 @@ const resolver: GQLMutationResolvers['unbindLikerId'] = async ( { dataSources: { userService, + atomService, connections: { knex }, }, } ) => { const { id: dbId } = fromGlobalId(id) - const user = await userService.loadById(dbId) + const user = await atomService.userIdLoader.load(dbId) // check user's liker id if (user.likerId !== likerId) { diff --git a/src/mutations/user/updateUserInfo.ts b/src/mutations/user/updateUserInfo.ts index 0dff1879a..00d7c178c 100644 --- a/src/mutations/user/updateUserInfo.ts +++ b/src/mutations/user/updateUserInfo.ts @@ -262,7 +262,7 @@ const resolver: GQLMutationResolvers['updateUserInfo'] = async ( } // trigger notifications - if (updateParams.paymentPasswordHash) { + if (updateParams.paymentPasswordHash && user.email) { notificationService.mail.sendPayment({ to: user.email, recipient: { diff --git a/src/mutations/user/updateUserState.ts b/src/mutations/user/updateUserState.ts index 1a58eaf32..610b8f94e 100644 --- a/src/mutations/user/updateUserState.ts +++ b/src/mutations/user/updateUserState.ts @@ -39,7 +39,7 @@ const resolver: GQLMutationResolvers['updateUserState'] = async ( } // sync - const user = await userService.loadById(id) + const user = await atomService.userIdLoader.load(id) const archivedUser = await userService.archive(id) // async @@ -72,7 +72,6 @@ const resolver: GQLMutationResolvers['updateUserState'] = async ( where: { id: user.id }, data: { state, - updatedAt: knex.fn.now(), }, }) } @@ -87,7 +86,7 @@ const resolver: GQLMutationResolvers['updateUserState'] = async ( } if (id) { - const user = (await userService.loadById(id)) as User + const user = (await atomService.userIdLoader.load(id)) as User validateUserState(user) return [await handleUpdateUserState(user)] } diff --git a/src/mutations/user/userLogin.ts b/src/mutations/user/userLogin.ts index f72944afa..86ad06cd2 100644 --- a/src/mutations/user/userLogin.ts +++ b/src/mutations/user/userLogin.ts @@ -22,7 +22,7 @@ const resolver: GQLMutationResolvers['userLogin'] = async ( password, archivedCallback, }) - await userService.verifyPassword({ password, hash: user.passwordHash }) + await userService.verifyPassword({ password, hash: user.passwordHash ?? '' }) setCookie({ req, res, token, user }) diff --git a/src/mutations/user/walletLogin.ts b/src/mutations/user/walletLogin.ts index f32791fa9..b25ce924f 100644 --- a/src/mutations/user/walletLogin.ts +++ b/src/mutations/user/walletLogin.ts @@ -92,7 +92,7 @@ const _walletLogin: Exclude< userService, atomService, systemService, - connections: { knex, redis }, + connections: { redis }, }, } = context @@ -118,7 +118,6 @@ const _walletLogin: Exclude< data: { signature, userId: viewer.id, - updatedAt: knex.fn.now(), }, }) diff --git a/src/queries/article/access/circle.ts b/src/queries/article/access/circle.ts index cc6043f88..9556ca275 100644 --- a/src/queries/article/access/circle.ts +++ b/src/queries/article/access/circle.ts @@ -1,11 +1,11 @@ import type { GQLArticleAccessResolvers, Circle } from 'definitions' export const circle: GQLArticleAccessResolvers['circle'] = async ( - { articleId }, + { id }, _, { dataSources: { atomService, articleService } } ) => { - const articleCircle = await articleService.findArticleCircle(articleId) + const articleCircle = await articleService.findArticleCircle(id) if (!articleCircle || !articleCircle.circleId) { return null diff --git a/src/queries/article/access/secret.ts b/src/queries/article/access/secret.ts index 8e5a9f7c7..96f566855 100644 --- a/src/queries/article/access/secret.ts +++ b/src/queries/article/access/secret.ts @@ -3,7 +3,7 @@ import type { GQLArticleAccessResolvers } from 'definitions' import { ForbiddenError } from 'common/errors' export const secret: GQLArticleAccessResolvers['secret'] = async ( - { articleId, authorId }, + { id, authorId }, _, { viewer, dataSources: { articleService } } ) => { @@ -16,7 +16,7 @@ export const secret: GQLArticleAccessResolvers['secret'] = async ( throw new ForbiddenError('viewer has no permission') } - const articleCircle = await articleService.findArticleCircle(articleId) + const articleCircle = await articleService.findArticleCircle(id) if (!articleCircle) { return diff --git a/src/queries/article/access/type.ts b/src/queries/article/access/type.ts index 21cbd8213..678cb9f62 100644 --- a/src/queries/article/access/type.ts +++ b/src/queries/article/access/type.ts @@ -1,23 +1,7 @@ import type { GQLArticleAccessResolvers } from 'definitions' -import { ARTICLE_ACCESS_TYPE } from 'common/enums' - -export const type: Exclude< - GQLArticleAccessResolvers['type'], - undefined -> = async ({ articleId }, _, { dataSources: { articleService } }) => { - const articleCircle = await articleService.findArticleCircle(articleId) - - // not in circle, fallback to public - if (!articleCircle) { - return ARTICLE_ACCESS_TYPE.public - } - - // public - if (articleCircle.access === ARTICLE_ACCESS_TYPE.public) { - return ARTICLE_ACCESS_TYPE.public - } - - // paywall - return ARTICLE_ACCESS_TYPE.paywall -} +export const type: GQLArticleAccessResolvers['type'] = async ( + { id }, + _, + { dataSources: { articleService } } +) => articleService.getAccess(id) diff --git a/src/queries/article/appreciateLeft.ts b/src/queries/article/appreciateLeft.ts index 9f4893674..1b4adf140 100644 --- a/src/queries/article/appreciateLeft.ts +++ b/src/queries/article/appreciateLeft.ts @@ -1,7 +1,7 @@ import type { GQLArticleResolvers } from 'definitions' const resolver: GQLArticleResolvers['appreciateLeft'] = async ( - { articleId }, + { id }, _, { viewer, dataSources: { articleService } } ) => { @@ -10,7 +10,7 @@ const resolver: GQLArticleResolvers['appreciateLeft'] = async ( } return articleService.appreciateLeftByUser({ - articleId, + articleId: id, userId: viewer.id, }) } diff --git a/src/queries/article/appreciationsReceived.ts b/src/queries/article/appreciationsReceived.ts index e2165ab14..b4293c0c0 100644 --- a/src/queries/article/appreciationsReceived.ts +++ b/src/queries/article/appreciationsReceived.ts @@ -3,7 +3,7 @@ import type { GQLArticleResolvers } from 'definitions' import { connectionFromArray, fromConnectionArgs } from 'common/utils' const resolver: GQLArticleResolvers['appreciationsReceived'] = async ( - { articleId }, + { id }, { input }, { dataSources: { articleService } } ) => { @@ -13,12 +13,12 @@ const resolver: GQLArticleResolvers['appreciationsReceived'] = async ( return connectionFromArray( [], input, - await articleService.countAppreciations(articleId) + await articleService.countAppreciations(id) ) } const records = await articleService.findAppreciations({ - referenceId: articleId, + referenceId: id, take, skip, }) diff --git a/src/queries/article/appreciationsReceivedTotal.ts b/src/queries/article/appreciationsReceivedTotal.ts index 379c65c4b..ba4da320d 100644 --- a/src/queries/article/appreciationsReceivedTotal.ts +++ b/src/queries/article/appreciationsReceivedTotal.ts @@ -1,9 +1,9 @@ import { GQLArticleResolvers } from 'definitions' const resolver: GQLArticleResolvers['appreciationsReceivedTotal'] = async ( - { articleId }, - _: any, + { id }, + _, { dataSources: { articleService } } -) => articleService.sumAppreciation(articleId) +) => articleService.sumAppreciation(id) export default resolver diff --git a/src/queries/article/assets.ts b/src/queries/article/assets.ts index 85fba7c53..6ae02cf7d 100644 --- a/src/queries/article/assets.ts +++ b/src/queries/article/assets.ts @@ -1,7 +1,7 @@ import type { GQLArticleResolvers } from 'definitions' const resolver: GQLArticleResolvers['assets'] = async ( - { id, authorId, articleId }, + { id, authorId }, _, { viewer, dataSources: { systemService } } ) => { @@ -18,22 +18,10 @@ const resolver: GQLArticleResolvers['assets'] = async ( ) const articleAssets = await systemService.findAssetAndAssetMap({ entityTypeId: articleEntityTypeId, - entityId: articleId, + entityId: id, }) - // assets belonged to linked latest draft - let draftAssets: any[] = [] - if (id) { - const { id: draftEntityTypeId } = await systemService.baseFindEntityTypeId( - 'draft' - ) - draftAssets = await systemService.findAssetAndAssetMap({ - entityTypeId: draftEntityTypeId, - entityId: id, - }) - } - - const assets = [...articleAssets, ...draftAssets].map((asset) => ({ + const assets = articleAssets.map((asset: any) => ({ ...asset, path: systemService.genAssetUrl(asset), })) diff --git a/src/queries/article/author.ts b/src/queries/article/author.ts index 9ea57a33e..ca7d90e84 100644 --- a/src/queries/article/author.ts +++ b/src/queries/article/author.ts @@ -3,7 +3,7 @@ import type { GQLArticleResolvers } from 'definitions' const resolver: GQLArticleResolvers['author'] = ( { authorId }, _, - { dataSources: { userService } } -) => userService.loadById(authorId) + { dataSources: { atomService } } +) => atomService.userIdLoader.load(authorId) export default resolver diff --git a/src/queries/article/availableTranslations.ts b/src/queries/article/availableTranslations.ts index d93a6d009..d7fe47ce3 100644 --- a/src/queries/article/availableTranslations.ts +++ b/src/queries/article/availableTranslations.ts @@ -1,9 +1,9 @@ -import type { GQLArticleResolvers } from 'definitions' +import type { GQLArticleResolvers, LANGUAGES } from 'definitions' import { LANGUAGE } from 'common/enums' const resolver: GQLArticleResolvers['availableTranslations'] = async ( - { articleId }, + { id }, _, { dataSources: { atomService } } ) => { @@ -13,13 +13,13 @@ const resolver: GQLArticleResolvers['availableTranslations'] = async ( await atomService.findMany({ table: 'article_translation', select: ['language'], - where: { articleId }, + where: { articleId: id }, }) ) .map((t) => t.language) - .filter((l) => validLanguages.includes(l)) + .filter((l) => validLanguages.includes(l as LANGUAGES)) - return languages + return languages as LANGUAGES[] } export default resolver diff --git a/src/queries/article/canComment.ts b/src/queries/article/canComment.ts new file mode 100644 index 000000000..597c09ed6 --- /dev/null +++ b/src/queries/article/canComment.ts @@ -0,0 +1,12 @@ +import type { GQLArticleResolvers } from 'definitions' + +const resolver: GQLArticleResolvers['canComment'] = async ( + { id }, + _, + { dataSources: { articleService } } +) => { + const articleVersion = await articleService.loadLatestArticleVersion(id) + return articleVersion.canComment +} + +export default resolver diff --git a/src/queries/article/canSuperLike.ts b/src/queries/article/canSuperLike.ts index 50a17869a..5888b1402 100644 --- a/src/queries/article/canSuperLike.ts +++ b/src/queries/article/canSuperLike.ts @@ -1,39 +1,12 @@ import type { GQLArticleResolvers } from 'definitions' -import { environment } from 'common/environment' -import { getLogger } from 'common/logger' - -const logger = getLogger('mutation-superlike') - +// TODO: deprecated const resolver: GQLArticleResolvers['canSuperLike'] = async ( - { articleId }, + { id }, _, { viewer, dataSources: { userService } } ) => { - if (!viewer.id) { - return false - } - - const [author, liker] = await Promise.all([ - userService.baseFindById(viewer.id), - userService.findLiker({ userId: viewer.id }), - ]) - - if (!liker) { - return false - } - - try { - return await userService.likecoin.canSuperLike({ - liker, - url: `https://${environment.siteDomain}/@${author.userName}/${articleId}`, - likerIp: viewer.ip, - userAgent: viewer.userAgent, - }) - } catch (e) { - logger.error(e) - return false - } + return false } export default resolver diff --git a/src/queries/article/chapter/articleCount.ts b/src/queries/article/chapter/articleCount.ts deleted file mode 100644 index 3ff7617de..000000000 --- a/src/queries/article/chapter/articleCount.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { GQLChapterResolvers } from 'definitions' - -const resolver: GQLChapterResolvers['articleCount'] = async ( - { id: chapterId }, - _, - { dataSources: { atomService } } -) => - atomService.count({ - table: 'article_chapter', - where: { chapterId }, - }) - -export default resolver diff --git a/src/queries/article/chapter/articles.ts b/src/queries/article/chapter/articles.ts deleted file mode 100644 index 06ac36956..000000000 --- a/src/queries/article/chapter/articles.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { GQLChapterResolvers, Draft } from 'definitions' - -const resolver: GQLChapterResolvers['articles'] = async ( - { id: chapterId }, - _, - { dataSources: { atomService, articleService } } -) => { - const chapterArticles = await atomService.findMany({ - table: 'article_chapter', - where: { chapterId }, - orderBy: [{ column: 'order', order: 'asc' }], - }) - - return articleService.loadDraftsByArticles( - chapterArticles.map((item) => item.articleId) - ) as Promise -} - -export default resolver diff --git a/src/queries/article/chapter/topic.ts b/src/queries/article/chapter/topic.ts deleted file mode 100644 index ca37a658b..000000000 --- a/src/queries/article/chapter/topic.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { GQLChapterResolvers } from 'definitions' - -const resolver: GQLChapterResolvers['topic'] = async ( - { id: chapterId }, - _, - { dataSources: { atomService } } -) => { - const chapter = await atomService.findFirst({ - table: 'chapter', - where: { id: chapterId }, - }) - - return atomService.findFirst({ - table: 'topic', - where: { id: chapter.topicId }, - }) -} - -export default resolver diff --git a/src/queries/article/collectedBy.ts b/src/queries/article/collectedBy.ts index d20a0a419..623ec3de7 100644 --- a/src/queries/article/collectedBy.ts +++ b/src/queries/article/collectedBy.ts @@ -2,19 +2,17 @@ import type { GQLArticleResolvers } from 'definitions' import { ARTICLE_STATE } from 'common/enums' import { - connectionFromPromisedArray, + connectionFromArray, fromConnectionArgs, loadManyFilterError, } from 'common/utils' const resolver: GQLArticleResolvers['collectedBy'] = async ( - { articleId }, + { id }, { input }, { dataSources: { atomService, - articleService, - draftService, connections: { knex }, }, } @@ -23,12 +21,12 @@ const resolver: GQLArticleResolvers['collectedBy'] = async ( const [countRecord, connections] = await Promise.all([ knex('article_connection') - .where({ articleId }) + .where({ articleId: id }) .countDistinct('entrance_id') .first(), atomService.findMany({ table: 'article_connection', - where: { articleId }, + where: { articleId: id }, skip, take, }), @@ -39,18 +37,14 @@ const resolver: GQLArticleResolvers['collectedBy'] = async ( 10 ) - const articles = await articleService.dataloader + const articles = await atomService.articleIdLoader .loadMany(connections.map((connection) => connection.entranceId)) .then(loadManyFilterError) .then((items) => items.filter(({ state }) => state === ARTICLE_STATE.active) ) - return connectionFromPromisedArray( - draftService.loadByIds(articles.map((article) => article.draftId)), - input, - totalCount - ) + return connectionFromArray(articles, input, totalCount) } export default resolver diff --git a/src/queries/article/collection.ts b/src/queries/article/collection.ts index e482bb060..c6e922217 100644 --- a/src/queries/article/collection.ts +++ b/src/queries/article/collection.ts @@ -1,13 +1,14 @@ -import type { GQLArticleResolvers, Item, Draft } from 'definitions' +import type { GQLArticleResolvers, Item } from 'definitions' import { ARTICLE_STATE } from 'common/enums' import { connectionFromPromisedArray, fromConnectionArgs } from 'common/utils' const resolver: GQLArticleResolvers['collection'] = async ( - { articleId }, + { id }, { input }, { dataSources: { + atomService, articleService, connections: { knex }, }, @@ -19,10 +20,10 @@ const resolver: GQLArticleResolvers['collection'] = async ( knex('article_connection') .countDistinct('article_id', 'state') .innerJoin('article', 'article.id', 'article_id') - .where({ entranceId: articleId, state: ARTICLE_STATE.active }) + .where({ entranceId: id, state: ARTICLE_STATE.active }) .first(), articleService.findConnections({ - entranceId: articleId, + entranceId: id, take, skip, }), @@ -34,9 +35,9 @@ const resolver: GQLArticleResolvers['collection'] = async ( ) return connectionFromPromisedArray( - articleService.loadDraftsByArticles( + atomService.articleIdLoader.loadMany( connections.map((connection: Item) => connection.articleId) - ) as Promise, + ), input, totalCount ) diff --git a/src/queries/article/content.ts b/src/queries/article/content.ts index a6566e1fe..a06914649 100644 --- a/src/queries/article/content.ts +++ b/src/queries/article/content.ts @@ -4,19 +4,17 @@ import { ARTICLE_ACCESS_TYPE, ARTICLE_STATE } from 'common/enums' // ACL for article content const resolver: GQLArticleResolvers['content'] = async ( - { articleId, authorId, content }, + { id, authorId, state }, _, { viewer, dataSources: { articleService, paymentService } } ) => { - const article = await articleService.dataloader.load(articleId) - - const isActive = article.state === ARTICLE_STATE.active + const isActive = state === ARTICLE_STATE.active const isAdmin = viewer.hasRole('admin') const isAuthor = authorId === viewer.id // check viewer if (isAdmin || isAuthor) { - return content + return articleService.loadLatestArticleContent(id) } // check article state @@ -24,18 +22,18 @@ const resolver: GQLArticleResolvers['content'] = async ( return '' } - const articleCircle = await articleService.findArticleCircle(articleId) + const articleCircle = await articleService.findArticleCircle(id) // not in circle if (!articleCircle) { - return content + return articleService.loadLatestArticleContent(id) } const isPublic = articleCircle.access === ARTICLE_ACCESS_TYPE.public // public if (isPublic) { - return content + return articleService.loadLatestArticleContent(id) } if (!viewer.id) { @@ -52,7 +50,7 @@ const resolver: GQLArticleResolvers['content'] = async ( return '' } - return content + return articleService.loadLatestArticleContent(id) } export default resolver diff --git a/src/queries/article/contents/html.ts b/src/queries/article/contents/html.ts index 23076b15a..3209319b3 100644 --- a/src/queries/article/contents/html.ts +++ b/src/queries/article/contents/html.ts @@ -1,3 +1,67 @@ -import htmlContent from '../content' +import type { GQLArticleContentsResolvers } from 'definitions' -export const html = htmlContent +import { ARTICLE_ACCESS_TYPE, ARTICLE_STATE } from 'common/enums' + +export const html: GQLArticleContentsResolvers['html'] = async ( + // @ts-ignore + { articleId, contentId, content: draftContent }, + _, + { viewer, dataSources: { articleService, paymentService, atomService } } +) => { + const { authorId, state } = await atomService.articleIdLoader.load(articleId) + const isActive = state === ARTICLE_STATE.active + const isAdmin = viewer.hasRole('admin') + const isAuthor = authorId === viewer.id + + // check viewer + if (isAdmin || isAuthor) { + return ( + draftContent ?? + (await atomService.articleContentIdLoader.load(contentId)).content + ) + } + + // check article state + if (!isActive) { + return '' + } + + const articleCircle = await articleService.findArticleCircle(articleId) + + // not in circle + if (!articleCircle) { + return ( + draftContent ?? + (await atomService.articleContentIdLoader.load(contentId)).content + ) + } + + const isPublic = articleCircle.access === ARTICLE_ACCESS_TYPE.public + + // public + if (isPublic) { + return ( + draftContent ?? + (await atomService.articleContentIdLoader.load(contentId)).content + ) + } + + if (!viewer.id) { + return '' + } + + const isCircleMember = await paymentService.isCircleMember({ + userId: viewer.id, + circleId: articleCircle.circleId, + }) + + // not circle member + if (!isCircleMember) { + return '' + } + + return ( + draftContent ?? + (await atomService.articleContentIdLoader.load(contentId)).content + ) +} diff --git a/src/queries/article/contents/markdown.ts b/src/queries/article/contents/markdown.ts index 57bfa3436..82f17cf5c 100644 --- a/src/queries/article/contents/markdown.ts +++ b/src/queries/article/contents/markdown.ts @@ -3,19 +3,22 @@ import type { GQLArticleContentsResolvers } from 'definitions' import { ARTICLE_ACCESS_TYPE, ARTICLE_STATE } from 'common/enums' export const markdown: GQLArticleContentsResolvers['markdown'] = async ( - { articleId, authorId, contentMd }, + { articleId, contentMdId }, _, - { viewer, dataSources: { articleService, paymentService } } + { viewer, dataSources: { articleService, paymentService, atomService } } ) => { - const article = await articleService.dataloader.load(articleId) + if (!contentMdId) { + return '' + } - const isActive = article.state === ARTICLE_STATE.active + const { authorId, state } = await atomService.articleIdLoader.load(articleId) + const isActive = state === ARTICLE_STATE.active const isAdmin = viewer.hasRole('admin') const isAuthor = authorId === viewer.id // check viewer if (isAdmin || isAuthor) { - return contentMd || '' + return (await atomService.articleContentIdLoader.load(contentMdId)).content } // check article state @@ -27,14 +30,14 @@ export const markdown: GQLArticleContentsResolvers['markdown'] = async ( // not in circle if (!articleCircle) { - return contentMd || '' + return (await atomService.articleContentIdLoader.load(contentMdId)).content } const isPublic = articleCircle.access === ARTICLE_ACCESS_TYPE.public // public if (isPublic) { - return contentMd || '' + return (await atomService.articleContentIdLoader.load(contentMdId)).content } if (!viewer.id) { @@ -51,5 +54,5 @@ export const markdown: GQLArticleContentsResolvers['markdown'] = async ( return '' } - return contentMd || '' + return (await atomService.articleContentIdLoader.load(contentMdId)).content } diff --git a/src/queries/article/cover.ts b/src/queries/article/cover.ts index e7f955169..878327fc1 100644 --- a/src/queries/article/cover.ts +++ b/src/queries/article/cover.ts @@ -1,12 +1,14 @@ import type { GQLArticleResolvers } from 'definitions' const resolver: GQLArticleResolvers['cover'] = async ( - { articleId }, + { id }, _, { dataSources: { articleService, systemService } } ) => { - const article = await articleService.dataloader.load(articleId) - return article?.cover ? systemService.findAssetUrl(article.cover) : null + const articleVersion = await articleService.loadLatestArticleVersion(id) + return articleVersion.cover + ? systemService.findAssetUrl(articleVersion.cover) + : null } export default resolver diff --git a/src/queries/article/createdAt.ts b/src/queries/article/createdAt.ts index 5fc2be45d..8096b450d 100644 --- a/src/queries/article/createdAt.ts +++ b/src/queries/article/createdAt.ts @@ -1,12 +1,7 @@ import type { GQLArticleResolvers } from 'definitions' -const resolver: GQLArticleResolvers['createdAt'] = async ( - { articleId }, - _, - { dataSources: { articleService } } -) => { - const article = await articleService.dataloader.load(articleId) - return article.createdAt +const resolver: GQLArticleResolvers['createdAt'] = async ({ createdAt }) => { + return createdAt } export default resolver diff --git a/src/queries/article/dataHash.ts b/src/queries/article/dataHash.ts new file mode 100644 index 000000000..245edb228 --- /dev/null +++ b/src/queries/article/dataHash.ts @@ -0,0 +1,12 @@ +import type { GQLArticleResolvers } from 'definitions' + +const resolver: GQLArticleResolvers['dataHash'] = async ( + { id }, + _, + { dataSources: { articleService } } +) => { + const articleVersion = await articleService.loadLatestArticleVersion(id) + return articleVersion.dataHash || '' +} + +export default resolver diff --git a/src/queries/article/donated.ts b/src/queries/article/donated.ts new file mode 100644 index 000000000..f132e8d21 --- /dev/null +++ b/src/queries/article/donated.ts @@ -0,0 +1,14 @@ +import type { GQLArticleResolvers } from 'definitions' + +const resolver: GQLArticleResolvers['donated'] = async ( + { id: articleId }, + _, + { viewer, dataSources: { paymentService } } +) => { + if (!viewer.id) { + return false + } + return paymentService.isDonator(viewer.id, articleId) +} + +export default resolver diff --git a/src/queries/article/donationCount.ts b/src/queries/article/donationCount.ts index b1151f931..e8fc1dd02 100644 --- a/src/queries/article/donationCount.ts +++ b/src/queries/article/donationCount.ts @@ -3,16 +3,17 @@ import type { GQLArticleResolvers } from 'definitions' import { TRANSACTION_PURPOSE } from 'common/enums' const resolver: GQLArticleResolvers['donationCount'] = async ( - { articleId, authorId }, + { id, authorId }, _, { dataSources: { articleService }, viewer } ) => { + // only author can see donation count if (viewer?.id !== authorId) { return 0 } return articleService.countTransactions({ purpose: TRANSACTION_PURPOSE.donation, - targetId: articleId, + targetId: id, }) } diff --git a/src/queries/article/donations.ts b/src/queries/article/donations.ts index e3979a19f..8d8d1d787 100644 --- a/src/queries/article/donations.ts +++ b/src/queries/article/donations.ts @@ -8,29 +8,29 @@ import { } from 'common/utils' const resolver: GQLArticleResolvers['donations'] = async ( - { articleId }, + { id }, { input }, - { dataSources: { articleService, userService } } + { dataSources: { articleService, atomService } } ) => { const { take, skip } = fromConnectionArgs(input) const [totalCount, txs] = await Promise.all([ articleService.countTransactions({ purpose: TRANSACTION_PURPOSE.donation, - targetId: articleId, + targetId: id, }), articleService.findTransactions({ skip, take, purpose: TRANSACTION_PURPOSE.donation, - targetId: articleId, + targetId: id, }), ]) return connectionFromPromisedArray( txs.map((tx: Item) => ({ id: toGlobalId({ type: NODE_TYPES.Transaction, id: tx.id }), - sender: tx.senderId ? userService.loadById(tx.senderId) : null, + sender: tx.senderId ? atomService.userIdLoader.load(tx.senderId) : null, })), input, totalCount diff --git a/src/queries/article/hasAppreciate.ts b/src/queries/article/hasAppreciate.ts index f2e2bdf6b..c6e56078a 100644 --- a/src/queries/article/hasAppreciate.ts +++ b/src/queries/article/hasAppreciate.ts @@ -3,7 +3,7 @@ import type { GQLArticleResolvers } from 'definitions' import { APPRECIATION_PURPOSE } from 'common/enums' const resolver: GQLArticleResolvers['hasAppreciate'] = async ( - { articleId }, + { id }, _, { viewer, dataSources: { atomService } } ) => { @@ -11,16 +11,16 @@ const resolver: GQLArticleResolvers['hasAppreciate'] = async ( return false } - const record = await atomService.findFirst({ + const count = await atomService.count({ table: 'appreciation', where: { senderId: viewer.id, - referenceId: articleId, + referenceId: id, purpose: APPRECIATION_PURPOSE.appreciate, }, }) - return record > 0 + return count > 0 } export default resolver diff --git a/src/queries/article/id.ts b/src/queries/article/id.ts index 6402dab02..ac17bbb11 100644 --- a/src/queries/article/id.ts +++ b/src/queries/article/id.ts @@ -1,25 +1,10 @@ import type { GQLArticleResolvers } from 'definitions' import { NODE_TYPES } from 'common/enums' -import { getLogger } from 'common/logger' import { toGlobalId } from 'common/utils' -const logger = getLogger('query-article-id') - -const resolver: GQLArticleResolvers['id'] = async ( - { articleId, id }, - _, - __, - info -) => { - if (!articleId) { - logger.warn( - "Article's fields should derive from Draft instead of Article: %j", - info.path - ) - return toGlobalId({ type: NODE_TYPES.Article, id }) - } - return toGlobalId({ type: NODE_TYPES.Article, id: articleId }) +const resolver: GQLArticleResolvers['id'] = async ({ id }) => { + return toGlobalId({ type: NODE_TYPES.Article, id }) } export default resolver diff --git a/src/queries/article/index.ts b/src/queries/article/index.ts index f4ba2bb3d..43134e8ec 100644 --- a/src/queries/article/index.ts +++ b/src/queries/article/index.ts @@ -1,6 +1,4 @@ -import type { GQLArticleLicenseType } from 'definitions' - -import slugify from '@matters/slugify' +import type { GQLResolvers } from 'definitions' import { ARTICLE_APPRECIATE_LIMIT, NODE_TYPES } from 'common/enums' import { toGlobalId } from 'common/utils' @@ -12,21 +10,23 @@ import appreciationsReceivedTotal from './appreciationsReceivedTotal' import assets from './assets' import author from './author' import availableTranslations from './availableTranslations' +import canComment from './canComment' import canSuperLike from './canSuperLike' -import chapterArticleCount from './chapter/articleCount' -import chapterArticles from './chapter/articles' -import chapterTopic from './chapter/topic' import collectedBy from './collectedBy' import collection from './collection' import content from './content' import * as contents from './contents' import articleCover from './cover' import createdAt from './createdAt' +import dataHash from './dataHash' +import donated from './donated' import donationCount from './donationCount' import donations from './donations' import hasAppreciate from './hasAppreciate' import idResolver from './id' import language from './language' +import license from './license' +import mediaHash from './mediaHash' import * as articleOSS from './oss' import pinned from './pinned' import readerCount from './readerCount' @@ -39,11 +39,15 @@ import requestForDonation from './requestForDonation' import revisedAt from './revisedAt' import revisionCount from './revisionCount' import rootArticle from './rootArticle' +import sensitiveByAuthor from './sensitiveByAuthor' +import shortHash from './shortHash' +import slug from './slug' import state from './state' import sticky from './sticky' import subscribed from './subscribed' import subscribers from './subscribers' import summary from './summary' +import summaryCustomized from './summaryCustomized' import tagArticles from './tag/articles' import tagCover from './tag/cover' import tagCreator from './tag/creator' @@ -60,33 +64,28 @@ import tagParticipants from './tag/participants' import tagsRecommended from './tag/recommended' import tagSelected from './tag/selected' import tags from './tags' -import topicArticleCount from './topic/articleCount' -import topicArticles from './topic/articles' -import topicAuthor from './topic/author' -import topicChapterCount from './topic/chapterCount' -import topicChapters from './topic/chapters' -import topicCover from './topic/cover' -import topicLatestArticle from './topic/latestArticle' +import title from './title' import transactionsReceivedBy from './transactionsReceivedBy' -import translation from './translation' +import articleTranslation from './translation/article' +import articleVersionTranslation from './translation/articleVersion' import userArticles from './user/articles' -// import userTags from './user/tags' -import userTopics from './user/topics' +import versions from './versions' -export default { +const schema: GQLResolvers = { Query: { article: rootArticle, }, User: { articles: userArticles, - // tags: userTags, - topics: userTopics, }, Article: { id: idResolver, + title, content, - contents: (root: any) => root, + contents: ({ id }, _, { dataSources: { articleService } }) => + articleService.loadLatestArticleVersion(id), summary, + summaryCustomized, appreciationsReceived, appreciationsReceivedTotal, appreciateLimit: () => ARTICLE_APPRECIATE_LIMIT, @@ -99,39 +98,43 @@ export default { hasAppreciate, canSuperLike, language, - oss: (root: any) => root, + oss: (root) => root, relatedArticles, relatedDonationArticles, remark, - slug: ({ slug, title }: { slug: string; title: string }) => - slug || slugify(title), - dataHash: ({ dataHash }: { dataHash: string }) => dataHash || '', - mediaHash: ({ mediaHash }: { mediaHash: string }) => mediaHash || '', + slug, + sensitiveByAuthor, + dataHash, + mediaHash, + shortHash, state, sticky, pinned, subscribed, subscribers, tags, - translation, + translation: articleTranslation, availableTranslations, - topicScore: ({ score }: { score: number }) => - score ? Math.round(score) : null, + topicScore: (({ score }: { score: number }) => + score ? Math.round(score) : null) as any, transactionsReceivedBy, donations, readTime, createdAt, revisedAt, - access: (root: any) => root, + access: (root) => root, revisionCount, - license: ({ license }: { license: GQLArticleLicenseType }) => license, + license, + canComment, + donated, requestForDonation, replyToDonator, donationCount, readerCount, + versions, }, Tag: { - id: ({ id }: { id: string }) => toGlobalId({ type: NODE_TYPES.Tag, id }), + id: ({ id }) => toGlobalId({ type: NODE_TYPES.Tag, id }), articles: tagArticles, selected: tagSelected, creator: tagCreator, @@ -143,27 +146,15 @@ export default { numArticles: tagNumArticles, numAuthors: tagNumAuthors, followers: tagFollowers, - oss: (root: any) => root, + oss: (root) => root, cover: tagCover, participants: tagParticipants, recommended: tagsRecommended, }, - Topic: { - id: ({ id }: { id: string }) => toGlobalId({ type: NODE_TYPES.Topic, id }), - cover: topicCover, - chapterCount: topicChapterCount, - articleCount: topicArticleCount, - chapters: topicChapters, - articles: topicArticles, - author: topicAuthor, - latestArticle: topicLatestArticle, - }, - Chapter: { - id: ({ id }: { id: string }) => - toGlobalId({ type: NODE_TYPES.Chapter, id }), - articleCount: chapterArticleCount, - articles: chapterArticles, - topic: chapterTopic, + ArticleVersion: { + id: ({ id }) => toGlobalId({ type: NODE_TYPES.ArticleVersion, id }), + contents: (root) => root, + translation: articleVersionTranslation, }, ArticleContents: { html: contents.html, @@ -187,3 +178,5 @@ export default { selected: tagOSS.selected, }, } + +export default schema diff --git a/src/queries/article/language.ts b/src/queries/article/language.ts index 41cce33a6..bce6b4c44 100644 --- a/src/queries/article/language.ts +++ b/src/queries/article/language.ts @@ -5,19 +5,34 @@ import { stripHtml } from '@matters/ipns-site-generator' import { GCP } from 'connectors' const resolver: GQLArticleResolvers['language'] = async ( - { id, content, language: storedLanguage }, + { id: articleId }, _, - { dataSources: { draftService } } + { dataSources: { articleService, atomService } } ) => { + const { + id: versionId, + language: storedLanguage, + contentId, + content: draftContent, + } = await articleService.loadLatestArticleVersion(articleId) if (storedLanguage) { return storedLanguage } const gcp = new GCP() - gcp - .detectLanguage(stripHtml(content.slice(0, 300))) - .then((language) => language && draftService.baseUpdate(id, { language })) + const content = draftContent + ? draftContent + : (await atomService.articleContentIdLoader.load(contentId)).content + + gcp.detectLanguage(stripHtml(content).slice(0, 300)).then((language) => { + language && + atomService.update({ + table: 'article_version', + where: { id: versionId }, + data: { language }, + }) + }) // return first to prevent blocking return null diff --git a/src/queries/article/license.ts b/src/queries/article/license.ts new file mode 100644 index 000000000..5c70059ae --- /dev/null +++ b/src/queries/article/license.ts @@ -0,0 +1,12 @@ +import type { GQLArticleResolvers } from 'definitions' + +const resolver: GQLArticleResolvers['license'] = async ( + { id }, + _, + { dataSources: { articleService } } +) => { + const articleVersion = await articleService.loadLatestArticleVersion(id) + return articleVersion.license +} + +export default resolver diff --git a/src/queries/article/mediaHash.ts b/src/queries/article/mediaHash.ts new file mode 100644 index 000000000..5f383b4cf --- /dev/null +++ b/src/queries/article/mediaHash.ts @@ -0,0 +1,12 @@ +import type { GQLArticleResolvers } from 'definitions' + +const resolver: GQLArticleResolvers['mediaHash'] = async ( + { id }, + _, + { dataSources: { articleService } } +) => { + const articleVersion = await articleService.loadLatestArticleVersion(id) + return articleVersion.mediaHash || '' +} + +export default resolver diff --git a/src/queries/article/oss.ts b/src/queries/article/oss.ts index cdd8fe8a1..e9660c2d3 100644 --- a/src/queries/article/oss.ts +++ b/src/queries/article/oss.ts @@ -1,7 +1,7 @@ import type { GQLArticleOssResolvers } from 'definitions' export const boost: GQLArticleOssResolvers['boost'] = async ( - { articleId }, + { id: articleId }, _, { dataSources: { atomService } } ) => { @@ -18,11 +18,11 @@ export const boost: GQLArticleOssResolvers['boost'] = async ( } export const score: GQLArticleOssResolvers['score'] = async ( - { articleId }, + { id: articleId }, _, { dataSources: { atomService } } ) => { - const article = await atomService.findFirst({ + const article = await atomService.findUnique({ table: 'article_count_view', where: { id: articleId }, }) @@ -30,7 +30,7 @@ export const score: GQLArticleOssResolvers['score'] = async ( } export const inRecommendIcymi: GQLArticleOssResolvers['inRecommendIcymi'] = - async ({ articleId }, _, { dataSources: { atomService } }) => { + async ({ id: articleId }, _, { dataSources: { atomService } }) => { const record = await atomService.findFirst({ table: 'matters_choice', where: { articleId }, @@ -39,7 +39,7 @@ export const inRecommendIcymi: GQLArticleOssResolvers['inRecommendIcymi'] = } export const inRecommendHottest: GQLArticleOssResolvers['inRecommendHottest'] = - async ({ articleId }, _, { dataSources: { atomService } }) => { + async ({ id: articleId }, _, { dataSources: { atomService } }) => { const setting = await atomService.findFirst({ table: 'article_recommend_setting', where: { articleId }, @@ -53,7 +53,7 @@ export const inRecommendHottest: GQLArticleOssResolvers['inRecommendHottest'] = } export const inRecommendNewest: GQLArticleOssResolvers['inRecommendNewest'] = - async ({ articleId }, _, { dataSources: { atomService } }) => { + async ({ id: articleId }, _, { dataSources: { atomService } }) => { const setting = await atomService.findFirst({ table: 'article_recommend_setting', where: { articleId }, diff --git a/src/queries/article/pinned.ts b/src/queries/article/pinned.ts index ec36af770..dc9629e32 100644 --- a/src/queries/article/pinned.ts +++ b/src/queries/article/pinned.ts @@ -1,12 +1,5 @@ import type { GQLArticleResolvers } from 'definitions' -const resolver: GQLArticleResolvers['pinned'] = async ( - { articleId }, - _, - { dataSources: { articleService } } -) => { - const article = await articleService.dataloader.load(articleId) - return article.pinned -} +const resolver: GQLArticleResolvers['pinned'] = async ({ pinned }) => pinned export default resolver diff --git a/src/queries/article/readTime.ts b/src/queries/article/readTime.ts index b2dc2de44..7c031dc4c 100644 --- a/src/queries/article/readTime.ts +++ b/src/queries/article/readTime.ts @@ -1,7 +1,7 @@ import type { GQLArticleResolvers } from 'definitions' const resolver: GQLArticleResolvers['revisionCount'] = async ( - { articleId }, + { id: articleId }, _, { dataSources: { atomService } } ) => { diff --git a/src/queries/article/readerCount.ts b/src/queries/article/readerCount.ts index e7853b65e..29a20de53 100644 --- a/src/queries/article/readerCount.ts +++ b/src/queries/article/readerCount.ts @@ -1,7 +1,7 @@ import type { GQLArticleResolvers } from 'definitions' const resolver: GQLArticleResolvers['readerCount'] = async ( - { articleId, authorId }, + { id: articleId, authorId }, _, { dataSources: { articleService }, viewer } ) => { diff --git a/src/queries/article/relatedArticles.ts b/src/queries/article/relatedArticles.ts index a1c47e93d..ff4e866dc 100644 --- a/src/queries/article/relatedArticles.ts +++ b/src/queries/article/relatedArticles.ts @@ -1,16 +1,14 @@ -import type { GQLArticleResolvers } from 'definitions' +import type { GQLArticleResolvers, Article } from 'definitions' import _ from 'lodash' -import { getLogger } from 'common/logger' +import { ARTICLE_STATE, LATEST_WORKS_NUM } from 'common/enums' import { connectionFromArray, fromConnectionArgs } from 'common/utils' -const logger = getLogger('related-articles') - const resolver: GQLArticleResolvers['relatedArticles'] = async ( - { articleId, authorId }, + { id: articleId, authorId }, { input }, - { dataSources: { articleService, draftService, tagService } } + { dataSources: { articleService, tagService, atomService } } ) => { // return 3 recommendations by default const { take, skip } = fromConnectionArgs(input, { defaultTake: 3 }) @@ -18,14 +16,19 @@ const resolver: GQLArticleResolvers['relatedArticles'] = async ( // buffer for archived article and random draw const buffer = 7 - // helper function to prevent duplicates and origin article - const addRec = (rec: any[], extra: any[]) => - _.uniqBy(rec.concat(extra), 'id').filter((_rec) => _rec.id !== articleId) - - // const ids: string[] = [] - let articles: any[] = [] + // helper function to prevent duplicates and exclude both origin article and articles return by `latestWorks` API + const latestArticles = await articleService.findByAuthor(authorId, { + take: LATEST_WORKS_NUM, + orderBy: 'newest', + state: ARTICLE_STATE.active, + }) + const unwantedIds = [articleId, ...latestArticles.map(({ id }) => id)] + const addRec = (rec: Article[], extra: Article[]) => + _.uniqBy(rec.concat(extra), 'id').filter( + (_rec) => !unwantedIds.includes(_rec.id) + ) - let sameIdx = -1 + let articles: Article[] = [] // first select from tags const tagIds = await articleService.findTagIds({ id: articleId }) @@ -37,49 +40,30 @@ const resolver: GQLArticleResolvers['relatedArticles'] = async ( const articleIds = await tagService.findArticleIds({ id: tagId, - take, // : take - ids.length, // this ids.length is always 0?? + take, skip, }) // get articles and append - const articlesFromTag = await articleService.loadByIds(articleIds) - - articles = addRec(articles, articlesFromTag) - } + const articlesFromTag = await atomService.articleIdLoader.loadMany( + articleIds + ) - if ( - // tslint:disable-next-line - (sameIdx = articles?.findIndex((item: any) => item.id === articleId)) >= 0 - ) { - logger.info( - `found same article at {${sameIdx}} at tagService.findArticleIds step and remove it: %j`, - { sameIdx, articleId } + articles = addRec( + articles, + articlesFromTag.filter(({ state }) => state === ARTICLE_STATE.active) ) - articles.splice(sameIdx, 1) - sameIdx = -1 } // fall back to author if (articles.length < take + buffer) { const articlesFromAuthor = await articleService.findByAuthor(authorId, { - columns: ['id', 'draft_id'], + skip: 3, + state: ARTICLE_STATE.active, }) - // logger.info(`[recommendation] article ${articleId}, title ${title}, author result ${articlesFromAuthor.map(({ id: aid }: { id: string }) => aid)} `) articles = addRec(articles, articlesFromAuthor) } - if ( - // tslint:disable-next-line - (sameIdx = articles?.findIndex((item: any) => item.id === articleId)) >= 0 - ) { - logger.info( - `found same article at {${sameIdx}} at articleService.findByAuthor step and remove it: %j`, - { sameIdx, articleId } - ) - articles.splice(sameIdx, 1) - sameIdx = -1 - } - // random pick for last few elements const randomPick = 1 let pick = articles.slice(0, take - randomPick) @@ -87,33 +71,7 @@ const resolver: GQLArticleResolvers['relatedArticles'] = async ( _.sampleSize(articles.slice(take - randomPick), randomPick) ) - if ( - // tslint:disable-next-line - (sameIdx = pick?.findIndex((item: any) => item.id === articleId)) >= 0 - ) { - logger.info( - `found same article at {${sameIdx}} at randomPick step and remove it: %j`, - { sameIdx, articleId } - ) - pick.splice(sameIdx, 1) - sameIdx = -1 - } - - const nodes = await draftService.loadByIds(pick.map((item) => item.draftId)) - - if ( - // tslint:disable-next-line - (sameIdx = nodes?.findIndex((item: any) => item.articleId === articleId)) >= - 0 - ) { - logger.info( - `found same article at {${sameIdx}} at last step and remove it: %j`, - { sameIdx, articleId } - ) - nodes.splice(sameIdx, 1) - } - - return connectionFromArray(nodes, input) + return connectionFromArray(pick, input) } export default resolver diff --git a/src/queries/article/relatedDonationArticles.ts b/src/queries/article/relatedDonationArticles.ts index a14aa4a9f..afac212c7 100644 --- a/src/queries/article/relatedDonationArticles.ts +++ b/src/queries/article/relatedDonationArticles.ts @@ -5,9 +5,9 @@ import { chunk } from 'lodash' import { connectionFromPromisedArray, fromConnectionArgs } from 'common/utils' const resolver: GQLArticleResolvers['relatedDonationArticles'] = async ( - { articleId }, + { id: articleId }, { input }, - { dataSources: { articleService, draftService } } + { dataSources: { articleService } } ) => { const { random } = input const { take, skip } = fromConnectionArgs(input) @@ -32,9 +32,7 @@ const resolver: GQLArticleResolvers['relatedDonationArticles'] = async ( const filteredArticles = chunks[index] || [] return connectionFromPromisedArray( - draftService.loadByIds( - filteredArticles.map((article) => article.draftId) - ), + filteredArticles, input, articlePool.length ) @@ -50,11 +48,7 @@ const resolver: GQLArticleResolvers['relatedDonationArticles'] = async ( }), ]) - return connectionFromPromisedArray( - draftService.loadByIds(articles.map((article) => article.draftId)), - input, - totalCount - ) + return connectionFromPromisedArray(articles, input, totalCount) } export default resolver diff --git a/src/queries/article/remark.ts b/src/queries/article/remark.ts index dfce95e16..ddb77128c 100644 --- a/src/queries/article/remark.ts +++ b/src/queries/article/remark.ts @@ -1,12 +1,5 @@ import type { GQLArticleResolvers } from 'definitions' -const resolver: GQLArticleResolvers['remark'] = async ( - { articleId }, - _, - { dataSources: { articleService } } -) => { - const article = await articleService.dataloader.load(articleId) - return article.remark -} +const resolver: GQLArticleResolvers['remark'] = async ({ remark }) => remark export default resolver diff --git a/src/queries/article/replyToDonator.ts b/src/queries/article/replyToDonator.ts index f9e5c0386..54a886e50 100644 --- a/src/queries/article/replyToDonator.ts +++ b/src/queries/article/replyToDonator.ts @@ -1,44 +1,26 @@ import type { GQLArticleResolvers } from 'definitions' -import { - TRANSACTION_PURPOSE, - TRANSACTION_STATE, - TRANSACTION_TARGET_TYPE, -} from 'common/enums' - const resolver: GQLArticleResolvers['replyToDonator'] = async ( - { authorId, articleId, replyToDonator }, + { authorId, id: articleId }, _, - { viewer, dataSources } + { viewer, dataSources: { articleService, paymentService } } ) => { if (!viewer.id) { return null } + const getReplyToDonator = async () => { + const { replyToDonator } = await articleService.loadLatestArticleVersion( + articleId + ) + return replyToDonator + } + const isAuthor = viewer.id === authorId - const isDonator = await _isDonator(viewer.id, articleId, dataSources) - return isAuthor || isDonator ? replyToDonator ?? null : null -} -const _isDonator = async ( - viewerId: string, - articleId: string, - { atomService, paymentService }: any -) => { - const { id: entityTypeId } = await paymentService.baseFindEntityTypeId( - TRANSACTION_TARGET_TYPE.article - ) - const count = await atomService.count({ - table: 'transaction', - where: { - purpose: TRANSACTION_PURPOSE.donation, - state: TRANSACTION_STATE.succeeded, - targetType: entityTypeId, - targetId: articleId, - senderId: viewerId, - }, - }) - return count > 0 + return isAuthor || (await paymentService.isDonator(viewer.id, articleId)) + ? await getReplyToDonator() + : null } export default resolver diff --git a/src/queries/article/requestForDonation.ts b/src/queries/article/requestForDonation.ts index ae3597e44..e6a3feda2 100644 --- a/src/queries/article/requestForDonation.ts +++ b/src/queries/article/requestForDonation.ts @@ -1,8 +1,14 @@ import type { GQLArticleResolvers } from 'definitions' const resolver: GQLArticleResolvers['requestForDonation'] = async ( - { requestForDonation }, - _ -) => requestForDonation ?? null + { id: articleId }, + _, + { dataSources: { articleService } } +) => { + const { requestForDonation } = await articleService.loadLatestArticleVersion( + articleId + ) + return requestForDonation +} export default resolver diff --git a/src/queries/article/revisedAt.ts b/src/queries/article/revisedAt.ts index 8c868a28c..9f6200db0 100644 --- a/src/queries/article/revisedAt.ts +++ b/src/queries/article/revisedAt.ts @@ -1,29 +1,19 @@ import type { GQLArticleResolvers } from 'definitions' -import { PUBLISH_STATE } from 'common/enums' - const resolver: GQLArticleResolvers['revisedAt'] = async ( - { articleId }, + { id: articleId }, _, - { dataSources: { atomService } } + { dataSources: { articleService } } ) => { - const drafts = await atomService.findMany({ - table: 'draft', - select: ['created_at'], - where: { - articleId, - archived: true, - publishState: PUBLISH_STATE.published, - }, - orderBy: [{ column: 'created_at', order: 'desc' }], - take: 2, - }) - - if (drafts.length <= 1) { - return + const count = await articleService.countArticleVersions(articleId) + if (count === 1) { + return null } + const articleVersion = await articleService.loadLatestArticleVersion( + articleId + ) - return drafts[0].createdAt + return articleVersion.createdAt } export default resolver diff --git a/src/queries/article/revisionCount.ts b/src/queries/article/revisionCount.ts index 6cf5c431c..ef234eb74 100644 --- a/src/queries/article/revisionCount.ts +++ b/src/queries/article/revisionCount.ts @@ -1,12 +1,7 @@ import type { GQLArticleResolvers } from 'definitions' -const resolver: GQLArticleResolvers['revisionCount'] = async ( - { articleId }, - _, - { dataSources: { articleService } } -) => { - const article = await articleService.dataloader.load(articleId) - return article.revisionCount || 0 -} +const resolver: GQLArticleResolvers['revisionCount'] = async ({ + revisionCount, +}) => revisionCount || 0 export default resolver diff --git a/src/queries/article/rootArticle.ts b/src/queries/article/rootArticle.ts index fe71b4d2c..74777a2a8 100644 --- a/src/queries/article/rootArticle.ts +++ b/src/queries/article/rootArticle.ts @@ -1,16 +1,27 @@ import type { GQLQueryResolvers } from 'definitions' +import { UserInputError } from 'common/errors' +import { getLogger } from 'common/logger' + +const logger = getLogger('resolver-root-articles') + const resolver: GQLQueryResolvers['article'] = async ( - root, - { input: { mediaHash } }, - { viewer, dataSources: { draftService } } + _, + { input: { mediaHash, shortHash } }, + { dataSources: { articleService, atomService } } ) => { - // since draft is becoming content container, use node here - // as variable name instead of article. The root naming - // will be changed soon in the following refactoring. - const node = await draftService.findByMediaHash(mediaHash) + if (shortHash) { + return articleService.findArticleByShortHash(shortHash) + } + if (mediaHash) { + const node = await articleService.findVersionByMediaHash(mediaHash) + if (!node) { + logger.warn('article version by media_hash:%s not found', mediaHash) + } + return atomService.articleIdLoader.load(node.articleId) + } - return node + throw new UserInputError('one of mediaHash or shortHash is required') } export default resolver diff --git a/src/queries/article/sensitiveByAuthor.ts b/src/queries/article/sensitiveByAuthor.ts new file mode 100644 index 000000000..7485a407d --- /dev/null +++ b/src/queries/article/sensitiveByAuthor.ts @@ -0,0 +1,12 @@ +import type { GQLArticleResolvers } from 'definitions' + +const resolver: GQLArticleResolvers['sensitiveByAuthor'] = async ( + { id }, + _, + { dataSources: { articleService } } +) => { + const articleVersion = await articleService.loadLatestArticleVersion(id) + return articleVersion.sensitiveByAuthor +} + +export default resolver diff --git a/src/queries/article/shortHash.ts b/src/queries/article/shortHash.ts new file mode 100644 index 000000000..6d2ec9aad --- /dev/null +++ b/src/queries/article/shortHash.ts @@ -0,0 +1,6 @@ +import type { GQLArticleResolvers } from 'definitions' + +const resolver: GQLArticleResolvers['shortHash'] = async ({ shortHash }) => + shortHash || '' + +export default resolver diff --git a/src/queries/article/slug.ts b/src/queries/article/slug.ts new file mode 100644 index 000000000..0ffe0b02c --- /dev/null +++ b/src/queries/article/slug.ts @@ -0,0 +1,14 @@ +import type { GQLArticleResolvers } from 'definitions' + +import slugify from '@matters/slugify' + +const resolver: GQLArticleResolvers['slug'] = async ( + { id }, + _, + { dataSources: { articleService } } +) => { + const articleVersion = await articleService.loadLatestArticleVersion(id) + return slugify(articleVersion.title || '') +} + +export default resolver diff --git a/src/queries/article/state.ts b/src/queries/article/state.ts index b695eb0af..af5c52a8d 100644 --- a/src/queries/article/state.ts +++ b/src/queries/article/state.ts @@ -1,12 +1,5 @@ import type { GQLArticleResolvers } from 'definitions' -const resolver: GQLArticleResolvers['state'] = async ( - { articleId }, - _, - { dataSources: { articleService } } -) => { - const article = await articleService.dataloader.load(articleId) - return article.state -} +const resolver: GQLArticleResolvers['state'] = async ({ state }) => state export default resolver diff --git a/src/queries/article/sticky.ts b/src/queries/article/sticky.ts index dccbdf11c..d293b39da 100644 --- a/src/queries/article/sticky.ts +++ b/src/queries/article/sticky.ts @@ -1,12 +1,5 @@ import type { GQLArticleResolvers } from 'definitions' -const resolver: GQLArticleResolvers['sticky'] = async ( - { articleId }, - _, - { dataSources: { articleService } } -) => { - const article = await articleService.dataloader.load(articleId) - return article.pinned -} +const resolver: GQLArticleResolvers['sticky'] = async ({ pinned }) => pinned export default resolver diff --git a/src/queries/article/subscribed.ts b/src/queries/article/subscribed.ts index ad4b65860..15bc220af 100644 --- a/src/queries/article/subscribed.ts +++ b/src/queries/article/subscribed.ts @@ -3,7 +3,7 @@ import type { GQLArticleResolvers } from 'definitions' import { USER_ACTION } from 'common/enums' const resolver: GQLArticleResolvers['subscribed'] = async ( - { articleId }, + { id: articleId }, _, { viewer, dataSources: { atomService } } ) => { diff --git a/src/queries/article/subscribers.ts b/src/queries/article/subscribers.ts index 5320484eb..875e6f620 100644 --- a/src/queries/article/subscribers.ts +++ b/src/queries/article/subscribers.ts @@ -4,12 +4,12 @@ import { USER_ACTION } from 'common/enums' import { connectionFromPromisedArray, fromConnectionArgs } from 'common/utils' const resolver: GQLArticleResolvers['subscribers'] = async ( - { articleId }, + { id: articleId }, { input }, { dataSources: { articleService, - userService, + atomService, connections: { knex }, }, } @@ -30,7 +30,7 @@ const resolver: GQLArticleResolvers['subscribers'] = async ( ) return connectionFromPromisedArray( - userService.loadByIds( + atomService.userIdLoader.loadMany( actions.map(({ userId }: { userId: string }) => userId) ), input, diff --git a/src/queries/article/summary.ts b/src/queries/article/summary.ts index 2ec71071c..99f8541c9 100644 --- a/src/queries/article/summary.ts +++ b/src/queries/article/summary.ts @@ -4,16 +4,19 @@ import { makeSummary } from '@matters/ipns-site-generator' import { ARTICLE_ACCESS_TYPE } from 'common/enums' -import { type as accessTypeResolver } from './access/type' - const resolver: GQLArticleResolvers['summary'] = async ( - parent, - args, - context, - info + { id }, + _, + { dataSources: { articleService, atomService } } ) => { - const { summary, content: cont, summaryCustomized } = parent - const accessType = await accessTypeResolver(parent, args, context, info) + const { + summary, + contentId, + summaryCustomized, + content: draftContent, + } = await articleService.loadLatestArticleVersion(id) + + const accessType = await articleService.getAccess(id) if (accessType === ARTICLE_ACCESS_TYPE.paywall) { if (!summaryCustomized) { @@ -22,7 +25,12 @@ const resolver: GQLArticleResolvers['summary'] = async ( return summary || '' } - return summary || makeSummary(cont) + if (draftContent) { + return summary || makeSummary(draftContent) + } + + const { content } = await atomService.articleContentIdLoader.load(contentId) + return summary || makeSummary(content) } export default resolver diff --git a/src/queries/article/summaryCustomized.ts b/src/queries/article/summaryCustomized.ts new file mode 100644 index 000000000..95c672251 --- /dev/null +++ b/src/queries/article/summaryCustomized.ts @@ -0,0 +1,12 @@ +import type { GQLArticleResolvers } from 'definitions' + +const resolver: GQLArticleResolvers['summaryCustomized'] = async ( + { id }, + _, + { dataSources: { articleService } } +) => { + const articleVersion = await articleService.loadLatestArticleVersion(id) + return articleVersion.summaryCustomized +} + +export default resolver diff --git a/src/queries/article/tag/articles.ts b/src/queries/article/tag/articles.ts index fa8d09582..c723a4e44 100644 --- a/src/queries/article/tag/articles.ts +++ b/src/queries/article/tag/articles.ts @@ -5,7 +5,7 @@ import { connectionFromPromisedArray, fromConnectionArgs } from 'common/utils' const resolver: GQLTagResolvers['articles'] = async ( root, { input }, - { dataSources: { tagService, articleService } } + { dataSources: { tagService, atomService } } ) => { const { selected, sortBy } = input const { take, skip } = fromConnectionArgs(input) @@ -30,7 +30,7 @@ const resolver: GQLTagResolvers['articles'] = async ( ]) return connectionFromPromisedArray( - articleService.loadDraftsByArticles(articleIds), + atomService.articleIdLoader.loadMany(articleIds), input, totalCount ) diff --git a/src/queries/article/tag/creator.ts b/src/queries/article/tag/creator.ts index 8052643ad..8f863a326 100644 --- a/src/queries/article/tag/creator.ts +++ b/src/queries/article/tag/creator.ts @@ -3,13 +3,13 @@ import type { GQLTagResolvers } from 'definitions' const resolver: GQLTagResolvers['creator'] = ( { creator }, _, - { dataSources: { userService } } + { dataSources: { atomService } } ) => { if (!creator) { return null } - return userService.loadById(creator) + return atomService.userIdLoader.load(creator) } export default resolver diff --git a/src/queries/article/tag/editors.ts b/src/queries/article/tag/editors.ts index 0d1989717..e6e8a2f1f 100644 --- a/src/queries/article/tag/editors.ts +++ b/src/queries/article/tag/editors.ts @@ -5,7 +5,7 @@ import { environment } from 'common/environment' const resolver: GQLTagResolvers['editors'] = ( { editors, owner }, { input }, - { dataSources: { userService } } + { dataSources: { atomService } } ) => { let ids = editors || [] @@ -17,7 +17,7 @@ const resolver: GQLTagResolvers['editors'] = ( ids = ids.filter((editor: string) => editor !== owner) } - return userService.loadByIds(ids) + return atomService.userIdLoader.loadMany(ids) } export default resolver diff --git a/src/queries/article/tag/followers.ts b/src/queries/article/tag/followers.ts index 75046104c..19b64c976 100644 --- a/src/queries/article/tag/followers.ts +++ b/src/queries/article/tag/followers.ts @@ -10,7 +10,7 @@ import { const resolver: GQLTagResolvers['followers'] = async ( { id }, { input }, - { dataSources: { tagService, userService } } + { dataSources: { tagService, atomService } } ) => { if (!id) { return connectionFromArray([], input) @@ -28,7 +28,7 @@ const resolver: GQLTagResolvers['followers'] = async ( {} ) - const users = await userService.loadByIds( + const users = await atomService.userIdLoader.loadMany( actions.map(({ userId }: { userId: string }) => userId) ) const data = users.map((user) => ({ ...user, __cursor: cursors[user.id] })) diff --git a/src/queries/article/tag/owner.ts b/src/queries/article/tag/owner.ts index c7c60468e..8f607e578 100644 --- a/src/queries/article/tag/owner.ts +++ b/src/queries/article/tag/owner.ts @@ -3,7 +3,7 @@ import type { GQLTagResolvers } from 'definitions' const resolver: GQLTagResolvers['owner'] = ( { owner }, _, - { dataSources: { userService } } -) => (owner ? userService.loadById(owner) : null) + { dataSources: { atomService } } +) => (owner ? atomService.userIdLoader.load(owner) : null) export default resolver diff --git a/src/queries/article/tag/participants.ts b/src/queries/article/tag/participants.ts index 85291fe8f..2b651568b 100644 --- a/src/queries/article/tag/participants.ts +++ b/src/queries/article/tag/participants.ts @@ -9,7 +9,7 @@ import { const resolver: GQLTagResolvers['participants'] = async ( { id, owner }, { input }, - { dataSources: { tagService, userService } } + { dataSources: { tagService, atomService } } ) => { if (!id) { return connectionFromArray([], input) @@ -27,7 +27,7 @@ const resolver: GQLTagResolvers['participants'] = async ( }) return connectionFromPromisedArray( - userService.loadByIds(userIds.map(({ authorId }) => authorId)), + atomService.userIdLoader.loadMany(userIds.map(({ authorId }) => authorId)), input, totalCount ) diff --git a/src/queries/article/tag/recommended.ts b/src/queries/article/tag/recommended.ts index 7b4602857..73d207759 100644 --- a/src/queries/article/tag/recommended.ts +++ b/src/queries/article/tag/recommended.ts @@ -13,7 +13,7 @@ import { const resolver: GQLTagResolvers['recommended'] = async ( { id }, { input }, - { dataSources: { tagService } } + { dataSources: { tagService, atomService } } ) => { const { take, skip } = fromConnectionArgs(input, { allowTakeAll: true, @@ -28,7 +28,7 @@ const resolver: GQLTagResolvers['recommended'] = async ( const relatedIds = await tagService.findRelatedTags({ id }) const tags = ( - await tagService.loadByIds(relatedIds.map((tag: any) => `${tag.id}`)) + await atomService.tagIdLoader.loadMany(relatedIds.map((tag) => tag.id)) ).filter(({ content }) => normalizeTagInput(content) === content) const totalCount = tags?.length ?? 0 @@ -46,7 +46,7 @@ const resolver: GQLTagResolvers['recommended'] = async ( const filteredTags = chunks[index] || [] return connectionFromPromisedArray( - tagService.loadByIds(filteredTags.map((tag: any) => `${tag.id}`)), + atomService.tagIdLoader.loadMany(filteredTags.map((tag) => tag.id)), input, totalCount ) diff --git a/src/queries/article/tag/selected.ts b/src/queries/article/tag/selected.ts index ff702068f..00ee67b7c 100644 --- a/src/queries/article/tag/selected.ts +++ b/src/queries/article/tag/selected.ts @@ -6,15 +6,15 @@ import { fromGlobalId } from 'common/utils' const resolver: GQLTagResolvers['selected'] = async ( { id }, { input }, - { dataSources: { tagService, articleService, draftService } } + { dataSources: { tagService, articleService } } ) => { let articleId: string | undefined if (input.id) { articleId = fromGlobalId(input.id).id } else if (input.mediaHash) { - const node = await draftService.findByMediaHash(input.mediaHash) - articleId = node.id + const node = await articleService.findVersionByMediaHash(input.mediaHash) + articleId = node.articleId } if (!articleId) { diff --git a/src/queries/article/tags.ts b/src/queries/article/tags.ts index dbd832f13..0806e126e 100644 --- a/src/queries/article/tags.ts +++ b/src/queries/article/tags.ts @@ -1,12 +1,12 @@ import type { GQLArticleResolvers } from 'definitions' const resolver: GQLArticleResolvers['tags'] = async ( - { articleId }, + { id: articleId }, _, - { dataSources: { articleService, tagService } } + { dataSources: { articleService, atomService } } ) => { const tagIds = await articleService.findTagIds({ id: articleId }) - return tagService.loadByIds(tagIds) + return atomService.tagIdLoader.loadMany(tagIds) } export default resolver diff --git a/src/queries/article/title.ts b/src/queries/article/title.ts new file mode 100644 index 000000000..5b25e882a --- /dev/null +++ b/src/queries/article/title.ts @@ -0,0 +1,12 @@ +import type { GQLArticleResolvers } from 'definitions' + +const resolver: GQLArticleResolvers['title'] = async ( + { id }, + _, + { dataSources: { articleService } } +) => { + const articleVersion = await articleService.loadLatestArticleVersion(id) + return articleVersion.title || '' +} + +export default resolver diff --git a/src/queries/article/topic/articleCount.ts b/src/queries/article/topic/articleCount.ts deleted file mode 100644 index 095654da5..000000000 --- a/src/queries/article/topic/articleCount.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { GQLTopicResolvers } from 'definitions' - -const resolver: GQLTopicResolvers['articleCount'] = async ( - { id: topicId }, - _, - { dataSources: { atomService } } -) => - atomService.count({ - table: 'article_topic', - where: { topicId }, - }) - -export default resolver diff --git a/src/queries/article/topic/articles.ts b/src/queries/article/topic/articles.ts deleted file mode 100644 index 0371eb76a..000000000 --- a/src/queries/article/topic/articles.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { GQLTopicResolvers } from 'definitions' - -const resolver: GQLTopicResolvers['articles'] = async ( - { id: topicId }, - _, - { dataSources: { atomService, articleService } } -) => { - const topicArticles = await atomService.findMany({ - table: 'article_topic', - where: { topicId }, - orderBy: [{ column: 'order', order: 'asc' }], - }) - - return articleService.loadDraftsByArticles( - topicArticles.map((item) => item.articleId) - ) -} - -export default resolver diff --git a/src/queries/article/topic/author.ts b/src/queries/article/topic/author.ts deleted file mode 100644 index 1232e5a41..000000000 --- a/src/queries/article/topic/author.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { GQLTopicResolvers } from 'definitions' - -const resolver: GQLTopicResolvers['author'] = ( - { userId }, - _, - { dataSources: { userService } } -) => userService.loadById(userId) - -export default resolver diff --git a/src/queries/article/topic/chapterCount.ts b/src/queries/article/topic/chapterCount.ts deleted file mode 100644 index d3c09995a..000000000 --- a/src/queries/article/topic/chapterCount.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { GQLTopicResolvers } from 'definitions' - -const resolver: GQLTopicResolvers['chapterCount'] = async ( - { id: topicId }, - _, - { dataSources: { atomService } } -) => - atomService.count({ - table: 'chapter', - where: { topicId }, - }) - -export default resolver diff --git a/src/queries/article/topic/chapters.ts b/src/queries/article/topic/chapters.ts deleted file mode 100644 index 9e1659efa..000000000 --- a/src/queries/article/topic/chapters.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { GQLTopicResolvers } from 'definitions' - -const resolver: GQLTopicResolvers['chapters'] = async ( - { id: topicId }, - _, - { dataSources: { atomService } } -) => - atomService.findMany({ - table: 'chapter', - where: { topicId }, - orderBy: [{ column: 'order', order: 'asc' }], - }) - -export default resolver diff --git a/src/queries/article/topic/cover.ts b/src/queries/article/topic/cover.ts deleted file mode 100644 index 1feb65306..000000000 --- a/src/queries/article/topic/cover.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { GQLTopicResolvers } from 'definitions' - -import _find from 'lodash/find' -import _isNil from 'lodash/isNil' - -const resolver: GQLTopicResolvers['cover'] = async ( - { cover }, - _, - { dataSources: { systemService } } -) => { - if (!cover) { - return null - } - - return systemService.findAssetUrl(cover) -} - -export default resolver diff --git a/src/queries/article/topic/latestArticle.ts b/src/queries/article/topic/latestArticle.ts deleted file mode 100644 index 5e2dd9158..000000000 --- a/src/queries/article/topic/latestArticle.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { GQLTopicResolvers } from 'definitions' - -const resolver: GQLTopicResolvers['latestArticle'] = async ( - { id: topicId }, - _, - { - dataSources: { - draftService, - connections: { knex }, - }, - } -) => { - const latestArticle = await knex - .select('article.*') - .from( - knex - .select() - .union([ - knex('article_topic') - .select('article_id') - .where({ topic_id: topicId }), - knex('article_chapter') - .select('article_id') - .leftJoin('chapter', 'chapter.id', 'article_chapter.chapter_id') - .where({ 'chapter.topic_id': topicId }), - ]) - .as('topic_articles') - ) - .leftJoin('article', 'article.id', 'topic_articles.article_id') - .orderBy([{ column: 'created_at', order: 'desc' }]) - .first() - - if (!latestArticle) { - return null - } - - return draftService.loadById(latestArticle.draftId) -} - -export default resolver diff --git a/src/queries/article/transactionsReceivedBy.ts b/src/queries/article/transactionsReceivedBy.ts index ccf12ef0a..434d75990 100644 --- a/src/queries/article/transactionsReceivedBy.ts +++ b/src/queries/article/transactionsReceivedBy.ts @@ -1,4 +1,4 @@ -import type { GQLArticleResolvers, Item } from 'definitions' +import type { GQLArticleResolvers, Transaction } from 'definitions' import { TRANSACTION_PURPOSE } from 'common/enums' import { @@ -11,9 +11,9 @@ const dashCase = (str: string) => str.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase()) const resolver: GQLArticleResolvers['transactionsReceivedBy'] = async ( - { articleId }, + { id: articleId }, { input }, - { dataSources: { articleService, userService } } + { dataSources: { articleService, atomService } } ) => { const { take, skip } = fromConnectionArgs(input) @@ -41,7 +41,9 @@ const resolver: GQLArticleResolvers['transactionsReceivedBy'] = async ( ]) return connectionFromPromisedArray( - userService.loadByIds(txs.map(({ senderId }: Item) => senderId)), + atomService.userIdLoader.loadMany( + txs.map(({ senderId }: Transaction) => senderId) + ), input, totalCount ) diff --git a/src/queries/article/translation.ts b/src/queries/article/translation.ts deleted file mode 100644 index 7a97d34f2..000000000 --- a/src/queries/article/translation.ts +++ /dev/null @@ -1,154 +0,0 @@ -import type { GQLArticleResolvers } from 'definitions' - -import { invalidateFQC } from '@matters/apollo-response-cache' -import { makeSummary } from '@matters/ipns-site-generator' - -import { ARTICLE_ACCESS_TYPE, NODE_TYPES } from 'common/enums' -import { getLogger } from 'common/logger' -import { GCP } from 'connectors' - -const logger = getLogger('query-translations') - -const resolver: GQLArticleResolvers['translation'] = async ( - { - content: originContent, - title: originTitle, - summary: originSummary, - articleId, - authorId, - language: storedLanguage, - }, - { input }, - { - viewer, - dataSources: { - atomService, - articleService, - paymentService, - tagService, - connections: { redis }, - }, - } -) => { - const language = input && input.language ? input.language : viewer.language - - // paywalled content - let isPaywalledContent = false - const isAuthor = authorId === viewer.id - const articleCircle = await articleService.findArticleCircle(articleId) - if ( - !isAuthor && - articleCircle && - articleCircle.access === ARTICLE_ACCESS_TYPE.paywall - ) { - if (viewer.id) { - const isCircleMember = await paymentService.isCircleMember({ - userId: viewer.id, - circleId: articleCircle.circleId, - }) - - // not circle member - if (!isCircleMember) { - isPaywalledContent = true - } - } else { - isPaywalledContent = true - } - } - - // it's same as original language - if (language === storedLanguage) { - return { - content: isPaywalledContent ? '' : originContent, - title: originTitle, - summary: originSummary, - language, - } - } - - // get translation - const translation = await atomService.findFirst({ - table: 'article_translation', - where: { articleId, language }, - }) - - if (translation) { - return { - ...translation, - content: isPaywalledContent ? '' : translation.content, - } - } - - const gcp = new GCP() - - // or translate and store to db - const [title, content, summary] = await Promise.all( - [ - originTitle, - originContent, - originSummary || makeSummary(originContent), - ].map((text) => - gcp.translate({ - content: text, - target: language, - }) - ) - ) - - if (title && content) { - const data = { articleId, title, content, summary, language } - await atomService.upsert({ - table: 'article_translation', - where: { articleId, language }, - create: data, - update: { ...data, updatedAt: atomService.knex.fn.now() }, - }) - - // translate tags - const tagIds = await articleService.findTagIds({ id: articleId }) - if (tagIds && tagIds.length > 0) { - try { - const tags = await tagService.loadByIds(tagIds) - await Promise.all( - tags.map(async (tag) => { - if (tag instanceof Error) { - return - } - const translatedTag = await gcp.translate({ - content: tag.content, - target: language, - }) - const tagData = { - tagId: tag.id, - content: translatedTag, - language, - } - await atomService.upsert({ - table: 'tag_translation', - where: { tagId: tag.id }, - create: tagData, - update: { ...tagData, updatedAt: atomService.knex.fn.now() }, - }) - }) - ) - } catch (error) { - logger.error(error) - } - } - - await invalidateFQC({ - node: { type: NODE_TYPES.Article, id: articleId }, - redis, - }) - - return { - title, - content: isPaywalledContent ? '' : content, - summary, - language, - } - } else { - return null - } -} -export default resolver diff --git a/src/queries/article/translation/article.ts b/src/queries/article/translation/article.ts new file mode 100644 index 000000000..fcfa9ca28 --- /dev/null +++ b/src/queries/article/translation/article.ts @@ -0,0 +1,18 @@ +import type { GQLArticleResolvers } from 'definitions' + +const resolver: GQLArticleResolvers['translation'] = async ( + { id: articleId }, + { input }, + { viewer, dataSources: { articleService } } +) => { + const language = input && input.language ? input.language : viewer.language + const articleVersion = await articleService.loadLatestArticleVersion( + articleId + ) + return articleService.getOrCreateTranslation( + articleVersion, + language, + viewer.id + ) +} +export default resolver diff --git a/src/queries/article/translation/articleVersion.ts b/src/queries/article/translation/articleVersion.ts new file mode 100644 index 000000000..4c524e4a5 --- /dev/null +++ b/src/queries/article/translation/articleVersion.ts @@ -0,0 +1,11 @@ +import type { GQLArticleVersionResolvers } from 'definitions' + +const resolver: GQLArticleVersionResolvers['translation'] = async ( + root, + { input }, + { viewer, dataSources: { articleService } } +) => { + const language = input && input.language ? input.language : viewer.language + return articleService.getOrCreateTranslation(root, language, viewer.id) +} +export default resolver diff --git a/src/queries/article/user/articles.ts b/src/queries/article/user/articles.ts index 5117ef0d3..b04ff351c 100644 --- a/src/queries/article/user/articles.ts +++ b/src/queries/article/user/articles.ts @@ -8,7 +8,7 @@ const logger = getLogger('resolver-user-articles') const resolver: GQLUserResolvers['articles'] = async ( { id }, { input }, - { dataSources: { articleService, draftService }, viewer } + { dataSources: { articleService }, viewer } ) => { if (!id) { return connectionFromArray([], input) @@ -33,10 +33,7 @@ const resolver: GQLUserResolvers['articles'] = async ( orderBy: input.sort, }) - return connectionFromPromisedArray( - draftService.loadByIds(articles.map((article: any) => article.draftId)), - input - ) + return connectionFromPromisedArray(articles, input) } export default resolver diff --git a/src/queries/article/user/topics.ts b/src/queries/article/user/topics.ts deleted file mode 100644 index ecc0367c9..000000000 --- a/src/queries/article/user/topics.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { GQLUserResolvers } from 'definitions' - -import { connectionFromArray, fromConnectionArgs } from 'common/utils' - -const resolver: GQLUserResolvers['topics'] = async ( - { id }, - { input }, - { dataSources: { atomService }, viewer } -) => { - const { take, skip } = fromConnectionArgs(input, { allowTakeAll: true }) - - if (!id) { - return connectionFromArray([], input) - } - - const isViewer = viewer.id === id - const isPublicOnly = !!input?.filter?.public || !isViewer - - const [totalCount, topics] = await Promise.all([ - atomService.count({ - table: 'topic', - where: { userId: id, ...(isPublicOnly ? { public: true } : {}) }, - }), - atomService.findMany({ - table: 'topic', - where: { userId: id, ...(isPublicOnly ? { public: true } : {}) }, - take, - skip, - orderBy: [{ column: 'order', order: 'asc' }], - }), - ]) - - return connectionFromArray(topics, input, totalCount) -} - -export default resolver diff --git a/src/queries/article/versions.ts b/src/queries/article/versions.ts new file mode 100644 index 000000000..0b052fa50 --- /dev/null +++ b/src/queries/article/versions.ts @@ -0,0 +1,18 @@ +import type { GQLArticleResolvers } from 'definitions' + +import { connectionFromArray, fromConnectionArgs } from 'common/utils' + +const resolver: GQLArticleResolvers['versions'] = async ( + { id }, + { input }, + { dataSources: { articleService } } +) => { + const { take, skip } = fromConnectionArgs(input) + const [versions, totalCount] = await articleService.findArticleVersions(id, { + take, + skip, + }) + return connectionFromArray(versions, input, totalCount) +} + +export default resolver diff --git a/src/queries/circle/analytics/content/paywall.ts b/src/queries/circle/analytics/content/paywall.ts index 24652c67b..5a048d4a1 100644 --- a/src/queries/circle/analytics/content/paywall.ts +++ b/src/queries/circle/analytics/content/paywall.ts @@ -31,11 +31,7 @@ const resolver: GQLCircleContentAnalyticsResolvers['paywall'] = async ( table: 'article', where: { id: articleId }, }) - const node = await atomService.findUnique({ - table: 'draft', - where: { id: article.draftId }, - }) - return { node, readCount } + return { node: article, readCount } }) ) return data diff --git a/src/queries/circle/analytics/content/public.ts b/src/queries/circle/analytics/content/public.ts index b486f091b..64b98d72a 100644 --- a/src/queries/circle/analytics/content/public.ts +++ b/src/queries/circle/analytics/content/public.ts @@ -31,11 +31,7 @@ const resolver: GQLCircleContentAnalyticsResolvers['public'] = async ( table: 'article', where: { id: articleId }, }) - const node = await atomService.findUnique({ - table: 'draft', - where: { id: article.draftId }, - }) - return { node, readCount } + return { node: article, readCount } }) ) return data diff --git a/src/queries/circle/analytics/income/nextMonth.ts b/src/queries/circle/analytics/income/nextMonth.ts index fd019bfb1..635afb9a8 100644 --- a/src/queries/circle/analytics/income/nextMonth.ts +++ b/src/queries/circle/analytics/income/nextMonth.ts @@ -21,7 +21,7 @@ const resolver: GQLCircleIncomeAnalyticsResolvers['nextMonth'] = async ( atomService.findFirst({ table: 'circle_price', where: { - circle_id: id, + circleId: id, state: 'active', }, }), diff --git a/src/queries/circle/followers.ts b/src/queries/circle/followers.ts index 6fc844689..753474e0c 100644 --- a/src/queries/circle/followers.ts +++ b/src/queries/circle/followers.ts @@ -10,7 +10,7 @@ import { const resolver: GQLCircleResolvers['followers'] = async ( { id }, { input }, - { dataSources: { atomService, userService } } + { dataSources: { atomService } } ) => { if (!id) { return connectionFromArray([], input) @@ -25,7 +25,7 @@ const resolver: GQLCircleResolvers['followers'] = async ( }), atomService.findMany({ table: 'action_circle', - select: ['user_id'], + select: ['userId'], where: { targetId: id, action: CIRCLE_ACTION.follow }, skip, take, @@ -33,7 +33,7 @@ const resolver: GQLCircleResolvers['followers'] = async ( ]) return connectionFromPromisedArray( - userService.loadByIds(actions.map(({ userId }) => userId)), + atomService.userIdLoader.loadMany(actions.map(({ userId }) => userId)), input, totalCount ) diff --git a/src/queries/circle/invitation/invitee.ts b/src/queries/circle/invitation/invitee.ts index 3fda00f09..e09175211 100644 --- a/src/queries/circle/invitation/invitee.ts +++ b/src/queries/circle/invitation/invitee.ts @@ -5,7 +5,7 @@ import { ServerError } from 'common/errors' const resolver: GQLInvitationResolvers['invitee'] = async ( { email, userId }, _, - { dataSources: { userService } } + { dataSources: { atomService } } ) => { if (email) { return { __type: 'Person', email } @@ -15,7 +15,7 @@ const resolver: GQLInvitationResolvers['invitee'] = async ( throw new ServerError('userId is missing') } - const user = await userService.loadById(userId) + const user = await atomService.userIdLoader.load(userId) if (user) { return { __type: 'User', ...user } } diff --git a/src/queries/circle/invitation/inviter.ts b/src/queries/circle/invitation/inviter.ts index 2342df9c5..704e374dd 100644 --- a/src/queries/circle/invitation/inviter.ts +++ b/src/queries/circle/invitation/inviter.ts @@ -3,6 +3,6 @@ import type { GQLInvitationResolvers } from 'definitions' const resolver: GQLInvitationResolvers['inviter'] = async ( { inviter }, _, - { dataSources: { userService } } -) => userService.loadById(inviter) + { dataSources: { atomService } } +) => atomService.userIdLoader.load(inviter) export default resolver diff --git a/src/queries/circle/member/user.ts b/src/queries/circle/member/user.ts index f7cfbe704..50506041e 100644 --- a/src/queries/circle/member/user.ts +++ b/src/queries/circle/member/user.ts @@ -3,7 +3,7 @@ import type { GQLMemberResolvers } from 'definitions' const resolver: GQLMemberResolvers['user'] = async ( { id }, _, - { dataSources: { userService } } -) => userService.loadById(id) + { dataSources: { atomService } } +) => atomService.userIdLoader.load(id) export default resolver diff --git a/src/queries/circle/owner.ts b/src/queries/circle/owner.ts index 3aacfc5f2..b0de78ae3 100644 --- a/src/queries/circle/owner.ts +++ b/src/queries/circle/owner.ts @@ -3,7 +3,7 @@ import type { GQLCircleResolvers } from 'definitions' const resolver: GQLCircleResolvers['owner'] = async ( { owner }, _, - { dataSources: { userService } } -) => userService.loadById(owner) + { dataSources: { atomService } } +) => atomService.userIdLoader.load(owner) export default resolver diff --git a/src/queries/circle/prices.ts b/src/queries/circle/prices.ts index 20c40cfee..fb17efd9f 100644 --- a/src/queries/circle/prices.ts +++ b/src/queries/circle/prices.ts @@ -12,7 +12,7 @@ const resolver: GQLCircleResolvers['prices'] = async ( const prices = await atomService.findMany({ table: 'circle_price', where: { - circle_id: id, + circleId: id, state: 'active', }, }) diff --git a/src/queries/circle/rootCircle.ts b/src/queries/circle/rootCircle.ts index 6036ff50a..7390b12dd 100644 --- a/src/queries/circle/rootCircle.ts +++ b/src/queries/circle/rootCircle.ts @@ -8,7 +8,7 @@ const resolver: GQLQueryResolvers['circle'] = async ( { dataSources: { atomService } } ) => { if (!name) { - return + return null } const circle = await atomService.findFirst({ diff --git a/src/queries/circle/works.ts b/src/queries/circle/works.ts index 8ad111dc0..ed61f3f64 100644 --- a/src/queries/circle/works.ts +++ b/src/queries/circle/works.ts @@ -12,7 +12,6 @@ const resolver: GQLCircleResolvers['works'] = async ( { input }, { dataSources: { - draftService, connections: { knex }, }, } @@ -36,11 +35,7 @@ const resolver: GQLCircleResolvers['works'] = async ( ]) const totalCount = parseInt(count ? (count.count as string) : '0', 10) - return connectionFromPromisedArray( - draftService.loadByIds(articles.map(({ draftId }) => draftId)), - input, - totalCount - ) + return connectionFromPromisedArray(articles, input, totalCount) } export default resolver diff --git a/src/queries/comment/article/commentCount.ts b/src/queries/comment/article/commentCount.ts index bac28c634..b8d417c3c 100644 --- a/src/queries/comment/article/commentCount.ts +++ b/src/queries/comment/article/commentCount.ts @@ -1,7 +1,7 @@ import type { GQLArticleResolvers } from 'definitions' const resolver: GQLArticleResolvers['commentCount'] = ( - { articleId }, + { id: articleId }, _, { dataSources: { commentService } } ) => commentService.countByArticle(articleId) diff --git a/src/queries/comment/article/comments.ts b/src/queries/comment/article/comments.ts index 30ef91131..e5f22bc50 100644 --- a/src/queries/comment/article/comments.ts +++ b/src/queries/comment/article/comments.ts @@ -1,10 +1,16 @@ +import type { CommentFilter } from 'connectors' import type { GQLArticleResolvers } from 'definitions' -import { NODE_TYPES } from 'common/enums' -import { fromGlobalId, toGlobalId } from 'common/utils' +import { COMMENT_TYPE } from 'common/enums' +import { + connectionFromArray, + connectionFromArrayWithKeys, + cursorToKeys, + fromGlobalId, +} from 'common/utils' const resolver: GQLArticleResolvers['comments'] = async ( - { articleId }, + { id }, { input: { sort, first, ...rest } }, { dataSources: { atomService, commentService } } ) => { @@ -21,10 +27,10 @@ const resolver: GQLArticleResolvers['comments'] = async ( let before let after if (rest.after) { - after = fromGlobalId(rest.after).id + after = cursorToKeys(rest.after).idCursor?.toString() } if (rest.before) { - before = fromGlobalId(rest.before).id + before = cursorToKeys(rest.before).idCursor?.toString() } // handle filter @@ -32,65 +38,43 @@ const resolver: GQLArticleResolvers['comments'] = async ( table: 'entity_type', where: { table: 'article' }, }) - let where = { targetId: articleId, targetTypeId } as { [key: string]: any } + + const where: CommentFilter = { + type: COMMENT_TYPE.article, + targetId: id, + targetTypeId, + } if (rest.filter) { const { parentComment, author, state } = rest.filter if (parentComment || parentComment === null) { - where = { - parentCommentId: parentComment ? fromGlobalId(parentComment).id : null, - ...where, - } + where.parentCommentId = parentComment + ? fromGlobalId(parentComment).id + : null } if (author) { - where = { - authorId: fromGlobalId(author).id, - ...where, - } + where.authorId = fromGlobalId(author).id } if (state) { - where = { - state, - ...where, - } + where.state = state } } - const [comments, range] = await Promise.all([ - commentService.find({ - sort, - before, - after, - first, - where, - order, - includeAfter: rest.includeAfter, - includeBefore: rest.includeBefore, - }), - commentService.range(where), - ]) - - const edges = comments.map((comment) => ({ - cursor: toGlobalId({ type: NODE_TYPES.Comment, id: comment.id }), - node: comment, - })) - - const firstEdge = edges[0] - const firstId = firstEdge && parseInt(firstEdge.node.id, 10) - - const lastEdge = edges[edges.length - 1] - const lastId = lastEdge && parseInt(lastEdge.node.id, 10) + const [comments, totalCount] = await commentService.find({ + sort, + before, + after, + first, + where, + order, + includeAfter: rest.includeAfter, + includeBefore: rest.includeBefore, + }) - return { - edges, - totalCount: range.count, - pageInfo: { - startCursor: firstEdge ? firstEdge.cursor : '', - endCursor: lastEdge ? lastEdge.cursor : '', - hasPreviousPage: - order === 'asc' ? firstId > range.min : firstId < range.max, - hasNextPage: order === 'asc' ? lastId < range.max : lastId > range.min, - }, + if (!comments.length) { + return connectionFromArray([], rest) } + + return connectionFromArrayWithKeys(comments, rest, totalCount) } export default resolver diff --git a/src/queries/comment/article/featuredComments.ts b/src/queries/comment/article/featuredComments.ts index a21cac858..d36ad2198 100644 --- a/src/queries/comment/article/featuredComments.ts +++ b/src/queries/comment/article/featuredComments.ts @@ -1,10 +1,10 @@ -import type { GQLArticleResolvers } from 'definitions' +import type { GQLArticleResolvers, Comment } from 'definitions' import { COMMENT_STATE, COMMENT_TYPE } from 'common/enums' import { connectionFromArray } from 'common/utils' const resolver: GQLArticleResolvers['featuredComments'] = async ( - { articleId }, + { id: articleId }, { input: { first, after } }, { dataSources: { atomService } } ) => { @@ -22,7 +22,10 @@ const resolver: GQLArticleResolvers['featuredComments'] = async ( }) // use simple pagination for now - return connectionFromArray(featuredsComments, { first, after }) + return connectionFromArray(featuredsComments as unknown as Comment[], { + first, + after, + }) } export default resolver diff --git a/src/queries/comment/article/pinCommentLeft.ts b/src/queries/comment/article/pinCommentLeft.ts index a2b6b813a..ce69d92b1 100644 --- a/src/queries/comment/article/pinCommentLeft.ts +++ b/src/queries/comment/article/pinCommentLeft.ts @@ -1,7 +1,7 @@ import type { GQLArticleResolvers } from 'definitions' const resolver: GQLArticleResolvers['pinCommentLeft'] = ( - { articleId }, + { id: articleId }, _, { dataSources: { commentService } } ) => commentService.pinLeftByArticle(articleId) diff --git a/src/queries/comment/article/pinnedComments.ts b/src/queries/comment/article/pinnedComments.ts index 1bcec969f..4d3be45aa 100644 --- a/src/queries/comment/article/pinnedComments.ts +++ b/src/queries/comment/article/pinnedComments.ts @@ -3,7 +3,7 @@ import type { GQLArticleResolvers } from 'definitions' import { COMMENT_STATE, COMMENT_TYPE } from 'common/enums' const resolver: GQLArticleResolvers['pinnedComments'] = ( - { articleId }, + { id: articleId }, _, { dataSources: { atomService } } ) => diff --git a/src/queries/comment/author.ts b/src/queries/comment/author.ts index 5b34cd743..6666d375e 100644 --- a/src/queries/comment/author.ts +++ b/src/queries/comment/author.ts @@ -3,7 +3,7 @@ import type { GQLCommentResolvers } from 'definitions' const resolver: GQLCommentResolvers['author'] = ( { authorId }, _, - { dataSources: { userService } } -) => userService.loadById(authorId) + { dataSources: { atomService } } +) => atomService.userIdLoader.load(authorId) export default resolver diff --git a/src/queries/comment/circle/broadcast.ts b/src/queries/comment/circle/broadcast.ts index 2b0b9c184..9feb3f0da 100644 --- a/src/queries/comment/circle/broadcast.ts +++ b/src/queries/comment/circle/broadcast.ts @@ -1,16 +1,18 @@ +import type { CommentFilter } from 'connectors' import type { GQLCircleResolvers } from 'definitions' -import { COMMENT_STATE, COMMENT_TYPE, NODE_TYPES } from 'common/enums' +import { COMMENT_TYPE } from 'common/enums' import { - connectionFromArray, // fromConnectionArgs + connectionFromArray, + connectionFromArrayWithKeys, + cursorToKeys, fromGlobalId, - toGlobalId, } from 'common/utils' const resolver: GQLCircleResolvers['broadcast'] = async ( { id }, { input: { sort, first, ...rest } }, - { dataSources: { commentService } } + { dataSources: { atomService, commentService } } ) => { if (!id) { return connectionFromArray([], rest) @@ -29,19 +31,23 @@ const resolver: GQLCircleResolvers['broadcast'] = async ( let before let after if (rest.after) { - after = fromGlobalId(rest.after).id + after = cursorToKeys(rest.after).idCursor?.toString() } if (rest.before) { - before = fromGlobalId(rest.before).id + before = cursorToKeys(rest.before).idCursor?.toString() } - // const { take, skip } = fromConnectionArgs(input) + // handle filter + const { id: targetTypeId } = await atomService.findFirst({ + table: 'entity_type', + where: { table: 'circle' }, + }) - const where: Record = { - state: COMMENT_STATE.active, - parentCommentId: null, + const where: CommentFilter = { targetId: id, + targetTypeId, type: COMMENT_TYPE.circleBroadcast, + parentCommentId: null, } if (rest.filter) { const { parentComment, author, state } = rest.filter @@ -58,68 +64,22 @@ const resolver: GQLCircleResolvers['broadcast'] = async ( } } - const [comments, range] = await Promise.all([ - commentService.find({ - sort, - before, - after, - first, - where, - order, - includeAfter: rest.includeAfter, - includeBefore: rest.includeBefore, - }), - commentService.range(where), - ]) - - const edges = comments.map((comment) => ({ - cursor: toGlobalId({ type: NODE_TYPES.Comment, id: comment.id }), - node: comment, - })) - - const firstEdge = edges[0] - const firstId = firstEdge && parseInt(firstEdge.node.id, 10) + const [comments, totalCount] = await commentService.find({ + sort, + before, + after, + first, + where, + order, + includeAfter: rest.includeAfter, + includeBefore: rest.includeBefore, + }) - const lastEdge = edges[edges.length - 1] - const lastId = lastEdge && parseInt(lastEdge.node.id, 10) - - return { - edges, - totalCount: range.count, - pageInfo: { - startCursor: firstEdge ? firstEdge.cursor : '', - endCursor: lastEdge ? lastEdge.cursor : '', - hasPreviousPage: - order === 'asc' ? firstId > range.min : firstId < range.max, - hasNextPage: order === 'asc' ? lastId < range.max : lastId > range.min, - }, + if (!comments.length) { + return connectionFromArray([], rest) } - /* - const [totalCount, comments] = await Promise.all([ - atomService.count({ - table: 'comment', - where, - }), - atomService.findMany({ - table: 'comment', - where, - skip, - take, - orderByRaw: ` - pinned DESC, - CASE pinned - WHEN true THEN - pinned_at - WHEN false THEN - created_at - END DESC - `, - }), - ]) - - return connectionFromArray(comments, input, totalCount) - */ + return connectionFromArrayWithKeys(comments, rest, totalCount) } export default resolver diff --git a/src/queries/comment/circle/discussion.ts b/src/queries/comment/circle/discussion.ts index 462ac75b1..301d31cbf 100644 --- a/src/queries/comment/circle/discussion.ts +++ b/src/queries/comment/circle/discussion.ts @@ -1,16 +1,18 @@ +import type { CommentFilter } from 'connectors' import type { GQLCircleResolvers } from 'definitions' -import { COMMENT_STATE, COMMENT_TYPE, NODE_TYPES } from 'common/enums' +import { COMMENT_TYPE } from 'common/enums' import { - connectionFromArray, // fromConnectionArgs + connectionFromArray, + connectionFromArrayWithKeys, + cursorToKeys, fromGlobalId, - toGlobalId, } from 'common/utils' const resolver: GQLCircleResolvers['discussion'] = async ( { id, owner }, { input: { sort, first, ...rest } }, - { viewer, dataSources: { paymentService, commentService } } + { viewer, dataSources: { atomService, paymentService, commentService } } ) => { if (!id || !viewer.id) { return connectionFromArray([], rest) @@ -39,20 +41,24 @@ const resolver: GQLCircleResolvers['discussion'] = async ( let before let after if (rest.after) { - after = fromGlobalId(rest.after).id + after = cursorToKeys(rest.after).idCursor?.toString() } if (rest.before) { - before = fromGlobalId(rest.before).id + before = cursorToKeys(rest.before).idCursor?.toString() } - // const { take, skip } = fromConnectionArgs(input) + // handle filter + const { id: targetTypeId } = await atomService.findFirst({ + table: 'entity_type', + where: { table: 'circle' }, + }) - const where: Record = { - state: COMMENT_STATE.active, - parentCommentId: null, - targetId: id, + const where = { type: COMMENT_TYPE.circleDiscussion, - } + targetId: id, + targetTypeId, + parentCommentId: null, + } as CommentFilter if (rest.filter) { const { parentComment, author, state } = rest.filter @@ -69,59 +75,22 @@ const resolver: GQLCircleResolvers['discussion'] = async ( } } - const [comments, range] = await Promise.all([ - commentService.find({ - sort, - before, - after, - first, - where, - order, - includeAfter: rest.includeAfter, - includeBefore: rest.includeBefore, - }), - commentService.range(where), - ]) - - const edges = comments.map((comment) => ({ - cursor: toGlobalId({ type: NODE_TYPES.Comment, id: comment.id }), - node: comment, - })) - - const firstEdge = edges[0] - const firstId = firstEdge && parseInt(firstEdge.node.id, 10) - - const lastEdge = edges[edges.length - 1] - const lastId = lastEdge && parseInt(lastEdge.node.id, 10) + const [comments, totalCount] = await commentService.find({ + sort, + before, + after, + first, + where, + order, + includeAfter: rest.includeAfter, + includeBefore: rest.includeBefore, + }) - return { - edges, - totalCount: range.count, - pageInfo: { - startCursor: firstEdge ? firstEdge.cursor : '', - endCursor: lastEdge ? lastEdge.cursor : '', - hasPreviousPage: - order === 'asc' ? firstId > range.min : firstId < range.max, - hasNextPage: order === 'asc' ? lastId < range.max : lastId > range.min, - }, + if (!comments.length) { + return connectionFromArray([], rest) } - /* - const [totalCount, comments] = await Promise.all([ - atomService.count({ - table: 'comment', - where, - }), - atomService.findMany({ - table: 'comment', - where, - skip, - take, - orderBy: [{ column: 'created_at', order: 'desc' }], - }), - ]) - return connectionFromArray(comments, input, totalCount) - */ + return connectionFromArrayWithKeys(comments, rest, totalCount) } export default resolver diff --git a/src/queries/comment/comments.ts b/src/queries/comment/comments.ts index 1b7dd21c1..387fb06b9 100644 --- a/src/queries/comment/comments.ts +++ b/src/queries/comment/comments.ts @@ -1,8 +1,8 @@ import type { GQLCommentResolvers } from 'definitions' -import { connectionFromPromisedArray, fromConnectionArgs } from 'common/utils' +import { connectionFromArray, fromConnectionArgs } from 'common/utils' -const resolver: GQLCommentResolvers['comments'] = ( +const resolver: GQLCommentResolvers['comments'] = async ( { id }, { input: { author, sort, ...connectionArgs } }, { dataSources: { commentService } } @@ -11,10 +11,14 @@ const resolver: GQLCommentResolvers['comments'] = ( allowTakeAll: true, }) - return connectionFromPromisedArray( - commentService.findByParent({ id, author, sort, skip, take }), - connectionArgs - ) + const [comments, totalCount] = await commentService.findByParent({ + id, + author, + sort, + skip, + take, + }) + return connectionFromArray(comments, connectionArgs, totalCount) } export default resolver diff --git a/src/queries/comment/node.ts b/src/queries/comment/node.ts index bde87177a..756870106 100644 --- a/src/queries/comment/node.ts +++ b/src/queries/comment/node.ts @@ -5,14 +5,15 @@ import { CIRCLE_STATE, COMMENT_TYPE } from 'common/enums' const resolver: GQLCommentResolvers['node'] = async ( { targetId, targetTypeId, type }, _, - { dataSources: { atomService, articleService } } + { dataSources: { atomService } } ) => { if (!targetId || !targetTypeId) { - return + // TODO: schema is not nullable, but we should handle this case + return null as any } if (type === COMMENT_TYPE.article) { - const draft = await articleService.draftLoader.load(targetId) + const draft = await atomService.articleIdLoader.load(targetId) return { ...draft, __type: 'Article' } } else { const circle = await atomService.findFirst({ diff --git a/src/queries/comment/parentComment.ts b/src/queries/comment/parentComment.ts index 32cd9f7f0..1026356fa 100644 --- a/src/queries/comment/parentComment.ts +++ b/src/queries/comment/parentComment.ts @@ -3,7 +3,8 @@ import type { GQLCommentResolvers } from 'definitions' const resolver: GQLCommentResolvers['parentComment'] = ( { parentCommentId }, _, - { dataSources: { commentService } } -) => (parentCommentId ? commentService.loadById(parentCommentId) : null) + { dataSources: { atomService } } +) => + parentCommentId ? atomService.commentIdLoader.load(parentCommentId) : null export default resolver diff --git a/src/queries/comment/replyTo.ts b/src/queries/comment/replyTo.ts index e8b5752c6..ebf977292 100644 --- a/src/queries/comment/replyTo.ts +++ b/src/queries/comment/replyTo.ts @@ -3,7 +3,7 @@ import type { GQLCommentResolvers } from 'definitions' const resolver: GQLCommentResolvers['replyTo'] = ( { replyTo }, _, - { dataSources: { commentService } } -) => (replyTo ? commentService.loadById(replyTo) : null) + { dataSources: { atomService } } +) => (replyTo ? atomService.commentIdLoader.load(replyTo) : null) export default resolver diff --git a/src/queries/comment/user/commentedArticles.ts b/src/queries/comment/user/commentedArticles.ts index e2c501ba9..10a9f4987 100644 --- a/src/queries/comment/user/commentedArticles.ts +++ b/src/queries/comment/user/commentedArticles.ts @@ -8,7 +8,6 @@ const resolver: GQLUserResolvers['commentedArticles'] = async ( { input }, { dataSources: { - draftService, connections: { knex }, }, } @@ -34,11 +33,7 @@ const resolver: GQLUserResolvers['commentedArticles'] = async ( const [count, articles] = await Promise.all([countQuery, articlesQuery]) const totalCount = parseInt(count ? (count.count as string) : '0', 10) - return connectionFromPromisedArray( - draftService.loadByIds(articles.map((article) => article.draftId)), - input, - totalCount - ) + return connectionFromPromisedArray(articles, input, totalCount) } export default resolver diff --git a/src/queries/draft/article/drafts.ts b/src/queries/draft/article/drafts.ts index 223ade4d0..72dd7997e 100644 --- a/src/queries/draft/article/drafts.ts +++ b/src/queries/draft/article/drafts.ts @@ -1,20 +1,32 @@ -import type { GQLArticleResolvers, Draft } from 'definitions' - -import publishedResolver from './newestPublishedDraft' -import unpublishedResolver from './newestUnpublishedDraft' +import type { GQLArticleResolvers } from 'definitions' const resolver: GQLArticleResolvers['drafts'] = async ( - parent, - args, - context, - info + { id: articleId, authorId }, + _, + { dataSources: { atomService } } ) => { - const drafts = await Promise.all([ - unpublishedResolver(parent, args, context, info), // keep pending unpublished before published - publishedResolver(parent, args, context, info), - ]) - - return drafts.filter((draft) => draft) as Draft[] + const versions = await atomService.findMany({ + table: 'article_version', + where: { articleId }, + orderBy: [{ column: 'created_at', order: 'desc' }], + }) + return versions.map(async (version) => { + return { + ...version, + authorId, + content: ( + await atomService.articleContentIdLoader.load(version.contentId) + ).content, + publishState: 'published', + // unused fields in front-end + contentMd: '', + iscnPublish: false, + collection: null, + remark: null, + archived: false, + language: null, + } + }) } export default resolver diff --git a/src/queries/draft/article/newestPublishedDraft.ts b/src/queries/draft/article/newestPublishedDraft.ts index 761289aa0..828e96d01 100644 --- a/src/queries/draft/article/newestPublishedDraft.ts +++ b/src/queries/draft/article/newestPublishedDraft.ts @@ -1,18 +1,29 @@ import type { GQLArticleResolvers } from 'definitions' -import { PUBLISH_STATE } from 'common/enums' - -const resolver: Exclude< - GQLArticleResolvers['newestPublishedDraft'], - undefined -> = async ({ articleId }, _, { dataSources: { atomService } }) => { - const draft = await atomService.findFirst({ - table: 'draft', - where: { articleId, publishState: PUBLISH_STATE.published }, +const resolver: GQLArticleResolvers['newestPublishedDraft'] = async ( + { id: articleId, authorId }, + _, + { dataSources: { atomService } } +) => { + const version = await atomService.findFirst({ + table: 'article_version', + where: { articleId }, orderBy: [{ column: 'created_at', order: 'desc' }], }) - - return draft + return { + ...version, + authorId, + content: (await atomService.articleContentIdLoader.load(version.contentId)) + .content, + publishState: 'published', + // unused fields in front-end + contentMd: '', + iscnPublish: false, + collection: null, + remark: null, + archived: false, + language: null, + } } export default resolver diff --git a/src/queries/draft/article/newestUnpublishedDraft.ts b/src/queries/draft/article/newestUnpublishedDraft.ts index f722631fe..4c1e31445 100644 --- a/src/queries/draft/article/newestUnpublishedDraft.ts +++ b/src/queries/draft/article/newestUnpublishedDraft.ts @@ -2,10 +2,11 @@ import type { GQLArticleResolvers } from 'definitions' import { PUBLISH_STATE } from 'common/enums' -const resolver: Exclude< - GQLArticleResolvers['newestUnpublishedDraft'], - undefined -> = async ({ articleId }, _, { dataSources: { atomService } }) => { +const resolver: GQLArticleResolvers['newestUnpublishedDraft'] = async ( + { id: articleId }, + _, + { dataSources: { atomService } } +) => { const draft = await atomService.findFirst({ table: 'draft', where: { articleId }, diff --git a/src/queries/draft/collection.ts b/src/queries/draft/collection.ts index 8e0f9b9f1..d12246d76 100644 --- a/src/queries/draft/collection.ts +++ b/src/queries/draft/collection.ts @@ -3,16 +3,16 @@ import type { GQLDraftResolvers } from 'definitions' import { connectionFromArray, connectionFromPromisedArray } from 'common/utils' const resolver: GQLDraftResolvers['collection'] = ( - { collection }, + { collection: connections }, { input }, - { dataSources: { articleService } } + { dataSources: { atomService } } ) => { - if (!collection || collection.length === 0) { + if (!connections || connections.length === 0) { return connectionFromArray([], input) } return connectionFromPromisedArray( - articleService.loadDraftsByArticles(collection), + atomService.articleIdLoader.loadMany(connections), input ) } diff --git a/src/queries/draft/index.ts b/src/queries/draft/index.ts index 304603b39..fb1491fd5 100644 --- a/src/queries/draft/index.ts +++ b/src/queries/draft/index.ts @@ -1,3 +1,5 @@ +import type { GQLResolvers } from 'definitions' + import { makeSummary } from '@matters/ipns-site-generator' import slugify from '@matters/slugify' @@ -14,7 +16,7 @@ import draftContent from './content' import draftCover from './cover' import drafts from './drafts' -export default { +const schema: GQLResolvers = { Article: { drafts: articleDrafts, newestUnpublishedDraft: articleNewestUnpublishedDraft, @@ -24,23 +26,25 @@ export default { drafts, }, Draft: { - id: ({ id }: { id: string }) => toGlobalId({ type: NODE_TYPES.Draft, id }), - slug: ({ title }: { title: string }) => slugify(title), - mediaHash: ({ mediaHash }: { mediaHash: string }) => mediaHash || '', - wordCount: ({ content }: { content?: string }) => - content ? countWords(content) : 0, - summary: ({ summary, content }: { summary?: string; content: string }) => - summary || makeSummary(content || ''), + id: ({ id }) => toGlobalId({ type: NODE_TYPES.Draft, id }), + slug: ({ title }) => slugify(title), + mediaHash: ({ mediaHash }) => mediaHash ?? '', + wordCount: ({ content }) => (content ? countWords(content) : 0), + summary: ({ summary, content }) => summary || makeSummary(content || ''), + summaryCustomized: ({ summary }) => !!summary, content: draftContent, cover: draftCover, collection, assets, - article: (root: any) => (root.articleId ? root : null), - access: (root: any) => root, - license: ({ license }: { license: any }) => license, + article: (root, _, { dataSources: { atomService } }) => + root.articleId ? atomService.articleIdLoader.load(root.articleId) : null, + access: (root) => root, + license: ({ license }) => license, }, DraftAccess: { - type: ({ access }: { access: string }) => access, + type: ({ access }) => access, circle: draftAccess.circle, }, } + +export default schema diff --git a/src/queries/index.ts b/src/queries/index.ts index c1d4c3d30..cd69a3ae6 100644 --- a/src/queries/index.ts +++ b/src/queries/index.ts @@ -8,6 +8,7 @@ import exchangeRates from './exchangeRates' import notice from './notice' import oauthClient from './oauthClient' import oauthRequestToken from './oauthRequestToken' +import recommendation from './recommendation' import response from './response' import scalars from './scalars' import system from './system' @@ -25,5 +26,6 @@ export default merge( response, oauthClient, oauthRequestToken, - exchangeRates + exchangeRates, + recommendation ) diff --git a/src/queries/notice/index.ts b/src/queries/notice/index.ts index ca3ca64c8..12508fe8e 100644 --- a/src/queries/notice/index.ts +++ b/src/queries/notice/index.ts @@ -57,7 +57,7 @@ const notice: { revised_article_not_published: NOTICE_TYPE.ArticleNotice, circle_new_article: NOTICE_TYPE.ArticleNotice, // deprecated - // article-artilce + // article-article article_new_collected: NOTICE_TYPE.ArticleArticleNotice, // comment @@ -122,11 +122,11 @@ const notice: { } throw new ServerError(`Unknown ArticleNotice type: ${type}`) }, - target: ({ entities }, _, { dataSources: { draftService } }) => { + target: ({ entities }, _, { dataSources: { atomService } }) => { if (!entities) { throw new ServerError('entities is empty') } - return draftService.loadById(entities.target.draftId) + return atomService.articleIdLoader.load(entities.target.id) }, }, ArticleArticleNotice: { @@ -137,18 +137,18 @@ const notice: { } throw new ServerError(`Unknown ArticleArticleNotice type: ${type}`) }, - target: ({ entities }, _, { dataSources: { draftService } }) => { + target: ({ entities }, _, { dataSources: { atomService } }) => { if (!entities) { throw new ServerError('entities is empty') } - return draftService.loadById(entities.target.draftId) + return atomService.articleIdLoader.load(entities.target.id) }, - article: ({ entities, type }, _, { dataSources: { draftService } }) => { + article: ({ entities, type }, _, { dataSources: { atomService } }) => { if (type === DB_NOTICE_TYPE.article_new_collected) { if (!entities) { throw new ServerError('entities is empty') } - return draftService.loadById(entities.collection.draftId) + return atomService.articleIdLoader.load(entities.collection.id) } throw new ServerError(`Unknown ArticleArticleNotice type: ${type}`) }, @@ -247,41 +247,45 @@ const notice: { } return entities.target }, - comments: async ({ data }, _, { dataSources: { commentService } }) => { + comments: async ({ data }, _, { dataSources: { atomService } }) => { const { comments } = data || {} if (!comments || comments.length <= 0) { return null } - return (await commentService.loadByIds(comments)).map((c) => ({ - ...c, - __typename: NODE_TYPES.Comment, - })) + return (await atomService.commentIdLoader.loadMany(comments)).map( + (c) => ({ + ...c, + __typename: NODE_TYPES.Comment, + }) + ) }, - replies: async ({ data }, _, { dataSources: { commentService } }) => { + replies: async ({ data }, _, { dataSources: { atomService } }) => { const { replies } = data || {} if (!replies || replies.length <= 0) { return null } - return (await commentService.loadByIds(replies)).map((c) => ({ + return (await atomService.commentIdLoader.loadMany(replies)).map((c) => ({ ...c, __typename: NODE_TYPES.Comment, })) }, - mentions: async ({ data }, _, { dataSources: { commentService } }) => { + mentions: async ({ data }, _, { dataSources: { atomService } }) => { const { mentions } = data || {} if (!mentions || mentions.length <= 0) { return null } - return (await commentService.loadByIds(mentions)).map((c) => ({ - ...c, - __typename: NODE_TYPES.Comment, - })) + return (await atomService.commentIdLoader.loadMany(mentions)).map( + (c) => ({ + ...c, + __typename: NODE_TYPES.Comment, + }) + ) }, }, OfficialAnnouncementNotice: { diff --git a/src/queries/oauthClient/user.ts b/src/queries/oauthClient/user.ts index 527e908af..d8e226b63 100644 --- a/src/queries/oauthClient/user.ts +++ b/src/queries/oauthClient/user.ts @@ -3,7 +3,7 @@ import type { GQLOAuthClientResolvers } from 'definitions' const resolver: GQLOAuthClientResolvers['user'] = async ( { userId }, _, - { dataSources: { userService } } -) => (userId ? userService.loadById(userId) : null) + { dataSources: { atomService } } +) => (userId ? atomService.userIdLoader.load(userId) : null) export default resolver diff --git a/src/queries/recommendation.ts b/src/queries/recommendation.ts new file mode 100644 index 000000000..7e8145a19 --- /dev/null +++ b/src/queries/recommendation.ts @@ -0,0 +1,16 @@ +import type { GQLResolvers } from 'definitions' + +import { NODE_TYPES, MATTERS_CHOICE_TOPIC_STATE } from 'common/enums' +import { toGlobalId } from 'common/utils' + +const recommendation: GQLResolvers = { + IcymiTopic: { + id: ({ id }) => toGlobalId({ type: NODE_TYPES.IcymiTopic, id }), + articles: async ({ articles }, _, { dataSources: { atomService } }) => + atomService.articleIdLoader.loadMany(articles), + archivedAt: ({ updatedAt, state }) => + state === MATTERS_CHOICE_TOPIC_STATE.archived ? updatedAt : null, + }, +} + +export default recommendation diff --git a/src/queries/response/article/responseCount.ts b/src/queries/response/article/responseCount.ts index cabfbf9c9..37afe1d9b 100644 --- a/src/queries/response/article/responseCount.ts +++ b/src/queries/response/article/responseCount.ts @@ -1,7 +1,7 @@ import type { GQLArticleResolvers } from 'definitions' const resolver: GQLArticleResolvers['responseCount'] = async ( - { articleId }, + { id: articleId }, _, { dataSources: { articleService, commentService } } ) => { diff --git a/src/queries/response/article/responses.ts b/src/queries/response/article/responses.ts index 3f2a2693a..17414edfc 100644 --- a/src/queries/response/article/responses.ts +++ b/src/queries/response/article/responses.ts @@ -1,17 +1,17 @@ -import type { GQLArticleResolvers } from 'definitions' +import type { GQLArticleResolvers, Article, Comment } from 'definitions' import _last from 'lodash/last' import { NODE_TYPES } from 'common/enums' +import { ServerError } from 'common/errors' import { fromGlobalId, toGlobalId } from 'common/utils' const resolver: GQLArticleResolvers['responses'] = async ( - { articleId }, + { id: articleId }, { input: { sort, first, ...restParams } }, - { dataSources: { articleService, commentService } } + { dataSources: { articleService, atomService } } ) => { const order = sort === 'oldest' ? 'asc' : 'desc' - const state = 'active' // set default first as 10, and use null for querying all. if (!restParams.before && typeof first === 'undefined') { @@ -21,77 +21,91 @@ const resolver: GQLArticleResolvers['responses'] = async ( let after let before if (restParams.after) { - after = fromGlobalId(restParams.after).id + after = fromGlobalId(restParams.after) } if (restParams.before) { - before = fromGlobalId(restParams.before).id + before = fromGlobalId(restParams.before) } // fetch order and range based on Collection and Comment const { includeAfter, includeBefore, articleOnly } = restParams - const [sources, range] = await Promise.all([ - articleService.findResponses({ - id: articleId, - order, - state, - after, - before, - first, - includeAfter, - includeBefore, - articleOnly, - }), - articleService.responseRange({ - id: articleId, - order, - state, - }), - ]) + const sources = await articleService.findResponses({ + id: articleId, + order, + after, + before, + first, + includeAfter, + includeBefore, + articleOnly, + }) // fetch responses const items = await Promise.all( - sources.map((source: { [key: string]: any }) => { + sources.map((source: { entityId: string; type: string }) => { switch (source.type) { case 'Article': { - return articleService.draftLoader.load(source.entityId) + return atomService.articleIdLoader.load(source.entityId) } case 'Comment': { - return commentService.baseFindById(source.entityId) + return atomService.commentIdLoader.load(source.entityId) + } + default: { + throw new ServerError(`Unknown response type: ${source.type}`) } } }) ) // re-process edges - const edges = items.map((item: { [key: string]: any }) => { - const type = item.title ? NODE_TYPES.Article : NODE_TYPES.Comment - const id = type === 'Article' ? item.articleId : item.id + const isArticle = (item: Article | Comment): item is Article => + !('articleId' in item) + const edges = items.map((item) => { + const type = isArticle(item) ? NODE_TYPES.Article : NODE_TYPES.Comment return { - cursor: toGlobalId({ type, id }), - node: { __type: type, ...item }, - } as any + cursor: toGlobalId({ type, id: item.id }), + node: { __type: type, ...item } as any, + } }) // handle page info - const head = sources[0] as { [key: string]: any } - const headSeq = head && parseInt(head.seq, 10) + if (!sources.length) { + return { + edges: [], + totalCount: 0, + pageInfo: { + startCursor: '', + endCursor: '', + hasPreviousPage: false, + hasNextPage: false, + }, + } + } + + const head = sources[0] + const headCursor = head && head.createdAt - const tail = _last(sources) as { [key: string]: any } - const tailSeq = tail && parseInt(tail.seq, 10) + const tail = _last(sources) + const tailCursor = tail && tail.createdAt const edgeHead = edges[0] const edgeTail = _last(edges) return { edges, - totalCount: range.count, + totalCount: +head.totalCount, pageInfo: { startCursor: edgeHead ? edgeHead.cursor : '', endCursor: edgeTail ? edgeTail.cursor : '', hasPreviousPage: - order === 'asc' ? headSeq > range.min : headSeq < range.max, - hasNextPage: order === 'asc' ? tailSeq < range.max : tailSeq > range.min, + order === 'asc' + ? headCursor > head.minCursor + : headCursor < head.maxCursor, + hasNextPage: + order === 'asc' + ? tailCursor < head.maxCursor + : tailCursor > head.minCursor, }, } } diff --git a/src/queries/system/index.ts b/src/queries/system/index.ts index d7d20580a..8d102ba54 100644 --- a/src/queries/system/index.ts +++ b/src/queries/system/index.ts @@ -5,6 +5,7 @@ import node from './node' import nodes from './nodes' import { announcements, features, translations } from './official' import OSS from './oss' +import report from './report' import search from './search' const system: GQLResolvers = { @@ -33,6 +34,7 @@ const system: GQLResolvers = { translations, }, OSS, + Report: report, } export default system diff --git a/src/queries/system/official/announcements.ts b/src/queries/system/official/announcements.ts index b44ad2d13..e6d36de18 100644 --- a/src/queries/system/official/announcements.ts +++ b/src/queries/system/official/announcements.ts @@ -6,7 +6,7 @@ import { NODE_TYPES } from 'common/enums' import { fromGlobalId, toGlobalId } from 'common/utils' export const announcements: GQLOfficialResolvers['announcements'] = async ( - root, + _, { input: { id, visible } }, { dataSources: { atomService, systemService }, viewer } ) => { @@ -45,5 +45,5 @@ export const announcements: GQLOfficialResolvers['announcements'] = async ( } }) ) - return items + return items as any } diff --git a/src/queries/system/official/translations.ts b/src/queries/system/official/translations.ts index fb0ca6a15..358c156fd 100644 --- a/src/queries/system/official/translations.ts +++ b/src/queries/system/official/translations.ts @@ -5,20 +5,15 @@ import { fromGlobalId, toGlobalId } from 'common/utils' export const translations: GQLAnnouncementResolvers['translations'] = async ( { id }, - _, // { input: { id, visible } }, - { dataSources: { atomService, systemService }, viewer } + _, + { dataSources: { atomService, systemService } } ) => { - // const isAdmin = viewer.hasRole('admin') - // const visibleFilter = !isAdmin ? { visible: true } : typeof visible === 'boolean' ? { visible } : {} - const { id: dbId } = id ? fromGlobalId(id) : { id: null } const records = await atomService.findMany({ table: 'announcement_translation', where: { ...(dbId ? { announcementId: dbId } : {}), - // ...visibleFilter, }, - // ...(dbId ? { where: { id: dbId } } : {}), orderBy: [{ column: 'createdAt', order: 'desc' }], }) @@ -26,7 +21,7 @@ export const translations: GQLAnnouncementResolvers['translations'] = async ( const items = await Promise.all( records.map(async (record) => { const cover = record?.cover - ? systemService.findAssetUrl(record.cover) + ? await systemService.findAssetUrl(record.cover) : null return { ...record, @@ -35,5 +30,5 @@ export const translations: GQLAnnouncementResolvers['translations'] = async ( } }) ) - return items + return items as any } diff --git a/src/queries/system/oss/articles.ts b/src/queries/system/oss/articles.ts index aecf043dd..6acc4d9d0 100644 --- a/src/queries/system/oss/articles.ts +++ b/src/queries/system/oss/articles.ts @@ -5,7 +5,7 @@ import { connectionFromPromisedArray, fromConnectionArgs } from 'common/utils' export const articles: GQLOssResolvers['articles'] = async ( _, { input }, - { dataSources: { articleService, draftService } } + { dataSources: { articleService } } ) => { const { take, skip } = fromConnectionArgs(input) @@ -17,9 +17,5 @@ export const articles: GQLOssResolvers['articles'] = async ( orderBy: [{ column: 'id', order: 'desc' }], }), ]) - return connectionFromPromisedArray( - draftService.loadByIds(items.map((item) => item.draftId)), - input, - totalCount - ) + return connectionFromPromisedArray(items, input, totalCount) } diff --git a/src/queries/system/oss/badgedUsers.ts b/src/queries/system/oss/badgedUsers.ts index 5ce29d476..dbd0a8677 100644 --- a/src/queries/system/oss/badgedUsers.ts +++ b/src/queries/system/oss/badgedUsers.ts @@ -7,7 +7,7 @@ export const badgedUsers: GQLOssResolvers['badgedUsers'] = async ( { input }, { dataSources: { - userService, + atomService, connections: { knex }, }, } @@ -52,7 +52,7 @@ export const badgedUsers: GQLOssResolvers['badgedUsers'] = async ( ) return connectionFromPromisedArray( - userService.loadByIds(users.map(({ userId }) => userId)), + atomService.userIdLoader.loadMany(users.map(({ userId }) => userId)), input, totalCount ) diff --git a/src/queries/system/oss/icymiTopics.ts b/src/queries/system/oss/icymiTopics.ts new file mode 100644 index 000000000..f07ace417 --- /dev/null +++ b/src/queries/system/oss/icymiTopics.ts @@ -0,0 +1,24 @@ +import type { GQLOssResolvers } from 'definitions' + +import { connectionFromPromisedArray, fromConnectionArgs } from 'common/utils' + +export const icymiTopics: GQLOssResolvers['icymiTopics'] = async ( + _, + { input }, + { dataSources: { atomService } } +) => { + const { take, skip } = fromConnectionArgs(input) + + const totalCount = await atomService.count({ table: 'matters_choice_topic' }) + + return connectionFromPromisedArray( + atomService.findMany({ + table: 'matters_choice_topic', + skip, + take, + orderBy: [{ column: 'id', order: 'desc' }], + }), + input, + totalCount + ) +} diff --git a/src/queries/system/oss/index.ts b/src/queries/system/oss/index.ts index 24752a2c1..d38c9d976 100644 --- a/src/queries/system/oss/index.ts +++ b/src/queries/system/oss/index.ts @@ -1,7 +1,9 @@ import { articles } from './articles' import { badgedUsers } from './badgedUsers' import { comments } from './comments' +import { icymiTopics } from './icymiTopics' import { oauthClients } from './oauthClients' +import { reports } from './reports' import { restrictedUsers } from './restrictedUsers' import { seedingUsers } from './seedingUsers' import { skippedListItems } from './skippedListItems' @@ -18,4 +20,6 @@ export default { badgedUsers, skippedListItems, restrictedUsers, + reports, + icymiTopics, } diff --git a/src/queries/system/oss/reports.ts b/src/queries/system/oss/reports.ts new file mode 100644 index 000000000..36cee9e01 --- /dev/null +++ b/src/queries/system/oss/reports.ts @@ -0,0 +1,23 @@ +import type { GQLOssResolvers } from 'definitions' + +import { connectionFromPromisedArray, fromConnectionArgs } from 'common/utils' + +export const reports: GQLOssResolvers['reports'] = async ( + _, + { input }, + { dataSources: { atomService } } +) => { + const { take, skip } = fromConnectionArgs(input) + + const table = 'report' + const countQuery = atomService.count({ table, where: {} }) + const reportsQuery = atomService.findMany({ + table, + skip, + take, + orderBy: [{ column: 'created_at', order: 'desc' }], + }) + const [totalCount, items] = await Promise.all([countQuery, reportsQuery]) + + return connectionFromPromisedArray(items, input, totalCount) +} diff --git a/src/queries/system/oss/seedingUsers.ts b/src/queries/system/oss/seedingUsers.ts index 29751a87f..f30aaad4f 100644 --- a/src/queries/system/oss/seedingUsers.ts +++ b/src/queries/system/oss/seedingUsers.ts @@ -20,7 +20,7 @@ export const seedingUsers: GQLOssResolvers['seedingUsers'] = async ( const [totalCount, users] = await Promise.all([countQuery, usersQuery]) return connectionFromPromisedArray( - userService.loadByIds(users.map(({ userId }) => userId)), + atomService.userIdLoader.loadMany(users.map(({ userId }) => userId)), input, totalCount ) diff --git a/src/queries/system/report.ts b/src/queries/system/report.ts new file mode 100644 index 000000000..c85792b03 --- /dev/null +++ b/src/queries/system/report.ts @@ -0,0 +1,32 @@ +import type { GQLReportResolvers } from 'definitions' + +import { NODE_TYPES } from 'common/enums' +import { ServerError } from 'common/errors' +import { toGlobalId } from 'common/utils' + +const report: GQLReportResolvers = { + id: ({ id }) => toGlobalId({ type: NODE_TYPES.Report, id }), + reporter: ({ reporterId }, _, { dataSources: { atomService } }) => + atomService.userIdLoader.load(reporterId), + target: async ( + { articleId, commentId }, + _, + { dataSources: { atomService } } + ) => { + if (articleId) { + return { + ...(await atomService.articleIdLoader.load(articleId)), + __type: NODE_TYPES.Article, + } + } else if (commentId) { + return { + ...(await atomService.commentIdLoader.load(commentId)), + __type: NODE_TYPES.Comment, + } + } else { + throw new ServerError('target not found') + } + }, +} + +export default report diff --git a/src/queries/system/search.ts b/src/queries/system/search.ts index a3b53407a..3a5fbaec1 100644 --- a/src/queries/system/search.ts +++ b/src/queries/system/search.ts @@ -1,4 +1,4 @@ -import type { GQLNode, GQLQueryResolvers } from 'definitions' +import type { GQLNode, GQLQueryResolvers, SearchHistory } from 'definitions' import { compact } from 'lodash' @@ -34,7 +34,7 @@ const resolver: GQLQueryResolvers['search'] = async ( } if (input.key && input.record) { - systemService.baseCreate( + systemService.baseCreate( { userId: viewer ? viewer.id : null, searchKey: input.key }, 'search_history' ) diff --git a/src/queries/system/utils.ts b/src/queries/system/utils.ts index 4200647dd..20f9ce2de 100644 --- a/src/queries/system/utils.ts +++ b/src/queries/system/utils.ts @@ -1,4 +1,4 @@ -import type { Context } from 'definitions' +import type { Context, Draft } from 'definitions' import { NODE_TYPES } from 'common/enums' import { EntityNotFoundError, ForbiddenError } from 'common/errors' @@ -7,26 +7,18 @@ import { fromGlobalId } from 'common/utils' export const getNode = async (globalId: string, context: Context) => { const { viewer, - dataSources: { - articleService, - atomService, - collectionService, - userService, - commentService, - draftService, - tagService, - }, + dataSources: { atomService }, } = context const services = { - [NODE_TYPES.Article]: articleService.draftLoader, - [NODE_TYPES.User]: userService.dataloader, - [NODE_TYPES.Comment]: commentService.dataloader, - [NODE_TYPES.Draft]: draftService.dataloader, - [NODE_TYPES.Tag]: tagService.dataloader, + [NODE_TYPES.Article]: atomService.articleIdLoader, + [NODE_TYPES.ArticleVersion]: atomService.articleVersionIdLoader, + [NODE_TYPES.User]: atomService.userIdLoader, + [NODE_TYPES.Comment]: atomService.commentIdLoader, + [NODE_TYPES.Draft]: atomService.draftIdLoader, + [NODE_TYPES.Tag]: atomService.tagIdLoader, [NODE_TYPES.Circle]: atomService.circleIdLoader, - [NODE_TYPES.Topic]: atomService.topicIdLoader, - [NODE_TYPES.Chapter]: atomService.chapterIdLoader, - [NODE_TYPES.Collection]: collectionService.dataloader, + [NODE_TYPES.Collection]: atomService.collectionIdLoader, + [NODE_TYPES.IcymiTopic]: atomService.icymiTopicIdLoader, } as const const { type, id } = fromGlobalId(globalId) @@ -43,7 +35,7 @@ export const getNode = async (globalId: string, context: Context) => { throw new EntityNotFoundError('target does not exist') } - if (type === 'Draft' && viewer.id !== node.authorId) { + if (type === 'Draft' && viewer.id !== (node as Draft).authorId) { throw new ForbiddenError('only author is allowed to view draft') } diff --git a/src/queries/user/analytics/topDonators.ts b/src/queries/user/analytics/topDonators.ts index 573c99b53..c5d6ab82f 100644 --- a/src/queries/user/analytics/topDonators.ts +++ b/src/queries/user/analytics/topDonators.ts @@ -9,7 +9,7 @@ import { const resolver: GQLUserAnalyticsResolvers['topDonators'] = async ( { id }, { input }, - { dataSources: { userService } } + { dataSources: { atomService, userService } } ) => { if (!id) { return connectionFromArray([], input) @@ -30,7 +30,7 @@ const resolver: GQLUserAnalyticsResolvers['topDonators'] = async ( ...connection, edges: connection.edges.map(async (edge) => ({ cursor: edge.cursor, - node: await userService.loadById(edge.node.senderId), + node: await atomService.userIdLoader.load(edge.node.senderId), donationCount: edge.node.count, })), } as any diff --git a/src/queries/user/appreciation.ts b/src/queries/user/appreciation.ts index b5edc4374..591ba6759 100644 --- a/src/queries/user/appreciation.ts +++ b/src/queries/user/appreciation.ts @@ -1,7 +1,6 @@ import type { GQLAppreciationResolvers, GQLAppreciationPurpose, - Draft, } from 'definitions' import { camelCase } from 'lodash' @@ -48,19 +47,25 @@ const trans = { export const Appreciation: GQLAppreciationResolvers = { purpose: ({ purpose }) => camelCase(purpose) as GQLAppreciationPurpose, - content: async (trx, _, { viewer, dataSources: { articleService } }) => { + content: async ( + trx, + _, + { viewer, dataSources: { atomService, articleService } } + ) => { switch (trx.purpose) { case APPRECIATION_PURPOSE.appreciate: - case APPRECIATION_PURPOSE.superlike: - const node = await articleService.draftLoader.load( + case APPRECIATION_PURPOSE.superlike: { + const article = await atomService.articleIdLoader.load( trx.referenceId as string ) - if (!node) { + if (!article) { throw new ArticleNotFoundError( 'reference article linked draft not found' ) } + const node = await articleService.loadLatestArticleVersion(article.id) return node.title + } case APPRECIATION_PURPOSE.appreciateSubsidy: return trans.appreciateSubsidy(viewer.language, {}) case APPRECIATION_PURPOSE.systemSubsidy: @@ -82,12 +87,12 @@ export const Appreciation: GQLAppreciationResolvers = { return '' } }, - sender: (trx, _, { dataSources: { userService } }) => - trx.senderId ? userService.loadById(trx.senderId) : null, - recipient: (trx, _, { dataSources: { userService } }) => - userService.loadById(trx.recipientId), - target: (trx, _, { dataSources: { articleService } }) => + sender: (trx, _, { dataSources: { atomService } }) => + trx.senderId ? atomService.userIdLoader.load(trx.senderId) : null, + recipient: (trx, _, { dataSources: { atomService } }) => + atomService.userIdLoader.load(trx.recipientId), + target: (trx, _, { dataSources: { atomService } }) => trx.purpose === APPRECIATION_PURPOSE.appreciate && trx.referenceId - ? (articleService.draftLoader.load(trx.referenceId) as Promise) + ? atomService.articleIdLoader.load(trx.referenceId) : null, } diff --git a/src/queries/user/blockList.ts b/src/queries/user/blockList.ts index 47d2a62f9..8d69bd67e 100644 --- a/src/queries/user/blockList.ts +++ b/src/queries/user/blockList.ts @@ -9,7 +9,7 @@ import { const resolver: GQLUserResolvers['blockList'] = async ( { id }, { input }, - { dataSources: { userService } } + { dataSources: { atomService, userService } } ) => { if (!id) { return connectionFromArray([], input) @@ -21,7 +21,7 @@ const resolver: GQLUserResolvers['blockList'] = async ( const actions = await userService.findBlockList({ userId: id, skip, take }) return connectionFromPromisedArray( - userService.loadByIds( + atomService.userIdLoader.loadMany( actions.map(({ targetId }: { targetId: string }) => targetId) ), input, diff --git a/src/queries/user/collection/articles.ts b/src/queries/user/collection/articles.ts index 5f19e4f98..83d627191 100644 --- a/src/queries/user/collection/articles.ts +++ b/src/queries/user/collection/articles.ts @@ -9,7 +9,7 @@ import { const resolver: GQLCollectionResolvers['articles'] = async ( { id: collectionId }, { input: { first, after, reversed } }, - { dataSources: { draftService, collectionService } } + { dataSources: { atomService, collectionService } } ) => { if (!collectionId) { return connectionFromArray([], { first, after }) @@ -18,7 +18,7 @@ const resolver: GQLCollectionResolvers['articles'] = async ( const { skip, take } = fromConnectionArgs({ first, after }) if (take === 0) { - const [_, count] = await collectionService.findAndCountArticlesInCollection( + const [, count] = await collectionService.findAndCountArticlesInCollection( collectionId, { skip, @@ -37,7 +37,9 @@ const resolver: GQLCollectionResolvers['articles'] = async ( }) return connectionFromPromisedArray( - draftService.loadByIds(articles.map(({ draftId }) => draftId)), + atomService.articleIdLoader.loadMany( + articles.map(({ articleId }) => articleId) + ), { first, after }, totalCount ) diff --git a/src/queries/user/collection/author.ts b/src/queries/user/collection/author.ts index e12362ffd..f123442a2 100644 --- a/src/queries/user/collection/author.ts +++ b/src/queries/user/collection/author.ts @@ -3,7 +3,7 @@ import type { GQLCollectionResolvers } from 'definitions' const resolver: GQLCollectionResolvers['author'] = ( { authorId }, _, - { dataSources: { userService } } -) => userService.loadById(authorId) + { dataSources: { atomService } } +) => atomService.userIdLoader.load(authorId) export default resolver diff --git a/src/queries/user/featuredTags.ts b/src/queries/user/featuredTags.ts index e1b488374..a719c722d 100644 --- a/src/queries/user/featuredTags.ts +++ b/src/queries/user/featuredTags.ts @@ -3,7 +3,7 @@ import type { GQLUserInfoResolvers } from 'definitions' const resolver: GQLUserInfoResolvers['featuredTags'] = async ( { id }, _, - { dataSources: { atomService, tagService } } + { dataSources: { atomService } } ) => { if (id === undefined) { return null @@ -14,7 +14,7 @@ const resolver: GQLUserInfoResolvers['featuredTags'] = async ( where: { userId: id }, }) - return tagService.loadByIds(userTags?.tagIds) + return atomService.tagIdLoader.loadMany(userTags?.tagIds) } export default resolver diff --git a/src/queries/user/followers.ts b/src/queries/user/followers.ts index c9083b545..1427902c7 100644 --- a/src/queries/user/followers.ts +++ b/src/queries/user/followers.ts @@ -10,7 +10,7 @@ import { const resolver: GQLUserResolvers['followers'] = async ( { id }, { input }, - { dataSources: { userService } } + { dataSources: { atomService, userService } } ) => { if (!id) { return connectionFromArray([], input) @@ -27,7 +27,7 @@ const resolver: GQLUserResolvers['followers'] = async ( (map, action) => ({ ...map, [action.userId]: action.id }), {} ) - const users = await userService.loadByIds( + const users = await atomService.userIdLoader.loadMany( actions.map(({ userId }: { userId: string }) => userId) ) const data = users.map((user) => ({ ...user, __cursor: cursors[user.id] })) diff --git a/src/queries/user/following/circles.ts b/src/queries/user/following/circles.ts index 56adeca50..95f84add7 100644 --- a/src/queries/user/following/circles.ts +++ b/src/queries/user/following/circles.ts @@ -25,7 +25,7 @@ const resolver: GQLFollowingResolvers['circles'] = async ( }), atomService.findMany({ table: 'action_circle', - select: ['target_id'], + select: ['targetId'], where: { userId: id, action: CIRCLE_ACTION.follow }, skip, take, diff --git a/src/queries/user/following/tags.ts b/src/queries/user/following/tags.ts index 109a083df..ef5a1ba92 100644 --- a/src/queries/user/following/tags.ts +++ b/src/queries/user/following/tags.ts @@ -10,7 +10,7 @@ import { const resolver: GQLFollowingResolvers['tags'] = async ( { id }, { input }, - { dataSources: { atomService, tagService } } + { dataSources: { atomService } } ) => { if (!id) { return connectionFromArray([], input) @@ -25,7 +25,7 @@ const resolver: GQLFollowingResolvers['tags'] = async ( }), atomService.findMany({ table: 'action_tag', - select: ['target_id'], + select: ['targetId'], where: { userId: id, action: TAG_ACTION.follow }, skip, take, @@ -33,7 +33,7 @@ const resolver: GQLFollowingResolvers['tags'] = async ( ]) return connectionFromPromisedArray( - tagService.loadByIds(actions.map(({ targetId }) => targetId)), + atomService.tagIdLoader.loadMany(actions.map(({ targetId }) => targetId)), input, totalCount ) diff --git a/src/queries/user/following/users.ts b/src/queries/user/following/users.ts index 8d1bf525d..e61714743 100644 --- a/src/queries/user/following/users.ts +++ b/src/queries/user/following/users.ts @@ -9,7 +9,7 @@ import { const resolver: GQLFollowingResolvers['users'] = async ( { id }, { input }, - { dataSources: { userService } } + { dataSources: { userService, atomService } } ) => { if (!id) { return connectionFromArray([], input) @@ -21,7 +21,7 @@ const resolver: GQLFollowingResolvers['users'] = async ( const actions = await userService.findFollowees({ userId: id, skip, take }) return connectionFromPromisedArray( - userService.loadByIds( + atomService.userIdLoader.loadMany( actions.map(({ targetId }: { targetId: string }) => targetId) ), input, diff --git a/src/queries/user/index.ts b/src/queries/user/index.ts index 86ce2978d..6e9843d73 100644 --- a/src/queries/user/index.ts +++ b/src/queries/user/index.ts @@ -47,6 +47,7 @@ import isBlocking from './isBlocking' import isFollowee from './isFollowee' import isFollower from './isFollower' import isWalletAuth from './isWalletAuth' +import latestWorks from './latestWorks' import Liker from './liker' import likerId from './liker/likerId' import { hasNFTs, nfts } from './nfts' @@ -124,6 +125,7 @@ const user: { // hasFollowed, subscriptions, collections, + latestWorks, pinnedWorks, followers, isFollower, diff --git a/src/queries/user/latestWorks.ts b/src/queries/user/latestWorks.ts new file mode 100644 index 000000000..901251ac6 --- /dev/null +++ b/src/queries/user/latestWorks.ts @@ -0,0 +1,32 @@ +import type { GQLUserResolvers } from 'definitions' + +import { NODE_TYPES, LATEST_WORKS_NUM } from 'common/enums' + +const resolver: GQLUserResolvers['latestWorks'] = async ( + { id }, + _, + { dataSources: { articleService, collectionService } } +) => { + const [articles, collections] = await Promise.all([ + articleService.findByAuthor(id, { + take: LATEST_WORKS_NUM, + }), + collectionService.findByAuthor(id, { take: LATEST_WORKS_NUM }, true), + ]) + const works = [ + ...articles.map((article) => ({ + ...article, + __type: NODE_TYPES.Article, + })), + ...collections.map((collection) => ({ + ...collection, + __type: NODE_TYPES.Collection, + })), + ] + .sort((a, b) => a.updatedAt.getTime() - b.updatedAt.getTime()) + .slice(0, LATEST_WORKS_NUM) + + return works +} + +export default resolver diff --git a/src/queries/user/pinnedWorks.ts b/src/queries/user/pinnedWorks.ts index afada1c04..cf5e7c182 100644 --- a/src/queries/user/pinnedWorks.ts +++ b/src/queries/user/pinnedWorks.ts @@ -5,7 +5,7 @@ import { NODE_TYPES } from 'common/enums' const resolver: GQLUserResolvers['pinnedWorks'] = async ( { id }, _, - { dataSources: { articleService, collectionService, draftService } } + { dataSources: { articleService, collectionService } } ) => { const [articles, collections] = await Promise.all([ articleService.findPinnedByAuthor(id), @@ -19,16 +19,7 @@ const resolver: GQLUserResolvers['pinnedWorks'] = async ( })), ].sort((a, b) => a.pinnedAt.getTime() - b.pinnedAt.getTime()) - return await Promise.all( - pinnedWorks.map(async (work) => { - if (work.__type === NODE_TYPES.Article) { - const draft = await draftService.loadById(work.draftId) - return { ...draft, __type: NODE_TYPES.Article } - } else { - return work - } - }) - ) + return pinnedWorks } export default resolver diff --git a/src/queries/user/recommendation/following/index.ts b/src/queries/user/recommendation/following/index.ts index 1e7f2a73b..086ca59ba 100644 --- a/src/queries/user/recommendation/following/index.ts +++ b/src/queries/user/recommendation/following/index.ts @@ -62,15 +62,15 @@ const resolver: GQLRecommendationResolvers['following'] = async ( const nodeLoader = ({ id, type }: { id: string; type: string }) => { switch (type) { case 'Article': - return articleService.draftLoader.load(id) + return atomService.articleIdLoader.load(id) case 'Comment': - return commentService.dataloader.load(id) + return atomService.commentIdLoader.load(id) case 'Circle': return atomService.findFirst({ table: 'circle', where: { id } }) case 'User': - return userService.dataloader.load(id) + return atomService.userIdLoader.load(id) case 'Tag': - return tagService.dataloader.load(id) + return atomService.tagIdLoader.load(id) } } const activityLoader = async ({ @@ -83,7 +83,7 @@ const resolver: GQLRecommendationResolvers['following'] = async ( createdAt, }: any) => ({ __type: type, - actor: await userService.dataloader.load(actorId), + actor: await atomService.userIdLoader.load(actorId), node: await nodeLoader({ id: nodeId, type: nodeType }), target: await nodeLoader({ id: targetId, type: targetType }), createdAt, @@ -171,7 +171,7 @@ const resolver: GQLRecommendationResolvers['following'] = async ( node: { __type: 'UserRecommendationActivity', source, - nodes: await userService.dataloader.loadMany( + nodes: await atomService.userIdLoader.loadMany( recommendation.map(({ nodeId }: { nodeId: string }) => nodeId) ), }, @@ -183,7 +183,7 @@ const resolver: GQLRecommendationResolvers['following'] = async ( node: { __type: 'ArticleRecommendationActivity', source, - nodes: await articleService.draftLoader.loadMany( + nodes: await atomService.articleIdLoader.loadMany( recommendation.map(({ nodeId }: { nodeId: string }) => nodeId) ), }, @@ -195,7 +195,7 @@ const resolver: GQLRecommendationResolvers['following'] = async ( node: { __type: 'ArticleRecommendationActivity', source, - nodes: await articleService.draftLoader.loadMany( + nodes: await atomService.articleIdLoader.loadMany( recommendation.map(({ articleId }) => articleId) ), }, diff --git a/src/queries/user/recommendation/hottest.ts b/src/queries/user/recommendation/hottest.ts index 0169752f6..b8535823c 100644 --- a/src/queries/user/recommendation/hottest.ts +++ b/src/queries/user/recommendation/hottest.ts @@ -12,7 +12,6 @@ export const hottest: GQLRecommendationResolvers['hottest'] = async ( { viewer, dataSources: { - draftService, connections: { knexRO }, }, } @@ -30,7 +29,7 @@ export const hottest: GQLRecommendationResolvers['hottest'] = async ( const MAX_ITEM_COUNT = DEFAULT_TAKE_PER_PAGE * 50 const makeHottestQuery = () => { const query = knexRO - .select('article.draft_id', knexRO.raw('count(1) OVER() AS total_count')) + .select('article.*', knexRO.raw('count(1) OVER() AS total_count')) .from( knexRO .select() @@ -70,9 +69,5 @@ export const hottest: GQLRecommendationResolvers['hottest'] = async ( const totalCount = articles.length === 0 ? 0 : +articles[0].totalCount - return connectionFromPromisedArray( - draftService.loadByIds(articles.map(({ draftId }) => draftId)), - input, - totalCount - ) + return connectionFromPromisedArray(articles, input, totalCount) } diff --git a/src/queries/user/recommendation/icymi.ts b/src/queries/user/recommendation/icymi.ts index bb4d87f61..3d76779e7 100644 --- a/src/queries/user/recommendation/icymi.ts +++ b/src/queries/user/recommendation/icymi.ts @@ -1,52 +1,16 @@ import type { GQLRecommendationResolvers } from 'definitions' -import { ARTICLE_STATE, DEFAULT_TAKE_PER_PAGE } from 'common/enums' -import { connectionFromPromisedArray, fromConnectionArgs } from 'common/utils' +import { connectionFromArray, fromConnectionArgs } from 'common/utils' export const icymi: GQLRecommendationResolvers['icymi'] = async ( _, { input }, - { - dataSources: { - draftService, - connections: { knex }, - }, - } + { dataSources: { recommendationService } } ) => { const { take, skip } = fromConnectionArgs(input) - - const MAX_ITEM_COUNT = DEFAULT_TAKE_PER_PAGE * 50 - const makeICYMIQuery = () => - knex - .select('article.draft_id') - .from( - knex - .select() - .from('matters_choice') - .orderBy('updated_at', 'desc') - .limit(MAX_ITEM_COUNT) - .as('choice') - ) - .leftJoin('article', 'choice.article_id', 'article.id') - .where({ state: ARTICLE_STATE.active }) - .as('icymi') - - const [countRecord, articles] = await Promise.all([ - knex.select().from(makeICYMIQuery()).count().first(), - makeICYMIQuery() - .orderBy('choice.updated_at', 'desc') - .offset(skip) - .limit(take), - ]) - - const totalCount = parseInt( - countRecord ? (countRecord.count as string) : '0', - 10 - ) - - return connectionFromPromisedArray( - draftService.loadByIds(articles.map(({ draftId }) => draftId)), - input, - totalCount - ) + const [articles, totalCount] = await recommendationService.findIcymiArticles({ + take, + skip, + }) + return connectionFromArray(articles, input, totalCount) } diff --git a/src/queries/user/recommendation/icymiTopic.ts b/src/queries/user/recommendation/icymiTopic.ts new file mode 100644 index 000000000..7e2c045a2 --- /dev/null +++ b/src/queries/user/recommendation/icymiTopic.ts @@ -0,0 +1,13 @@ +import type { GQLRecommendationResolvers } from 'definitions' + +import { MATTERS_CHOICE_TOPIC_STATE } from 'common/enums' + +export const icymiTopic: GQLRecommendationResolvers['icymiTopic'] = async ( + _, + __, + { dataSources: { atomService } } +) => + atomService.findFirst({ + table: 'matters_choice_topic', + where: { state: MATTERS_CHOICE_TOPIC_STATE.published }, + }) diff --git a/src/queries/user/recommendation/index.ts b/src/queries/user/recommendation/index.ts index 7c4fb7ea5..3f06291f3 100644 --- a/src/queries/user/recommendation/index.ts +++ b/src/queries/user/recommendation/index.ts @@ -6,6 +6,7 @@ import { hottest } from './hottest' import hottestCircles from './hottestCircles' import { hottestTags } from './hottestTags' import { icymi } from './icymi' +import { icymiTopic } from './icymiTopic' import { newest } from './newest' import newestCircles from './newestCircles' import readTagsArticles from './readTagsArticles' @@ -18,6 +19,7 @@ const resolvers: GQLRecommendationResolvers = { readTagsArticles, hottest, icymi, + icymiTopic, newest, tags, hottestTags, diff --git a/src/queries/user/recommendation/newest.ts b/src/queries/user/recommendation/newest.ts index d1c4ff951..9b465b8cb 100644 --- a/src/queries/user/recommendation/newest.ts +++ b/src/queries/user/recommendation/newest.ts @@ -7,7 +7,7 @@ import { connectionFromPromisedArray, fromConnectionArgs } from 'common/utils' export const newest: GQLRecommendationResolvers['newest'] = async ( _, { input }, - { viewer, dataSources: { articleService, draftService } } + { viewer, dataSources: { articleService } } ) => { const { oss = false } = input @@ -28,7 +28,7 @@ export const newest: GQLRecommendationResolvers['newest'] = async ( }) return connectionFromPromisedArray( - draftService.loadByIds(articles.map(({ draftId }) => draftId)), + articles, input, MAX_ITEM_COUNT // totalCount ) diff --git a/src/queries/user/recommendation/readTagsArticles.ts b/src/queries/user/recommendation/readTagsArticles.ts index 6fe53caf5..96391171a 100644 --- a/src/queries/user/recommendation/readTagsArticles.ts +++ b/src/queries/user/recommendation/readTagsArticles.ts @@ -1,6 +1,4 @@ -import type { GQLRecommendationResolvers, Draft } from 'definitions' - -import _last from 'lodash/last' +import type { GQLRecommendationResolvers } from 'definitions' import { connectionFromArray, @@ -11,7 +9,7 @@ import { const resolver: GQLRecommendationResolvers['readTagsArticles'] = async ( { id: userId }, { input }, - { dataSources: { articleService, atomService } } + { dataSources: { atomService } } ) => { if (!userId) { return connectionFromArray([], input) @@ -19,7 +17,7 @@ const resolver: GQLRecommendationResolvers['readTagsArticles'] = async ( const { take, skip } = fromConnectionArgs(input) - const [totalCount, tagDrafts] = await Promise.all([ + const [totalCount, tagArticles] = await Promise.all([ atomService.count({ table: 'recommended_articles_from_read_tags_materialized', where: { userId }, @@ -33,9 +31,7 @@ const resolver: GQLRecommendationResolvers['readTagsArticles'] = async ( ]) return connectionFromPromisedArray( - articleService.loadDraftsByArticles( - tagDrafts.map(({ articleId }) => articleId) - ) as Promise, + atomService.articleIdLoader.loadMany(tagArticles.map((d) => d.articleId)), input, totalCount ) diff --git a/src/queries/user/recommendation/tags.ts b/src/queries/user/recommendation/tags.ts index a8d4d4ec2..255d35d26 100644 --- a/src/queries/user/recommendation/tags.ts +++ b/src/queries/user/recommendation/tags.ts @@ -8,9 +8,9 @@ import { ForbiddenError } from 'common/errors' import { connectionFromPromisedArray, fromConnectionArgs } from 'common/utils' export const tags: GQLRecommendationResolvers['tags'] = async ( - { id }, + _, { input }, - { viewer, dataSources: { tagService } } + { viewer, dataSources: { tagService, atomService } } ) => { const { filter, oss = false } = input const { take, skip } = fromConnectionArgs(input, { defaultTake: 5 }) @@ -38,7 +38,7 @@ export const tags: GQLRecommendationResolvers['tags'] = async ( const filteredTags = chunks[index] || [] return connectionFromPromisedArray( - tagService.loadByIds(filteredTags.map((tag: any) => `${tag.id}`)), + atomService.tagIdLoader.loadMany(filteredTags.map((tag) => tag.id)), input, curationTags.length ) @@ -54,7 +54,7 @@ export const tags: GQLRecommendationResolvers['tags'] = async ( }) return connectionFromPromisedArray( - tagService.loadByIds(items.map((item: any) => `${item.id}`)), + atomService.tagIdLoader.loadMany(items.map((item) => item.id)), input, totalCount ) diff --git a/src/queries/user/subscriptions.ts b/src/queries/user/subscriptions.ts index 8e1fb5683..a1a757e39 100644 --- a/src/queries/user/subscriptions.ts +++ b/src/queries/user/subscriptions.ts @@ -9,7 +9,7 @@ import { const resolver: GQLUserResolvers['subscriptions'] = async ( { id }, { input }, - { dataSources: { articleService, draftService, userService } } + { dataSources: { atomService, userService } } ) => { if (id === null) { return connectionFromArray([], input) @@ -20,12 +20,11 @@ const resolver: GQLUserResolvers['subscriptions'] = async ( userService.countSubscription(id), userService.findSubscriptions({ userId: id, skip, take }), ]) - const articles = (await articleService.loadByIds( - actions.map(({ targetId }: { targetId: string }) => targetId) - )) as any[] return connectionFromPromisedArray( - draftService.loadByIds(articles.map(({ draftId }) => draftId)), + atomService.articleIdLoader.loadMany( + actions.map(({ targetId }: { targetId: string }) => targetId) + ), input, totalCount ) diff --git a/src/queries/user/tags/pinnedTags.ts b/src/queries/user/tags/pinnedTags.ts index 36f79bfee..656342d2f 100644 --- a/src/queries/user/tags/pinnedTags.ts +++ b/src/queries/user/tags/pinnedTags.ts @@ -9,7 +9,7 @@ import { const resolver: GQLUserResolvers['pinnedTags'] = async ( { id }, { input }, - { dataSources: { tagService } } + { dataSources: { tagService, atomService } } ) => { if (id === null) { return connectionFromArray([], input) @@ -23,8 +23,7 @@ const resolver: GQLUserResolvers['pinnedTags'] = async ( }) return connectionFromPromisedArray( - // tagService.findPinnedTagsByUserId(id), - tagService.loadByIds(tagIds.map((tag: any) => `${tag.id}`)), + atomService.tagIdLoader.loadMany(tagIds.map((tag) => tag.id)), input ) } diff --git a/src/queries/user/tags/tagsUsageRecommendation.ts b/src/queries/user/tags/tagsUsageRecommendation.ts index c09501e45..0760d6732 100644 --- a/src/queries/user/tags/tagsUsageRecommendation.ts +++ b/src/queries/user/tags/tagsUsageRecommendation.ts @@ -9,7 +9,7 @@ import { const resolver: GQLUserResolvers['tags'] = async ( { id }, { input }, - { dataSources: { tagService } } + { dataSources: { tagService, atomService } } ) => { if (id === null) { return connectionFromArray([], input) @@ -24,7 +24,7 @@ const resolver: GQLUserResolvers['tags'] = async ( }) return connectionFromPromisedArray( - tagService.loadByIds( + atomService.tagIdLoader.loadMany( items.filter((item: any) => item?.id).map((item: any) => `${item.id}`) ), input, diff --git a/src/queries/user/totalWordCount.ts b/src/queries/user/totalWordCount.ts index bb47d09a5..94716d3df 100644 --- a/src/queries/user/totalWordCount.ts +++ b/src/queries/user/totalWordCount.ts @@ -7,13 +7,18 @@ const resolver: GQLUserStatusResolvers['totalWordCount'] = async ( _, { dataSources: { - connections: { knex }, + connections: { knexRO }, }, } ) => { - const record = await knex('article') + const record = await knexRO('article_version_newest') .sum('word_count') - .where({ authorId: id, state: ARTICLE_STATE.active }) + .whereIn( + 'articleId', + knexRO('article') + .where({ authorId: id, state: ARTICLE_STATE.active }) + .select('id') + ) .first() return parseInt(record && record.sum ? (record.sum as string) : '0', 10) || 0 diff --git a/src/queries/user/transaction.ts b/src/queries/user/transaction.ts index c7752d868..fcd8d5b85 100644 --- a/src/queries/user/transaction.ts +++ b/src/queries/user/transaction.ts @@ -18,10 +18,10 @@ export const Transaction: GQLTransactionResolvers = { id: ({ id }) => toGlobalId({ type: NODE_TYPES.Transaction, id }), fee: ({ fee }) => +fee || 0, purpose: ({ purpose }) => camelCase(purpose) as GQLTransactionPurpose, - sender: (trx, _, { dataSources: { userService } }) => - trx.senderId ? userService.loadById(trx.senderId) : null, - recipient: (trx, _, { dataSources: { userService } }) => - trx.recipientId ? userService.loadById(trx.recipientId) : null, + sender: (trx, _, { dataSources: { atomService } }) => + trx.senderId ? atomService.userIdLoader.load(trx.senderId) : null, + recipient: (trx, _, { dataSources: { atomService } }) => + trx.recipientId ? atomService.userIdLoader.load(trx.recipientId) : null, blockchainTx: async (trx, _, { dataSources: { paymentService } }) => { if (trx.provider !== PAYMENT_PROVIDER.blockchain) { return null @@ -38,11 +38,7 @@ export const Transaction: GQLTransactionResolvers = { txHash: blockchainTx.txHash, } }, - target: async ( - trx, - _, - { dataSources: { articleService, atomService, paymentService } } - ) => { + target: async (trx, _, { dataSources: { atomService } }) => { if (!trx.targetId || !trx.targetType) { return null } @@ -51,7 +47,7 @@ export const Transaction: GQLTransactionResolvers = { article: 'Article', circle_price: 'Circle', transaction: 'Transaction', - } + } as const const { table } = (await atomService.findFirst({ table: 'entity_type', @@ -61,7 +57,7 @@ export const Transaction: GQLTransactionResolvers = { let target switch (table) { case 'article': { - target = await articleService.draftLoader.load(trx.targetId) + target = await atomService.articleIdLoader.load(trx.targetId) break } case 'circle_price': { @@ -76,7 +72,7 @@ export const Transaction: GQLTransactionResolvers = { break } case 'transaction': { - target = await paymentService.dataloader.load(trx.targetId) + target = await atomService.transactionIdLoader.load(trx.targetId) break } } diff --git a/src/queries/user/unreadFollowing.ts b/src/queries/user/unreadFollowing.ts index 9dab18c84..983335717 100644 --- a/src/queries/user/unreadFollowing.ts +++ b/src/queries/user/unreadFollowing.ts @@ -20,10 +20,6 @@ const resolver: GQLUserStatusResolvers['unreadFollowing'] = async ( type: LOG_RECORD_TYPES.ReadFollowingFeed, }) - if (!readFollowingFeedLog) { - return true - } - const latestActivity = await knexRO .select() .from( @@ -80,6 +76,10 @@ const resolver: GQLUserStatusResolvers['unreadFollowing'] = async ( return false } + if (!readFollowingFeedLog) { + return true + } + return readFollowingFeedLog.readAt < latestActivity.createdAt } diff --git a/src/queries/user/userActivity.ts b/src/queries/user/userActivity.ts index cc3274511..a06710078 100644 --- a/src/queries/user/userActivity.ts +++ b/src/queries/user/userActivity.ts @@ -6,11 +6,7 @@ import { import { GQLUserActivityResolvers } from 'definitions' const resolver: GQLUserActivityResolvers = { - history: async ( - { id }, - { input }, - { dataSources: { userService, draftService } } - ) => { + history: async ({ id }, { input }, { dataSources: { userService } }) => { if (!id) { return connectionFromArray([], input) } @@ -21,14 +17,8 @@ const resolver: GQLUserActivityResolvers = { userService.countReadHistory(id), userService.findReadHistory({ userId: id, skip, take }), ]) - const nodes = await Promise.all( - reads.map(async ({ article, readAt }) => { - const node = await draftService.loadById(article.draftId) - return { readAt, article: node } - }) - ) - return connectionFromArray(nodes, input, totalCount) + return connectionFromArray(reads, input, totalCount) }, recentSearches: async ( diff --git a/src/routes/__test__/transaction.test.ts b/src/routes/__test__/transaction.test.ts index e9c4fd41a..13e705f37 100644 --- a/src/routes/__test__/transaction.test.ts +++ b/src/routes/__test__/transaction.test.ts @@ -1,4 +1,4 @@ -import type { Connections } from 'definitions' +import type { Connections, Customer } from 'definitions' import type Stripe from 'stripe' import { @@ -39,10 +39,10 @@ const createPayment = async () => { id: '1', email: 'test@matters.news', } - const customer = await paymentServce.createCustomer({ + const customer = (await paymentServce.createCustomer({ user, provider: PAYMENT_PROVIDER.stripe, - }) + })) as Customer return await paymentServce.createPayment({ userId: user.id, customerId: customer.id, @@ -137,7 +137,7 @@ describe('create or update dispute', () => { ).rejects.toThrow('Related payment transaction is not succeeded') await paymentServce.markTransactionStateAs({ - id: data?.transaction.id, + id: data?.transaction.id as string, state: TRANSACTION_STATE.succeeded, }) diff --git a/src/routes/graphql.ts b/src/routes/graphql.ts index cbce86291..c74052453 100644 --- a/src/routes/graphql.ts +++ b/src/routes/graphql.ts @@ -45,6 +45,7 @@ import { OpenSeaService, PaymentService, SystemService, + RecommendationService, TagService, UserService, CollectionService, @@ -56,7 +57,6 @@ import { RevisionQueue, AssetQueue, AppreciationQueue, - IPFSQueue, MigrationQueue, PayToByBlockchainQueue, PayToByMattersQueue, @@ -102,7 +102,6 @@ const payToByBlockchainQueue = new PayToByBlockchainQueue(connections) const payToByMattersQueue = new PayToByMattersQueue(connections) const payoutQueue = new PayoutQueue(connections) const userQueue = new UserQueue(connections) -const ipfsQueue = new IPFSQueue(connections) const queues = { publicationQueue, @@ -114,7 +113,6 @@ const queues = { payToByMattersQueue, payoutQueue, userQueue, - ipfsQueue, } export const graphql = async (app: Express) => { @@ -145,6 +143,7 @@ export const graphql = async (app: Express) => { oauthService: new OAuthService(connections), paymentService: new PaymentService(connections), collectionService: new CollectionService(connections), + recommendationService: new RecommendationService(connections), openseaService: new OpenSeaService(), likecoin: new LikeCoin(connections), exchangeRate: new ExchangeRate(connections.redis), diff --git a/src/routes/oauth/strategies.ts b/src/routes/oauth/strategies.ts index ad6336451..127178a46 100644 --- a/src/routes/oauth/strategies.ts +++ b/src/routes/oauth/strategies.ts @@ -1,3 +1,5 @@ +import type { User } from 'definitions' + import { invalidateFQC } from '@matters/apollo-response-cache' import LikeCoinStrategy from '@matters/passport-likecoin' import { get } from 'lodash' @@ -102,7 +104,7 @@ export default () => { accountType: 'general', }) - const user = await userService.dataloader.load(viewer.id) + const user = (await userService.baseFindById(viewer.id)) as User // invalidate user cache await invalidateFQC({ diff --git a/src/routes/pay/likecoin.ts b/src/routes/pay/likecoin.ts index e940ad8cd..e554ee307 100644 --- a/src/routes/pay/likecoin.ts +++ b/src/routes/pay/likecoin.ts @@ -1,6 +1,9 @@ +import type { EmailableUser } from 'definitions' + import { invalidateFQC } from '@matters/apollo-response-cache' import bodyParser from 'body-parser' import { RequestHandler, Router } from 'express' +import _capitalize from 'lodash/capitalize' import NP from 'number-precision' import { @@ -32,7 +35,8 @@ const invalidateCache = async ({ }) => { if (typeId) { const result = await userService.baseFindEntityTypeTable(typeId) - const type = NODE_TYPES[(result?.table as keyof typeof NODE_TYPES) || ''] + const type = + NODE_TYPES[(_capitalize(result?.table) as keyof typeof NODE_TYPES) || ''] if (type) { await invalidateFQC({ node: { type, id }, @@ -92,7 +96,6 @@ likecoinRouter.get('/', async (req, res) => { id: tx.id, provider_tx_id: tx_hash, state: cosmosState, - updatedAt: new Date(), } // correct amount if it changed via LikePay @@ -260,18 +263,23 @@ likecoinRouter.post('/', async (req, res, next) => { } // notification - const sender = await userService.baseFindById(resultTx.senderId) + const sender = resultTx.senderId + ? await userService.baseFindById(resultTx.senderId) + : null const recipient = await userService.baseFindById(resultTx.recipientId) const article = await atomService.findFirst({ table: 'article', where: { id: resultTx.targetId }, }) - await paymentService.notifyDonation({ - tx: resultTx, - sender, - recipient, - article, - }) + + if (sender && recipient) { + await paymentService.notifyDonation({ + tx: resultTx, + sender: sender as EmailableUser, + recipient: recipient as EmailableUser, + article, + }) + } // manaully invalidate cache invalidateCache({ diff --git a/src/routes/pay/stripe/circle.ts b/src/routes/pay/stripe/circle.ts index 67d35b03a..08c732219 100644 --- a/src/routes/pay/stripe/circle.ts +++ b/src/routes/pay/stripe/circle.ts @@ -312,7 +312,7 @@ export const completeCircleInvoice = async ( const customer = (await atomService.findFirst({ table: 'customer', where: { - customer_id: invoice.customer, + customerId: invoice.customer, provider: PAYMENT_PROVIDER.stripe, archived: false, }, @@ -355,6 +355,7 @@ export const completeCircleInvoice = async ( } else { throw new ServerError(`failed to complete invoice ${providerInvoiceId}`) } + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { logger.error(err) slack.sendStripeAlert({ diff --git a/src/routes/pay/stripe/customer.ts b/src/routes/pay/stripe/customer.ts index 20ccb133d..3bc73f11b 100644 --- a/src/routes/pay/stripe/customer.ts +++ b/src/routes/pay/stripe/customer.ts @@ -67,13 +67,13 @@ export const updateCustomerCard = async ( return } - const updatedCustomer = (await atomService.update({ + const updatedCustomer = await atomService.update({ table: 'customer', where: { id: customer.id }, data: { card_last_4: cardLast4, }, - })) as Customer + }) // set as default payment method await paymentService.stripe.updateCustomer({ diff --git a/src/routes/pay/stripe/index.ts b/src/routes/pay/stripe/index.ts index 750483840..e98865ed5 100644 --- a/src/routes/pay/stripe/index.ts +++ b/src/routes/pay/stripe/index.ts @@ -53,6 +53,7 @@ stripeRouter.post('/', async (req, res) => { sig, environment.stripeWebhookSecret ) + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { logger.error(err) slack.sendStripeAlert({ @@ -116,9 +117,9 @@ stripeRouter.post('/', async (req, res) => { remark: USER_BAN_REMARK.paymentHighRisk, noticeType: OFFICIAL_NOTICE_EXTEND_TYPE.user_banned_payment, }) - const user = await userService.loadById(tx.recipientId) + const user = await userService.baseFindById(tx.recipientId) slack.sendPaymentAlert({ - message: `user ${user.userName} banned due to high risk payment`, + message: `user ${user?.userName} banned due to high risk payment`, }) } break @@ -200,9 +201,9 @@ stripeRouter.post('/', async (req, res) => { remark: USER_BAN_REMARK.payoutReversedByAdmin, noticeType: OFFICIAL_NOTICE_EXTEND_TYPE.user_banned_payment, }) - const user = await userService.loadById(payoutTx.senderId) + const user = await userService.baseFindById(payoutTx.senderId) slack.sendPaymentAlert({ - message: `user ${user.userName} banned due to payout reversed`, + message: `user ${user?.userName} banned due to payout reversed`, }) break } diff --git a/src/routes/pay/stripe/transaction.ts b/src/routes/pay/stripe/transaction.ts index 040881434..066d6eb68 100644 --- a/src/routes/pay/stripe/transaction.ts +++ b/src/routes/pay/stripe/transaction.ts @@ -1,4 +1,4 @@ -import type { Connections } from 'definitions' +import type { Connections, UserHasUsername } from 'definitions' import _ from 'lodash' import Stripe from 'stripe' @@ -11,7 +11,7 @@ import { TRANSACTION_TARGET_TYPE, } from 'common/enums' import { numRound, toDBAmount } from 'common/utils' -import { NotificationService, PaymentService, UserService } from 'connectors' +import { NotificationService, PaymentService, AtomService } from 'connectors' const mappingTxPurposeToMailType = (type: TRANSACTION_PURPOSE) => { switch (type) { @@ -38,7 +38,7 @@ export const updateTxState = async ( }, connections: Connections ) => { - const userService = new UserService(connections) + const atomService = new AtomService(connections) const paymentService = new PaymentService(connections) const notificationService = new NotificationService(connections) @@ -70,21 +70,25 @@ export const updateTxState = async ( // trigger notifications const mailType = mappingTxPurposeToMailType(tx.purpose) if (eventType === 'payment_intent.succeeded' && mailType) { - const recipient = await userService.baseFindById(tx.recipientId) - notificationService.mail.sendPayment({ - to: recipient.email, - recipient: { - displayName: recipient.displayName, - userName: recipient.userName, - }, - type: mailType, - tx: { - recipient, - amount: numRound(tx.amount), - currency: tx.currency, - }, - language: recipient.language, - }) + const recipient = (await atomService.userIdLoader.load( + tx.recipientId + )) as UserHasUsername + if (recipient.email) { + notificationService.mail.sendPayment({ + to: recipient.email, + recipient: { + displayName: recipient.displayName, + userName: recipient.userName, + }, + type: mailType, + tx: { + recipient, + amount: numRound(+tx.amount), + currency: tx.currency, + }, + language: recipient.language, + }) + } } } diff --git a/src/schema.ts b/src/schema.ts index 00493a40f..e4f7d33fc 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -36,19 +36,8 @@ const typeResolver = (type: string, result: any) => { return type } -const idResolver = (type: string, result: any) => { - // correct the article id since we return draft as article in resolver - if ( - [NODE_TYPES.Article, NODE_TYPES.Draft, NODE_TYPES.Node].includes( - type as NODE_TYPES - ) && - result?.articleId - ) { - return result.articleId - } - - return result?.id -} +// handle null object to avoid error: Cannot read properties of null (reading 'id') +const idResolver = (type: string, result: any) => result?.id // add directives diff --git a/src/scripts/updateWordCount.ts b/src/scripts/updateWordCount.ts deleted file mode 100644 index 7de4c9a74..000000000 --- a/src/scripts/updateWordCount.ts +++ /dev/null @@ -1,44 +0,0 @@ -import 'module-alias/register' - -import { countWords } from 'common/utils' -import { ArticleService, DraftService } from 'connectors' -import { connections } from 'routes/connections' - -const main = async () => { - const articleIds = process.argv.slice(2) - console.log('going to update wordCount for artcies:', articleIds) - - const draftService = new DraftService(connections) - const articleService = new ArticleService(connections) - - await Promise.all( - articleIds.map(async (articleId) => { - console.log('articleId:', articleId) - const a = await articleService.baseFindById(articleId) - console.log('got article:', a.wordCount) - const n = countWords(a.content) - if (n !== a.wordCount) { - const na = await articleService.baseUpdate(articleId, { wordCount: n }) - console.log('update article countWords to:', na) - } - - const d = await draftService.baseFindById(a.draftId) - console.log('got draft:', d) - const nd = countWords(d.content) - if (nd !== d.wordCount) { - const na = await articleService.baseUpdate(a.draftId, { wordCount: n }) - console.log('update draft countWords to:', na) - } - }) - ) - - console.info('done.') - process.exit() -} - -if (require.main === module) { - main().catch((err) => { - console.error(new Date(), 'ERROR:', err) - process.exit(1) - }) -} diff --git a/src/types/__test__/1/article.test.ts b/src/types/__test__/1/article.test.ts index df3096b96..11ab40584 100644 --- a/src/types/__test__/1/article.test.ts +++ b/src/types/__test__/1/article.test.ts @@ -2,11 +2,8 @@ import type { Connections } from 'definitions' import _get from 'lodash/get' import _omit from 'lodash/omit' -import { v4 } from 'uuid' import { - ARTICLE_LICENSE_TYPE, - ARTICLE_STATE, NODE_TYPES, PAYMENT_CURRENCY, PAYMENT_PROVIDER, @@ -24,7 +21,6 @@ import { } from 'connectors' import { - getUserContext, publishArticle, putDraft, testClient, @@ -33,32 +29,19 @@ import { closeConnections, } from '../utils' -declare global { - // eslint-disable-next-line no-var - var mockEnums: any -} - let connections: Connections beforeAll(async () => { connections = await genConnections() -}, 50000) +}, 30000) afterAll(async () => { await closeConnections(connections) }) -jest.mock('common/enums', () => { - const originalModule = jest.requireActual('common/enums') - globalThis.mockEnums = { - ...originalModule, - __esModule: true, - } - return globalThis.mockEnums -}) - const mediaHash = 'someIpfsMediaHash1' -const ARTICLE_ID = toGlobalId({ type: NODE_TYPES.Article, id: 1 }) +const ARTICLE_DB_ID = '1' +const ARTICLE_ID = toGlobalId({ type: NODE_TYPES.Article, id: ARTICLE_DB_ID }) const GET_ARTICLE = /* GraphQL */ ` query ($input: ArticleInput!) { @@ -87,6 +70,9 @@ const GET_ARTICLE = /* GraphQL */ ` } } } + dataHash + mediaHash + shortHash } } ` @@ -105,26 +91,6 @@ const GET_ARTICLES = /* GraphQL */ ` } ` -const GET_VIEWER_STATUS = /* GraphQL */ ` - query { - viewer { - id - articles(input: { first: null }) { - edges { - node { - id - } - } - } - status { - articleCount - commentCount - totalWordCount - } - } - } -` - const GET_ARTICLE_TAGS = /* GraphQL */ ` query ($input: NodeInput!) { node(input: $input) { @@ -182,43 +148,6 @@ const PUBLISH_ARTICLE = ` } ` -const EDIT_ARTICLE = /* GraphQL */ ` - mutation ($input: EditArticleInput!) { - editArticle(input: $input) { - id - summary - summaryCustomized - content - access { - circle { - id - } - } - collection(input: { first: null }) { - totalCount - edges { - node { - id - } - } - } - tags { - id - content - } - sticky - state - license - requestForDonation - replyToDonator - revisionCount - canComment - sensitiveByAuthor - sensitiveByAdmin - } - } -` - const GET_RELATED_ARTICLES = /* GraphQL */ ` query ($input: ArticleInput!) { article(input: $input) { @@ -249,11 +178,39 @@ describe('query article', () => { test('query related articles', async () => { const server = await testClient({ connections }) - const result = await server.executeOperation({ + const { errors, data } = await server.executeOperation({ query: GET_RELATED_ARTICLES, variables: { input: { mediaHash } }, }) - expect(_get(result, 'data.article.relatedArticles.edges')).toBeDefined() + expect(errors).toBeUndefined() + expect(data.article.relatedArticles.edges).toBeDefined() + }) + + test('query article by mediaHash & shortHash', async () => { + const anonymousServer = await testClient({ connections }) + + const result1 = await anonymousServer.executeOperation({ + query: GET_ARTICLE, + variables: { + input: { + mediaHash: 'someIpfsMediaHash1', + }, + }, + }) + // console.log('result1', result1) + expect(_get(result1, 'data.article.shortHash')).toBe('short-hash-1') + + const result2 = await anonymousServer.executeOperation({ + query: GET_ARTICLE, + variables: { + input: { + shortHash: 'short-hash-1', + }, + }, + }) + + // console.log('result2', result2) + expect(_get(result2, 'data.article.mediaHash')).toBe('someIpfsMediaHash1') }) }) @@ -366,10 +323,10 @@ describe('toggle article state', () => { test('subscribe an article', async () => { const server = await testClient({ isAuth: true, - connections, isAdmin: true, + connections, }) - const { data } = await server.executeOperation({ + const { errors, data } = await server.executeOperation({ query: TOGGLE_SUBSCRIBE_ARTICLE, variables: { input: { @@ -378,7 +335,17 @@ describe('toggle article state', () => { }, }, }) - expect(_get(data, 'toggleSubscribeArticle.subscribed')).toBe(true) + expect(errors).toBeUndefined() + expect(data.toggleSubscribeArticle.subscribed).toBe(true) + + const atomService = new AtomService(connections) + const action = await atomService.findFirst({ + table: 'action_article', + where: { targetId: ARTICLE_DB_ID }, + orderBy: [{ column: 'id', order: 'desc' }], + }) + expect(action.targetId).toBe(ARTICLE_DB_ID) + expect(action.articleVersionId).not.toBeNull() }) test('unsubscribe an article ', async () => { @@ -471,738 +438,6 @@ describe('frozen user do muations to article', () => { }) }) -describe('edit article', () => { - test('edit article summary', async () => { - const summary = 'my customized summary' - const server = await testClient({ - isAuth: true, - connections, - isAdmin: false, - }) - const result = await server.executeOperation({ - query: EDIT_ARTICLE, - variables: { - input: { - id: ARTICLE_ID, - summary, - }, - }, - }) - expect(_get(result, 'data.editArticle.summary')).toBe(summary) - expect(_get(result, 'data.editArticle.summaryCustomized')).toBe(true) - - // reset summary - const resetResult1 = await server.executeOperation({ - query: EDIT_ARTICLE, - variables: { - input: { - id: ARTICLE_ID, - summary: null, - }, - }, - }) - expect( - _get(resetResult1, 'data.editArticle.summary.length') - ).toBeGreaterThan(0) - expect(_get(resetResult1, 'data.editArticle.summaryCustomized')).toBe(false) - - const resetResult2 = await server.executeOperation({ - query: EDIT_ARTICLE, - variables: { - input: { - id: ARTICLE_ID, - summary: '', - }, - }, - }) - expect(_get(resetResult2, 'data.editArticle.summaryCustomized')).toBe(false) - }) - - test('edit article tags', async () => { - const tags = ['abc', '123', 'tag3', 'tag4', 'tag5'] - const server = await testClient({ - isAuth: true, - connections, - isAdmin: false, - }) - const limit = 3 - globalThis.mockEnums.MAX_TAGS_PER_ARTICLE_LIMIT = limit - // set tags out of limit - const failedRes = await server.executeOperation({ - query: EDIT_ARTICLE, - variables: { - input: { - id: ARTICLE_ID, - tags: tags.slice(0, limit + 1), - }, - }, - }) - expect(_get(failedRes, 'errors.0.message')).toBe( - `Not allow more than ${limit} tags on an article` - ) - - // set tags within limit - const result = await server.executeOperation({ - query: EDIT_ARTICLE, - variables: { - input: { - id: ARTICLE_ID, - tags: tags.slice(0, limit), - }, - }, - }) - expect(_get(result, 'data.editArticle.tags.length')).toBe(limit) - expect(_get(result, 'data.editArticle.tags.0.content')).toBe(tags[0]) - expect(_get(result, 'data.editArticle.tags.1.content')).toBe(tags[1]) - expect(_get(result, 'data.editArticle.tags.2.content')).toBe(tags[2]) - - // do not change tags when not in input - const otherRes = await server.executeOperation({ - query: EDIT_ARTICLE, - variables: { - input: { - id: ARTICLE_ID, - }, - }, - }) - expect(_get(otherRes, 'data.editArticle.tags.length')).toBe(limit) - expect(_get(otherRes, 'data.editArticle.tags.0.content')).toBe(tags[0]) - expect(_get(otherRes, 'data.editArticle.tags.1.content')).toBe(tags[1]) - expect(_get(otherRes, 'data.editArticle.tags.2.content')).toBe(tags[2]) - - // decrease tags - const decreaseRes = await server.executeOperation({ - query: EDIT_ARTICLE, - variables: { - input: { - id: ARTICLE_ID, - tags: tags.slice(0, limit - 1), - }, - }, - }) - expect(_get(decreaseRes, 'data.editArticle.tags.length')).toBe(limit - 1) - - // increase tags - const increaseRes = await server.executeOperation({ - query: EDIT_ARTICLE, - variables: { - input: { - id: ARTICLE_ID, - tags: tags.slice(0, limit), - }, - }, - }) - expect(_get(increaseRes, 'data.editArticle.tags.length')).toBe(limit) - - // out of limit tags can remain - const smallerLimit = limit - 2 - globalThis.mockEnums.MAX_TAGS_PER_ARTICLE_LIMIT = smallerLimit - - const remainRes = await server.executeOperation({ - query: EDIT_ARTICLE, - variables: { - input: { - id: ARTICLE_ID, - tags: tags.slice(0, smallerLimit + 2), - }, - }, - }) - expect(_get(remainRes, 'data.editArticle.tags.length')).toBe( - smallerLimit + 2 - ) - - // out of limit collection can not increase - - const failedRes2 = await server.executeOperation({ - query: EDIT_ARTICLE, - variables: { - input: { - id: ARTICLE_ID, - tags: tags.slice(0, smallerLimit + 3), - }, - }, - }) - expect(_get(failedRes2, 'errors.0.message')).toBe( - `Not allow more than ${smallerLimit} tags on an article` - ) - - // out of limit collection can decrease, even to a amount still out of limit - - const stillOutLimitRes = await server.executeOperation({ - query: EDIT_ARTICLE, - variables: { - input: { - id: ARTICLE_ID, - tags: tags.slice(0, smallerLimit + 1), - }, - }, - }) - expect(_get(stillOutLimitRes, 'data.editArticle.tags.length')).toBe( - smallerLimit + 1 - ) - - // reset tags - const resetResult1 = await server.executeOperation({ - query: EDIT_ARTICLE, - variables: { - input: { - id: ARTICLE_ID, - tags: [], - }, - }, - }) - expect(_get(resetResult1, 'data.editArticle.tags.length')).toBe(0) - const resetResult2 = await server.executeOperation({ - query: EDIT_ARTICLE, - variables: { - input: { - id: ARTICLE_ID, - tags: null, - }, - }, - }) - expect(_get(resetResult2, 'data.editArticle.tags.length')).toBe(0) - }) - - test('edit article collection', async () => { - const server = await testClient({ - isAuth: true, - connections, - isAdmin: false, - }) - const collection = [ - toGlobalId({ type: NODE_TYPES.Article, id: 3 }), - toGlobalId({ type: NODE_TYPES.Article, id: 4 }), - toGlobalId({ type: NODE_TYPES.Article, id: 5 }), - toGlobalId({ type: NODE_TYPES.Article, id: 6 }), - toGlobalId({ type: NODE_TYPES.Article, id: 7 }), - ] - const limit = 2 - - // set collection within limit - const res = await server.executeOperation({ - query: EDIT_ARTICLE, - variables: { - input: { - id: ARTICLE_ID, - collection: collection.slice(0, limit), - }, - }, - }) - expect(_get(res, 'data.editArticle.collection.totalCount')).toBe(limit) - expect([ - _get(res, 'data.editArticle.collection.edges.0.node.id'), - _get(res, 'data.editArticle.collection.edges.1.node.id'), - ]).toEqual(collection.slice(0, limit)) - - // set collection out of limit - globalThis.mockEnums.MAX_ARTICLES_PER_CONNECTION_LIMIT = limit - const failedRes = await server.executeOperation({ - query: EDIT_ARTICLE, - variables: { - input: { - id: ARTICLE_ID, - collection: collection.slice(0, limit + 1), - }, - }, - }) - expect(_get(failedRes, 'errors.0.message')).toBe( - `Not allow more than ${limit} articles in connection` - ) - - // do not change collection when not in input - const otherRes = await server.executeOperation({ - query: EDIT_ARTICLE, - variables: { - input: { - id: ARTICLE_ID, - }, - }, - }) - expect(_get(otherRes, 'data.editArticle.collection.totalCount')).toBe(limit) - expect([ - _get(otherRes, 'data.editArticle.collection.edges.0.node.id'), - _get(otherRes, 'data.editArticle.collection.edges.1.node.id'), - ]).toEqual(collection.slice(0, limit)) - - // reorder collection - const reorderCollection = [...collection.slice(0, limit)].reverse() - expect(reorderCollection).not.toBe(collection.slice(0, limit)) - - const reorderRes = await server.executeOperation({ - query: EDIT_ARTICLE, - variables: { - input: { - id: ARTICLE_ID, - collection: reorderCollection, - }, - }, - }) - expect(_get(reorderRes, 'data.editArticle.collection.totalCount')).toBe( - reorderCollection.length - ) - expect([ - _get(reorderRes, 'data.editArticle.collection.edges.0.node.id'), - _get(reorderRes, 'data.editArticle.collection.edges.1.node.id'), - ]).toEqual(reorderCollection) - - // decrease collection - const decreaseRes = await server.executeOperation({ - query: EDIT_ARTICLE, - variables: { - input: { - id: ARTICLE_ID, - collection: collection.slice(0, limit - 1), - }, - }, - }) - - expect(_get(decreaseRes, 'data.editArticle.collection.totalCount')).toBe( - limit - 1 - ) - expect([ - _get(decreaseRes, 'data.editArticle.collection.edges.0.node.id'), - ]).toEqual(collection.slice(0, limit - 1)) - - // reset collection - const resetResult1 = await server.executeOperation({ - query: EDIT_ARTICLE, - variables: { - input: { - id: ARTICLE_ID, - collection: [], - }, - }, - }) - expect(_get(resetResult1, 'data.editArticle.collection.totalCount')).toBe(0) - - const resetResult2 = await server.executeOperation({ - query: EDIT_ARTICLE, - variables: { - input: { - id: ARTICLE_ID, - collection: null, - }, - }, - }) - expect(_get(resetResult2, 'data.editArticle.collection.totalCount')).toBe(0) - - // out of limit collection can remain - globalThis.mockEnums.MAX_ARTICLES_PER_CONNECTION_LIMIT = 10 - - const res1 = await server.executeOperation({ - query: EDIT_ARTICLE, - variables: { - input: { - id: ARTICLE_ID, - collection: collection.slice(0, limit + 2), - }, - }, - }) - expect(_get(res1, 'data.editArticle.collection.totalCount')).toBe(limit + 2) - - globalThis.mockEnums.MAX_ARTICLES_PER_CONNECTION_LIMIT = limit - const remainRes = await server.executeOperation({ - query: EDIT_ARTICLE, - variables: { - input: { - id: ARTICLE_ID, - collection: collection.slice(0, limit + 2), - }, - }, - }) - expect(_get(remainRes, 'data.editArticle.collection.totalCount')).toBe( - limit + 2 - ) - - // out of limit collection can not increase - const failedRes2 = await server.executeOperation({ - query: EDIT_ARTICLE, - variables: { - input: { - id: ARTICLE_ID, - collection: collection.slice(0, limit + 3), - }, - }, - }) - expect(_get(failedRes2, 'errors.0.message')).toBe( - `Not allow more than ${limit} articles in connection` - ) - - // out of limit collection can decrease, even to a amount still out of limit - const stillOutLimitRes = await server.executeOperation({ - query: EDIT_ARTICLE, - variables: { - input: { - id: ARTICLE_ID, - collection: collection.slice(0, limit + 1), - }, - }, - }) - expect( - _get(stillOutLimitRes, 'data.editArticle.collection.totalCount') - ).toBe(limit + 1) - - const withinLimitRes = await server.executeOperation({ - query: EDIT_ARTICLE, - variables: { - input: { - id: ARTICLE_ID, - collection: collection.slice(0, limit - 1), - }, - }, - }) - expect(_get(withinLimitRes, 'data.editArticle.collection.totalCount')).toBe( - limit - 1 - ) - }) - - test('toggle article sticky', async () => { - const server = await testClient({ - isAuth: true, - connections, - isAdmin: false, - }) - const enableResult = await server.executeOperation({ - query: EDIT_ARTICLE, - variables: { - input: { - id: ARTICLE_ID, - sticky: true, - }, - }, - }) - expect(_get(enableResult, 'data.editArticle.sticky')).toBe(true) - - const disableResult = await server.executeOperation({ - query: EDIT_ARTICLE, - variables: { - input: { - id: ARTICLE_ID, - sticky: false, - }, - }, - }) - expect(_get(disableResult, 'data.editArticle.sticky')).toBe(false) - }) - - test('edit license', async () => { - const server = await testClient({ - isAuth: true, - connections, - isAdmin: false, - }) - const result = await server.executeOperation({ - query: EDIT_ARTICLE, - variables: { - input: { - id: ARTICLE_ID, - license: ARTICLE_LICENSE_TYPE.cc_0, - }, - }, - }) - expect(_get(result, 'data.editArticle.license')).toBe( - ARTICLE_LICENSE_TYPE.cc_0 - ) - expect(_get(result, 'data.editArticle.revisionCount')).toBe(0) - - // change license to CC2 should throw error - const changeCC2Result = await server.executeOperation({ - query: EDIT_ARTICLE, - variables: { - input: { - id: ARTICLE_ID, - license: ARTICLE_LICENSE_TYPE.cc_by_nc_nd_2, - }, - }, - }) - expect(changeCC2Result.errors?.[0].extensions.code).toBe('BAD_USER_INPUT') - - // change license to ARR should succeed - const changeResult = await server.executeOperation({ - query: EDIT_ARTICLE, - variables: { - input: { - id: ARTICLE_ID, - license: ARTICLE_LICENSE_TYPE.arr, - }, - }, - }) - expect(_get(changeResult, 'data.editArticle.license')).toBe( - ARTICLE_LICENSE_TYPE.arr - ) - expect(_get(result, 'data.editArticle.revisionCount')).toBe(0) - - // reset license - const resetResult1 = await server.executeOperation({ - query: EDIT_ARTICLE, - variables: { - input: { - id: ARTICLE_ID, - license: null, - }, - }, - }) - expect( - _get(resetResult1, 'data.editArticle.summary.length') - ).toBeGreaterThan(0) - expect(_get(resetResult1, 'data.editArticle.summaryCustomized')).toBe(false) - - // should be still 0, after whatever how many times changing license - expect(_get(result, 'data.editArticle.revisionCount')).toBe(0) - }) - - test('edit support settings', async () => { - const requestForDonation = 'test support request' - const replyToDonator = 'test support reply' - const server = await testClient({ - isAuth: true, - connections, - isAdmin: false, - }) - const result = await server.executeOperation({ - query: EDIT_ARTICLE, - variables: { - input: { - id: ARTICLE_ID, - requestForDonation, - replyToDonator, - }, - }, - }) - - expect(_get(result, 'data.editArticle.requestForDonation')).toBe( - requestForDonation - ) - expect(_get(result, 'data.editArticle.replyToDonator')).toBe(replyToDonator) - - // update one support settings field will not reset other one - const requestForDonation2 = '' - const result2 = await server.executeOperation({ - query: EDIT_ARTICLE, - variables: { - input: { - id: ARTICLE_ID, - requestForDonation: requestForDonation2, - }, - }, - }) - expect(_get(result2, 'data.editArticle.requestForDonation')).toBe( - requestForDonation2 - ) - expect(_get(result2, 'data.editArticle.replyToDonator')).toBe( - replyToDonator - ) - - // non-donators can not view replyToDonator - const anonymousServer = await testClient({ connections }) - const result3 = await anonymousServer.executeOperation({ - query: GET_ARTICLE, - variables: { - input: { - mediaHash, - }, - }, - }) - expect(_get(result3, 'data.article.requestForDonation')).toBe( - requestForDonation2 - ) - expect(_get(result3, 'data.article.replyToDonator')).toBe(null) - - const context = await getUserContext( - { email: 'test2@matters.news' }, - connections - ) - const donatorServer = await testClient({ context, connections }) - const result4 = await donatorServer.executeOperation({ - query: GET_ARTICLE, - variables: { - input: { - mediaHash, - }, - }, - }) - expect(_get(result4, 'data.article.requestForDonation')).toBe( - requestForDonation2 - ) - expect(_get(result4, 'data.article.replyToDonator')).toBe(null) - - // donators can view replyToDonator - const paymentService = new PaymentService(connections) - await paymentService.createTransaction({ - amount: 1, - state: TRANSACTION_STATE.succeeded, - purpose: TRANSACTION_PURPOSE.donation, - senderId: context.viewer.id, - targetId: '1', - targetType: TRANSACTION_TARGET_TYPE.article, - provider: PAYMENT_PROVIDER.matters, - providerTxId: Math.random().toString(), - }) - const result5 = await donatorServer.executeOperation({ - query: GET_ARTICLE, - variables: { - input: { - mediaHash, - }, - }, - }) - expect(_get(result5, 'data.article.requestForDonation')).toBe( - requestForDonation2 - ) - expect(_get(result5, 'data.article.replyToDonator')).toBe(replyToDonator) - }) - - test('edit comment settings', async () => { - const server = await testClient({ - isAuth: true, - connections, - isAdmin: false, - }) - const result = await server.executeOperation({ - query: GET_ARTICLE, - variables: { - input: { - mediaHash, - }, - }, - }) - expect(_get(result, 'data.article.canComment')).toBeTruthy() - - // can not turn off - const result2 = await server.executeOperation({ - query: EDIT_ARTICLE, - variables: { - input: { - id: ARTICLE_ID, - canComment: false, - }, - }, - }) - expect(result2.errors).not.toBeUndefined() - - // can turn on - const atomService = new AtomService(connections) - await atomService.update({ - table: 'draft', - where: { id: 1 }, - data: { canComment: false }, - }) - const result3 = await server.executeOperation({ - query: EDIT_ARTICLE, - variables: { - input: { - id: ARTICLE_ID, - canComment: true, - }, - }, - }) - expect(_get(result3, 'data.editArticle.canComment')).toBeTruthy() - }) - - test('edit sensitive settings', async () => { - const server = await testClient({ - isAuth: true, - connections, - isAdmin: false, - }) - const result = await server.executeOperation({ - query: GET_ARTICLE, - variables: { - input: { - mediaHash, - }, - }, - }) - expect(_get(result, 'data.article.sensitiveByAuthor')).toBeFalsy() - expect(_get(result, 'data.article.sensitiveByAdmin')).toBeFalsy() - - // turn on by author - const result1 = await server.executeOperation({ - query: EDIT_ARTICLE, - variables: { - input: { - id: ARTICLE_ID, - sensitive: true, - }, - }, - }) - expect(_get(result1, 'data.editArticle.sensitiveByAuthor')).toBeTruthy() - - // turn on by admin - const adminServer = await testClient({ - isAuth: true, - connections, - isAdmin: true, - }) - const UPDATE_ARTICLE_SENSITIVE = ` - mutation UpdateArticleSensitive($input: UpdateArticleSensitiveInput!) { - updateArticleSensitive(input: $input) { - id - sensitiveByAdmin - } - } - ` - const result2 = await adminServer.executeOperation({ - query: UPDATE_ARTICLE_SENSITIVE, - variables: { - input: { - id: ARTICLE_ID, - sensitive: true, - }, - }, - }) - expect( - _get(result2, 'data.updateArticleSensitive.sensitiveByAdmin') - ).toBeTruthy() - }) - - test('archive article', async () => { - const server = await testClient({ - isAuth: true, - connections, - }) - - const { data } = await server.executeOperation({ - query: GET_VIEWER_STATUS, - }) - const articleId = _get(data, 'viewer.articles.edges.0.node.id') - const articleDbId = fromGlobalId(articleId).id - - // create duplicate article with same draft - const articleService = new ArticleService(connections) - const article = await articleService.baseFindById(articleDbId) - const article2 = await articleService.baseCreate({ - ..._omit(article, ['id', 'updatedAt', 'createdAt']), - uuid: v4(), - }) - const article2Id = toGlobalId({ type: NODE_TYPES.Article, id: article2.id }) - - // archive - const { data: archivedData } = await server.executeOperation({ - query: EDIT_ARTICLE, - variables: { - input: { - id: article2Id, - state: ARTICLE_STATE.archived, - }, - }, - }) - expect(archivedData.editArticle.state).toBe(ARTICLE_STATE.archived) - - // refetch & expect de-duplicated - const { data: data2 } = await server.executeOperation({ - query: GET_VIEWER_STATUS, - }) - expect(_get(data, 'viewer.status.articleCount') - 1).toBe( - _get(data2, 'viewer.status.articleCount') - ) - expect(_get(data, 'viewer.status.totalWordCount') - article.wordCount).toBe( - _get(data2, 'viewer.status.totalWordCount') - ) - }) -}) - describe('query article readerCount/donationCount', () => { beforeAll(async () => { // insert test data @@ -1234,8 +469,8 @@ describe('query article readerCount/donationCount', () => { expect(data.article.readerCount).toBe(0) expect(data.article.donationCount).toBe(0) - const userService = new UserService(connections) - const author = await userService.loadById('1') + const atomService = new AtomService(connections) + const author = await atomService.userIdLoader.load('1') const authorServer = await testClient({ connections, context: { viewer: author }, @@ -1377,3 +612,82 @@ describe('query article donations', () => { ).toBe(3) }) }) + +describe('articles versions', () => { + const GET_ARTICLE_VERSIONS = /* GraphQL */ ` + query ( + $articleInput: ArticleInput! + $versionsInput: ArticleVersionsInput! + ) { + article(input: $articleInput) { + id + contents { + html + markdown + } + versions(input: $versionsInput) { + edges { + node { + id + description + dataHash + mediaHash + title + summary + contents { + html + markdown + } + createdAt + } + } + totalCount + } + } + } + ` + test('query article versions', async () => { + const mediaHash = 'someIpfsMediaHash2' + const anonymousServer = await testClient({ connections }) + const { errors, data } = await anonymousServer.executeOperation({ + query: GET_ARTICLE_VERSIONS, + variables: { + articleInput: { mediaHash }, + versionsInput: { first: 1 }, + }, + }) + expect(errors).toBeUndefined() + expect(fromGlobalId(data.article.versions.edges[0].node.id).type).toBe( + NODE_TYPES.ArticleVersion + ) + expect(data.article.versions.totalCount).toBe(1) + + const articleId = fromGlobalId(data.article.id).id + + const articleService = new ArticleService(connections) + const article = await articleService.baseFindById(articleId) + + const content = 'test content' + const description = 'test description' + await articleService.createNewArticleVersion( + article.id, + article.authorId, + { content }, + description + ) + + const { errors: errors2, data: data2 } = + await anonymousServer.executeOperation({ + query: GET_ARTICLE_VERSIONS, + variables: { + articleInput: { mediaHash }, + versionsInput: { first: 1 }, + }, + }) + expect(errors2).toBeUndefined() + expect(data2.article.versions.totalCount).toBe(2) + expect(data2.article.versions.edges[0].node.title).toBeDefined() + expect(data2.article.versions.edges[0].node.contents.html).toBe(content) + expect(data2.article.versions.edges[0].node.description).toBe(description) + }) +}) diff --git a/src/types/__test__/1/collection.test.ts b/src/types/__test__/1/collection.test.ts index ec7ba4024..fa431afa3 100644 --- a/src/types/__test__/1/collection.test.ts +++ b/src/types/__test__/1/collection.test.ts @@ -140,6 +140,16 @@ const GET_PINNED_WORKS = /* GraphQL */ ` } } ` +const GET_LATEST_WORKS = /* GraphQL */ ` + query { + viewer { + latestWorks { + id + title + } + } + } +` describe('get viewer collections', () => { test('not logged-in user', async () => { @@ -822,3 +832,11 @@ describe('check article if in collections', () => { expect(data2?.viewer?.collections?.edges[0].node.contains).toBe(false) }) }) + +test('get latest works', async () => { + const server = await testClient({ isAuth: true, connections }) + const { data } = await server.executeOperation({ + query: GET_LATEST_WORKS, + }) + expect(data?.viewer?.latestWorks.length).toBeLessThan(5) +}) diff --git a/src/types/__test__/1/draft.test.ts b/src/types/__test__/1/draft.test.ts index 546cf59ed..6e1a0d49b 100644 --- a/src/types/__test__/1/draft.test.ts +++ b/src/types/__test__/1/draft.test.ts @@ -2,10 +2,16 @@ import type { Connections } from 'definitions' import _get from 'lodash/get' +import { AtomService } from 'connectors' import { ARTICLE_LICENSE_TYPE, NODE_TYPES } from 'common/enums' import { toGlobalId } from 'common/utils' -import { putDraft, genConnections, closeConnections } from '../utils' +import { + testClient, + putDraft, + genConnections, + closeConnections, +} from '../utils' declare global { // eslint-disable-next-line no-var @@ -30,6 +36,36 @@ jest.mock('common/enums', () => { return globalThis.mockEnums }) +describe('query draft', () => { + const GET_DRAFT_ARTICLE = /* GraphQL */ ` + query ($input: NodeInput!) { + node(input: $input) { + ... on Draft { + id + article { + title + } + } + } + } + ` + test('get draft article', async () => { + const id = toGlobalId({ type: NODE_TYPES.Draft, id: 4 }) + const atomService = new AtomService(connections) + const author = await atomService.userIdLoader.load('1') + const server = await testClient({ + connections, + context: { viewer: author }, + }) + const { errors, data } = await server.executeOperation({ + query: GET_DRAFT_ARTICLE, + variables: { input: { id } }, + }) + expect(errors).toBeUndefined() + expect(data.node.article.title).toBeDefined() + }) +}) + describe('put draft', () => { let draftId: string @@ -219,7 +255,7 @@ describe('put draft', () => { _get(editRes, 'collection.edges.3.node.id'), ]).toEqual(collection.slice(0, limit)) - // edit draft settting collection out of limit + // edit draft setting collection out of limit const editFailedRes = await putDraft( { draft: { @@ -233,7 +269,7 @@ describe('put draft', () => { `Not allow more than ${limit} articles in collection` ) - // edit draft settting collection within limit + // edit draft setting collection within limit const editSucceedRes = await putDraft( { draft: { diff --git a/src/types/__test__/1/editArticle.test.ts b/src/types/__test__/1/editArticle.test.ts new file mode 100644 index 000000000..1c0b99e4e --- /dev/null +++ b/src/types/__test__/1/editArticle.test.ts @@ -0,0 +1,1065 @@ +import type { Connections } from 'definitions' + +import _get from 'lodash/get' + +import { AtomService, ArticleService, PaymentService } from 'connectors' +import { + ARTICLE_LICENSE_TYPE, + ARTICLE_STATE, + NODE_TYPES, + PAYMENT_PROVIDER, + TRANSACTION_PURPOSE, + TRANSACTION_STATE, + TRANSACTION_TARGET_TYPE, +} from 'common/enums' +import { toGlobalId, fromGlobalId } from 'common/utils' +import { + getUserContext, + testClient, + genConnections, + closeConnections, +} from '../utils' + +declare global { + // eslint-disable-next-line no-var + var mockEnums: any +} + +let connections: Connections +beforeAll(async () => { + connections = await genConnections() +}, 30000) + +afterAll(async () => { + await closeConnections(connections) +}) + +jest.mock('common/enums', () => { + const originalModule = jest.requireActual('common/enums') + globalThis.mockEnums = { + ...originalModule, + __esModule: true, + } + return globalThis.mockEnums +}) + +const GET_ARTICLE = /* GraphQL */ ` + query ($input: NodeInput!) { + node(input: $input) { + ... on Article { + id + title + content + contents { + html + markdown + } + donated + requestForDonation + replyToDonator + canComment + sensitiveByAuthor + sensitiveByAdmin + readerCount + donationCount + donations(input: { first: 10 }) { + totalCount + edges { + node { + id + sender { + id + } + } + } + } + dataHash + mediaHash + shortHash + revisionCount + } + } + } +` + +const GET_VIEWER_STATUS = /* GraphQL */ ` + query { + viewer { + id + articles(input: { first: null }) { + edges { + node { + id + } + } + } + status { + articleCount + commentCount + totalWordCount + } + } + } +` + +const EDIT_ARTICLE = /* GraphQL */ ` + mutation ($input: EditArticleInput!) { + editArticle(input: $input) { + id + title + summary + summaryCustomized + content + access { + circle { + id + } + } + collection(input: { first: null }) { + totalCount + edges { + node { + id + } + } + } + tags { + id + content + } + sticky + state + license + requestForDonation + replyToDonator + canComment + sensitiveByAuthor + sensitiveByAdmin + revisionCount + versions(input: { first: 1 }) { + totalCount + edges { + node { + id + ... on ArticleVersion { + description + } + } + } + } + } + } +` + +describe('edit article', () => { + const authorId = '1' + const titleOriginal = 'original title' + const contentOriginal = 'original content' + let articleId: string + let articleGlobalId: string + beforeEach(async () => { + const articleService = new ArticleService(connections) + const [{ id: _articleId }] = await articleService.createArticle({ + title: titleOriginal, + content: contentOriginal, + authorId, + }) + articleId = _articleId + articleGlobalId = toGlobalId({ type: NODE_TYPES.Article, id: articleId }) + }) + test('edit article content', async () => { + const content = 'my customized content' + const server = await testClient({ + isAuth: true, + connections, + }) + const { errors, data } = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + content, + }, + }, + }) + expect(errors).toBeUndefined() + expect(data.editArticle.content).toContain(content) + expect(data.editArticle.revisionCount).toBe(1) + + // same content will not update revision count + const { errors: errors2, data: data2 } = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + content, + }, + }, + }) + expect(errors2).toBeUndefined() + expect(data2.editArticle.content).toContain(content) + expect(data2.editArticle.revisionCount).toBe(1) + }) + test('edit article title', async () => { + const title = 'my customized title' + const server = await testClient({ + isAuth: true, + connections, + }) + const { errors, data } = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + title, + }, + }, + }) + expect(errors).toBeUndefined() + expect(data.editArticle.title).toBe(title) + expect(data.editArticle.revisionCount).toBe(1) + + // same title will not update revision count + const { data: sameData } = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + title, + }, + }, + }) + expect(sameData.editArticle.title).toBe(title) + expect(sameData.editArticle.revisionCount).toBe(1) + + // empty string is not allowed + const { errors: errorsEmptyString } = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + title: '', + }, + }, + }) + expect(errorsEmptyString).toBeDefined() + + // null same as empty string + const { errors: errorsNull } = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + title: null, + }, + }, + }) + expect(errorsNull).toBeDefined() + }) + test('edit article summary', async () => { + const summary = 'my customized summary' + const server = await testClient({ + isAuth: true, + connections, + }) + const { errors, data } = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + summary, + }, + }, + }) + expect(errors).toBeUndefined() + expect(data.editArticle.summary).toBe(summary) + expect(data.editArticle.summaryCustomized).toBe(true) + expect(data.editArticle.revisionCount).toBe(1) + + // reset summary + const { errors: errors1, data: resetData1 } = await server.executeOperation( + { + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + summary: null, + }, + }, + } + ) + expect(errors1).toBeUndefined() + expect(resetData1.editArticle.summary.length).toBeGreaterThan(0) + expect(resetData1.editArticle.summaryCustomized).toBe(false) + expect(resetData1.editArticle.revisionCount).toBe(2) + + const { data: resetData2 } = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + summary: '', + }, + }, + }) + expect(resetData2.editArticle.summaryCustomized).toBe(false) + expect(resetData2.editArticle.revisionCount).toBe(2) + }) + + test('edit article tags', async () => { + const tags = ['abc', '123', 'tag3', 'tag4', 'tag5'] + const server = await testClient({ + isAuth: true, + connections, + }) + const limit = 3 + globalThis.mockEnums.MAX_TAGS_PER_ARTICLE_LIMIT = limit + // set tags out of limit + const { errors: failedErrors } = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + tags: tags.slice(0, limit + 1), + }, + }, + }) + expect(failedErrors[0].message).toBe( + `Not allow more than ${limit} tags on an article` + ) + + // set tags within limit + const { data: succeededData } = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + tags: tags.slice(0, limit), + }, + }, + }) + expect(succeededData.editArticle.tags.length).toBe(limit) + expect(succeededData.editArticle.tags[0].content).toBe(tags[0]) + expect(succeededData.editArticle.tags[1].content).toBe(tags[1]) + expect(succeededData.editArticle.tags[2].content).toBe(tags[2]) + expect(succeededData.editArticle.revisionCount).toBe(1) + + // set same tags + const { data: unchangedData } = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + tags: tags.slice(0, limit), + }, + }, + }) + expect(unchangedData.editArticle.revisionCount).toBe( + succeededData.editArticle.revisionCount + ) + + // do not change tags when not in input + const { data: otherData } = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + }, + }, + }) + expect(otherData.editArticle.tags.length).toBe(limit) + expect(otherData.editArticle.tags[0].content).toBe(tags[0]) + expect(otherData.editArticle.tags[1].content).toBe(tags[1]) + expect(otherData.editArticle.tags[2].content).toBe(tags[2]) + expect(otherData.editArticle.revisionCount).toBe(1) + + // decrease tags + const { data: decreaseData } = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + tags: tags.slice(0, limit - 1), + }, + }, + }) + expect(decreaseData.editArticle.tags.length).toBe(limit - 1) + expect(decreaseData.editArticle.revisionCount).toBe(2) + + // increase tags + const { data: increaseData } = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + tags: tags.slice(0, limit), + }, + }, + }) + expect(increaseData.editArticle.tags.length).toBe(limit) + expect(increaseData.editArticle.revisionCount).toBe(3) + + // out of limit tags can remain + const smallerLimit = limit - 2 + globalThis.mockEnums.MAX_TAGS_PER_ARTICLE_LIMIT = smallerLimit + + // workaround revision limit for testing + const originalCheckRevisionCount = + globalThis.mockEnums.MAX_ARTICLE_REVISION_COUNT + globalThis.mockEnums.MAX_ARTICLE_REVISION_COUNT = 100 + + const { data: remainData } = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + tags: tags.slice(0, smallerLimit + 2), + }, + }, + }) + expect(remainData.editArticle.tags.length).toBe(smallerLimit + 2) + + // out of limit collection can not increase + + const failedRes2 = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + tags: tags.slice(0, smallerLimit + 3), + }, + }, + }) + expect(_get(failedRes2, 'errors.0.message')).toBe( + `Not allow more than ${smallerLimit} tags on an article` + ) + + // out of limit collection can decrease, even to a amount still out of limit + + const stillOutLimitRes = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + tags: tags.slice(0, smallerLimit + 1), + }, + }, + }) + expect(_get(stillOutLimitRes, 'data.editArticle.tags.length')).toBe( + smallerLimit + 1 + ) + + // reset tags + const resetResult1 = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + tags: [], + }, + }, + }) + expect(_get(resetResult1, 'data.editArticle.tags.length')).toBe(0) + const resetResult2 = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + tags: null, + }, + }, + }) + expect(resetResult2.data.editArticle.tags.length).toBe(0) + + globalThis.mockEnums.MAX_ARTICLE_REVISION_COUNT = originalCheckRevisionCount + }) + + test('edit article connections', async () => { + const server = await testClient({ + isAuth: true, + connections, + }) + const collection = [ + toGlobalId({ type: NODE_TYPES.Article, id: 3 }), + toGlobalId({ type: NODE_TYPES.Article, id: 4 }), + toGlobalId({ type: NODE_TYPES.Article, id: 5 }), + toGlobalId({ type: NODE_TYPES.Article, id: 6 }), + toGlobalId({ type: NODE_TYPES.Article, id: 7 }), + ] + const limit = 2 + + // set connections within limit + const { data, errors } = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + collection: collection.slice(0, limit), + }, + }, + }) + expect(errors).toBeUndefined() + expect(data.editArticle.collection.totalCount).toBe(limit) + expect([ + data.editArticle.collection.edges[0].node.id, + data.editArticle.collection.edges[1].node.id, + ]).toEqual(collection.slice(0, limit)) + expect(data.editArticle.revisionCount).toBe(1) + + // set same connections + const { data: unchangedData } = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + collection: collection.slice(0, limit), + }, + }, + }) + expect(unchangedData.editArticle.revisionCount).toBe( + data.editArticle.revisionCount + ) + + // set connections out of limit + globalThis.mockEnums.MAX_ARTICLES_PER_CONNECTION_LIMIT = limit + const failedRes = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + collection: collection.slice(0, limit + 1), + }, + }, + }) + expect(_get(failedRes, 'errors.0.message')).toBe( + `Not allow more than ${limit} articles in connection` + ) + + // do not change collection when not in input + const otherRes = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + }, + }, + }) + expect(_get(otherRes, 'data.editArticle.collection.totalCount')).toBe(limit) + expect([ + _get(otherRes, 'data.editArticle.collection.edges.0.node.id'), + _get(otherRes, 'data.editArticle.collection.edges.1.node.id'), + ]).toEqual(collection.slice(0, limit)) + + // reorder collection + const reorderCollection = [...collection.slice(0, limit)].reverse() + expect(reorderCollection).not.toBe(collection.slice(0, limit)) + + const reorderRes = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + collection: reorderCollection, + }, + }, + }) + expect(_get(reorderRes, 'data.editArticle.collection.totalCount')).toBe( + reorderCollection.length + ) + expect([ + _get(reorderRes, 'data.editArticle.collection.edges.0.node.id'), + _get(reorderRes, 'data.editArticle.collection.edges.1.node.id'), + ]).toEqual(reorderCollection) + + // decrease collection + const decreaseRes = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + collection: collection.slice(0, limit - 1), + }, + }, + }) + + expect(_get(decreaseRes, 'data.editArticle.collection.totalCount')).toBe( + limit - 1 + ) + expect([ + _get(decreaseRes, 'data.editArticle.collection.edges.0.node.id'), + ]).toEqual(collection.slice(0, limit - 1)) + + // workaround revision limit for testing + const originalCheckRevisionCount = + globalThis.mockEnums.MAX_ARTICLE_REVISION_COUNT + globalThis.mockEnums.MAX_ARTICLE_REVISION_COUNT = 100 + + // reset collection + const resetResult1 = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + collection: [], + }, + }, + }) + expect(_get(resetResult1, 'data.editArticle.collection.totalCount')).toBe(0) + + const resetResult2 = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + collection: null, + }, + }, + }) + expect(_get(resetResult2, 'data.editArticle.collection.totalCount')).toBe(0) + + // out of limit collection can remain + globalThis.mockEnums.MAX_ARTICLES_PER_CONNECTION_LIMIT = 10 + + const res1 = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + collection: collection.slice(0, limit + 2), + }, + }, + }) + expect(_get(res1, 'data.editArticle.collection.totalCount')).toBe(limit + 2) + + globalThis.mockEnums.MAX_ARTICLES_PER_CONNECTION_LIMIT = limit + const remainRes = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + collection: collection.slice(0, limit + 2), + }, + }, + }) + expect(_get(remainRes, 'data.editArticle.collection.totalCount')).toBe( + limit + 2 + ) + + // out of limit collection can not increase + const failedRes2 = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + collection: collection.slice(0, limit + 3), + }, + }, + }) + expect(_get(failedRes2, 'errors.0.message')).toBe( + `Not allow more than ${limit} articles in connection` + ) + + // out of limit collection can decrease, even to a amount still out of limit + const stillOutLimitRes = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + collection: collection.slice(0, limit + 1), + }, + }, + }) + expect( + _get(stillOutLimitRes, 'data.editArticle.collection.totalCount') + ).toBe(limit + 1) + + const withinLimitRes = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + collection: collection.slice(0, limit - 1), + }, + }, + }) + expect(_get(withinLimitRes, 'data.editArticle.collection.totalCount')).toBe( + limit - 1 + ) + + globalThis.mockEnums.MAX_ARTICLE_REVISION_COUNT = originalCheckRevisionCount + }) + + test('toggle article sticky', async () => { + const server = await testClient({ + isAuth: true, + connections, + isAdmin: false, + }) + const enableResult = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + sticky: true, + }, + }, + }) + expect(_get(enableResult, 'data.editArticle.sticky')).toBe(true) + + const disableResult = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + sticky: false, + }, + }, + }) + expect(_get(disableResult, 'data.editArticle.sticky')).toBe(false) + }) + + test('edit license', async () => { + const server = await testClient({ + isAuth: true, + connections, + }) + const { data } = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + license: ARTICLE_LICENSE_TYPE.cc_0, + }, + }, + }) + expect(data.editArticle.license).toBe(ARTICLE_LICENSE_TYPE.cc_0) + expect(data.editArticle.revisionCount).toBe(0) + + // change license to CC2 should throw error + const { errors } = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + license: ARTICLE_LICENSE_TYPE.cc_by_nc_nd_2, + }, + }, + }) + expect(errors?.[0].extensions.code).toBe('BAD_USER_INPUT') + + // change license to ARR should succeed + const { data: data2 } = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + license: ARTICLE_LICENSE_TYPE.arr, + }, + }, + }) + expect(data2.editArticle.license).toBe(ARTICLE_LICENSE_TYPE.arr) + expect(data2.editArticle.revisionCount).toBe(0) + + // reset license + const { data: data3 } = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + license: null, + }, + }, + }) + expect(data3.editArticle.summary.length).toBeGreaterThan(0) + expect(data3.editArticle.summaryCustomized).toBe(false) + + // should be still 0, after whatever how many times changing license + expect(data3.editArticle.revisionCount).toBe(0) + }) + + test('edit support settings', async () => { + const requestForDonation = 'test support request' + const replyToDonator = 'test support reply' + const server = await testClient({ + isAuth: true, + connections, + }) + const { data } = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + requestForDonation, + replyToDonator, + }, + }, + }) + + expect(data.editArticle.requestForDonation).toBe(requestForDonation) + expect(data.editArticle.replyToDonator).toBe(replyToDonator) + + // update one support settings field will not reset other one + const requestForDonation2 = '' + const { data: data2 } = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + requestForDonation: requestForDonation2, + }, + }, + }) + expect(data2.editArticle.requestForDonation).toBe(requestForDonation2) + expect(data2.editArticle.replyToDonator).toBe(replyToDonator) + + // non-donators can not view replyToDonator + const anonymousServer = await testClient({ connections }) + const { data: data3, errors: errors3 } = + await anonymousServer.executeOperation({ + query: GET_ARTICLE, + variables: { + input: { + id: articleGlobalId, + }, + }, + }) + expect(errors3).toBeUndefined() + expect(data3.node.requestForDonation).toBe(requestForDonation2) + expect(data3.node.replyToDonator).toBe(null) + + const context = await getUserContext( + { email: 'test2@matters.news' }, + connections + ) + const donatorServer = await testClient({ context, connections }) + const { data: data4 } = await donatorServer.executeOperation({ + query: GET_ARTICLE, + variables: { + input: { + id: articleGlobalId, + }, + }, + }) + expect(data4.node.requestForDonation).toBe(requestForDonation2) + expect(data4.node.replyToDonator).toBe(null) + expect(data4.node.donated).toBe(false) + + // donators can view replyToDonator + const paymentService = new PaymentService(connections) + await paymentService.createTransaction({ + amount: 1, + state: TRANSACTION_STATE.pending, + purpose: TRANSACTION_PURPOSE.donation, + senderId: context.viewer.id, + targetId: articleId, + targetType: TRANSACTION_TARGET_TYPE.article, + provider: PAYMENT_PROVIDER.matters, + providerTxId: Math.random().toString(), + }) + const { data: data5 } = await donatorServer.executeOperation({ + query: GET_ARTICLE, + variables: { + input: { + id: articleGlobalId, + }, + }, + }) + expect(data5.node.requestForDonation).toBe(requestForDonation2) + expect(data5.node.replyToDonator).toBe(replyToDonator) + expect(data5.node.donated).toBe(true) + }) + + test('edit comment settings', async () => { + const server = await testClient({ + isAuth: true, + connections, + }) + const { errors, data } = await server.executeOperation({ + query: GET_ARTICLE, + variables: { + input: { + id: articleGlobalId, + }, + }, + }) + expect(errors).toBeUndefined() + expect(data.node.canComment).toBeTruthy() + + // can not turn off + const { errors: errors2 } = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + canComment: false, + }, + }, + }) + expect(errors2).not.toBeUndefined() + + // can turn on + const atomService = new AtomService(connections) + await atomService.update({ + table: 'draft', + where: { id: 1 }, + data: { canComment: false }, + }) + const { data: data3 } = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + canComment: true, + }, + }, + }) + expect(data3.editArticle.canComment).toBeTruthy() + expect(data3.editArticle.revisionCount).toBe(0) + }) + + test('edit sensitive settings', async () => { + const server = await testClient({ + isAuth: true, + connections, + }) + const { data } = await server.executeOperation({ + query: GET_ARTICLE, + variables: { + input: { + id: articleGlobalId, + }, + }, + }) + expect(data.node.sensitiveByAuthor).toBeFalsy() + expect(data.node.sensitiveByAdmin).toBeFalsy() + expect(data.node.revisionCount).toBe(0) + + // turn on by author + const { data: data2 } = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + sensitive: true, + }, + }, + }) + expect(data2.editArticle.sensitiveByAuthor).toBeTruthy() + expect(data2.editArticle.revisionCount).toBe(0) + + // turn on by admin + const adminServer = await testClient({ + isAuth: true, + isAdmin: true, + connections, + }) + const UPDATE_ARTICLE_SENSITIVE = ` + mutation UpdateArticleSensitive($input: UpdateArticleSensitiveInput!) { + updateArticleSensitive(input: $input) { + id + sensitiveByAdmin + } + } + ` + const { data: data3 } = await adminServer.executeOperation({ + query: UPDATE_ARTICLE_SENSITIVE, + variables: { + input: { + id: articleGlobalId, + sensitive: true, + }, + }, + }) + expect(data3.updateArticleSensitive.sensitiveByAdmin).toBeTruthy() + const { data: data4 } = await server.executeOperation({ + query: GET_ARTICLE, + variables: { + input: { + id: articleGlobalId, + }, + }, + }) + expect(data4.node.sensitiveByAuthor).toBeTruthy() + expect(data4.node.sensitiveByAdmin).toBeTruthy() + expect(data4.node.revisionCount).toBe(0) + expect(data4.node.revisionCount).toBe(0) + }) + + test('archive article', async () => { + const server = await testClient({ + isAuth: true, + connections, + }) + + const { data } = await server.executeOperation({ + query: GET_VIEWER_STATUS, + }) + + const articleId = data.viewer.articles.edges[0].node.id + const articleDbId = fromGlobalId(articleId).id + + // create duplicate article with same content + const articleService = new ArticleService(connections) + const article = await articleService.baseFindById(articleDbId) + const articleVersion = await articleService.loadLatestArticleVersion( + article.id + ) + const articleContent = await articleService.loadLatestArticleContent( + article.id + ) + const [article2, articleVersion2] = await articleService.createArticle({ + title: articleVersion.title, + content: articleContent, + authorId: article.authorId, + }) + const article2Id = toGlobalId({ type: NODE_TYPES.Article, id: article2.id }) + + const { data: beforeData } = await server.executeOperation({ + query: GET_VIEWER_STATUS, + }) + + // archive + const { data: archivedData } = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: article2Id, + state: ARTICLE_STATE.archived, + }, + }, + }) + expect(archivedData.editArticle.state).toBe(ARTICLE_STATE.archived) + + // refetch & expect de-duplicated + const { data: afterData } = await server.executeOperation({ + query: GET_VIEWER_STATUS, + }) + expect(beforeData.viewer.status.articleCount - 1).toBe( + afterData.viewer.status.articleCount + ) + expect( + beforeData.viewer.status.totalWordCount - articleVersion2.wordCount + ).toBe(afterData.viewer.status.totalWordCount) + }) + test('add description', async () => { + const description = 'some description' + const server = await testClient({ + isAuth: true, + connections, + }) + const { errors, data } = await server.executeOperation({ + query: EDIT_ARTICLE, + variables: { + input: { + id: articleGlobalId, + content: 'new content', + description, + }, + }, + }) + expect(errors).toBeUndefined() + expect(data.editArticle.versions.edges[0].node.description).toBe( + description + ) + }) +}) diff --git a/src/types/__test__/1/tag.test.ts b/src/types/__test__/1/tag.test.ts index b0ab56641..bca5e08f2 100644 --- a/src/types/__test__/1/tag.test.ts +++ b/src/types/__test__/1/tag.test.ts @@ -31,7 +31,7 @@ let connections: Connections beforeAll(async () => { connections = await genConnections() globalThis.connections = connections -}, 30000) +}, 50000) afterAll(async () => { await closeConnections(connections) diff --git a/src/types/__test__/1/translation.test.ts b/src/types/__test__/1/translation.test.ts new file mode 100644 index 000000000..99557b21e --- /dev/null +++ b/src/types/__test__/1/translation.test.ts @@ -0,0 +1,165 @@ +import type { Connections } from 'definitions' + +import { NODE_TYPES } from 'common/enums' +import { toGlobalId } from 'common/utils' +import { AtomService, ArticleService } from 'connectors' + +import { testClient, genConnections, closeConnections } from '../utils' + +let connections: Connections +beforeAll(async () => { + connections = await genConnections() +}, 30000) + +afterAll(async () => { + await closeConnections(connections) +}) + +const MOCKED_TRANSLATION = 'translated text' + +jest.mock('@google-cloud/translate', () => { + return { + v3: { + TranslationServiceClient: function () { + this.translateText = jest.fn().mockResolvedValue([ + { + translations: [{ translatedText: 'translated text' }], + }, + ]) + }, + } as any, + __esModule: true, + } +}) + +describe('article translations', () => { + const GET_ARTICLE_TRANSLATION = /* GraphQL */ ` + query ($nodeInput: NodeInput!, $translationInput: TranslationArgs!) { + node(input: $nodeInput) { + id + ... on Article { + translation(input: $translationInput) { + title + content + } + } + } + } + ` + test('query article translations', async () => { + const id = toGlobalId({ type: NODE_TYPES.Article, id: 1 }) + const server = await testClient({ connections }) + const { error, data } = await server.executeOperation({ + query: GET_ARTICLE_TRANSLATION, + variables: { + nodeInput: { id }, + translationInput: { language: 'en' }, + }, + }) + expect(error).toBeUndefined() + expect(data.node.translation.title).toBe(MOCKED_TRANSLATION) + expect(data.node.translation.content).toBe(MOCKED_TRANSLATION) + const atomService = new AtomService(connections) + const articleTranslation = await atomService.findMany({ + table: 'article_translation', + }) + articleTranslation.forEach((translation) => { + expect(translation.title).toBe(MOCKED_TRANSLATION) + expect(translation.content).toBe(MOCKED_TRANSLATION) + expect(translation.articleVersionId).not.toBeNull() + }) + }) +}) + +describe('article version translations', () => { + const GET_ARTICLE_TRANSLATION = /* GraphQL */ ` + query ($nodeInput: NodeInput!, $translationInput: TranslationArgs!) { + node(input: $nodeInput) { + id + ... on ArticleVersion { + translation(input: $translationInput) { + title + content + } + } + } + } + ` + test('query translations', async () => { + const id = toGlobalId({ type: NODE_TYPES.ArticleVersion, id: '1' }) + const server = await testClient({ connections }) + const { error, data } = await server.executeOperation({ + query: GET_ARTICLE_TRANSLATION, + variables: { + nodeInput: { id }, + translationInput: { language: 'en' }, + }, + }) + expect(error).toBeUndefined() + expect(data.node.translation.title).toBe(MOCKED_TRANSLATION) + expect(data.node.translation.content).toBe(MOCKED_TRANSLATION) + + const atomService = new AtomService(connections) + const { articleId } = await atomService.findUnique({ + table: 'article_version', + where: { id: '1' }, + }) + const article = await atomService.findUnique({ + table: 'article', + where: { id: articleId }, + }) + const articleService = new ArticleService(connections) + const newArticleVersion = await articleService.createNewArticleVersion( + articleId, + article.authorId, + { title: 'new title' } + ) + + const server2 = await testClient({ connections }) + const id2 = toGlobalId({ + type: NODE_TYPES.ArticleVersion, + id: newArticleVersion.id, + }) + const { error: error2, data: data2 } = await server2.executeOperation({ + query: GET_ARTICLE_TRANSLATION, + variables: { + nodeInput: { id: id2 }, + translationInput: { language: 'en' }, + }, + }) + expect(error2).toBeUndefined() + expect(data2.node.translation.title).toBe(MOCKED_TRANSLATION) + expect(data2.node.translation.content).toBe(MOCKED_TRANSLATION) + }) + test('query paywall article_version translations by unauthorized readers return empty string ', async () => { + const articleId = '1' + const id = toGlobalId({ type: NODE_TYPES.ArticleVersion, id: articleId }) + const server = await testClient({ connections }) + + const atomService = new AtomService(connections) + const circle = await atomService.create({ + table: 'circle', + data: { + name: 'test', + owner: '1', + displayName: 'test', + providerProductId: '1', + }, + }) + await atomService.create({ + table: 'article_circle', + data: { articleId, circleId: circle.id }, + }) + + const { error, data } = await server.executeOperation({ + query: GET_ARTICLE_TRANSLATION, + variables: { + nodeInput: { id }, + translationInput: { language: 'en' }, + }, + }) + expect(error).toBeUndefined() + expect(data.node.translation.title).toBe(MOCKED_TRANSLATION) + expect(data.node.translation.content).toBe('') + }) +}) diff --git a/src/types/__test__/2/circle.test.ts b/src/types/__test__/2/circle.test.ts index 0265d880b..a351940e7 100644 --- a/src/types/__test__/2/circle.test.ts +++ b/src/types/__test__/2/circle.test.ts @@ -711,13 +711,11 @@ describe('circle CRUD', () => { license: ARTICLE_LICENSE_TYPE.cc_0, } - const { data: data2, errors } = await server.executeOperation({ + const { data: data2 } = await server.executeOperation({ query: PUT_CIRCLE_ARTICLES, variables: { input: publicInput }, }) - console.log(errors) - expect(data2.putCircleArticles.works.totalCount).toBe(1) expect(data2.putCircleArticles.works.edges[0].node.access.type).toBe( ARTICLE_ACCESS_TYPE.public diff --git a/src/types/__test__/2/comment.test.ts b/src/types/__test__/2/comment.test.ts index 9e108e609..4b76cec8b 100644 --- a/src/types/__test__/2/comment.test.ts +++ b/src/types/__test__/2/comment.test.ts @@ -1,9 +1,11 @@ +import { v4 as uuidv4 } from 'uuid' import type { Connections } from 'definitions' import _get from 'lodash/get' import { NODE_TYPES } from 'common/enums' -import { toGlobalId } from 'common/utils' +import { AtomService } from 'connectors' +import { fromGlobalId, toGlobalId } from 'common/utils' import { testClient, genConnections, closeConnections } from '../utils' @@ -42,6 +44,14 @@ const GET_ARTILCE_COMMENTS = /* GraphQL */ ` id } } + cursor + } + totalCount + pageInfo { + startCursor + endCursor + hasPreviousPage + hasNextPage } } } @@ -89,6 +99,7 @@ const GET_COMMENT = /* GraphQL */ ` const PUT_COMMENT = /* GraphQL */ ` mutation ($input: PutCommentInput!) { putComment(input: $input) { + id replyTo { id } @@ -120,14 +131,15 @@ describe('query comment list on article', () => { test('query comments by author', async () => { const authorId = toGlobalId({ type: NODE_TYPES.User, id: 2 }) const server = await testClient({ connections }) - const result = await server.executeOperation({ + const { errors, data } = await server.executeOperation({ query: GET_ARTILCE_COMMENTS, variables: { nodeInput: { id: ARTICLE_ID }, commentsInput: { filter: { author: authorId } }, }, }) - const comments = result!.data!.node.comments.edges + expect(errors).toBeUndefined() + const comments = data!.node.comments.edges for (const comment of comments) { expect(comment.node.author.id).toBe(authorId) } @@ -135,13 +147,14 @@ describe('query comment list on article', () => { test('sort comments by newest', async () => { const server = await testClient({ connections }) - const { data } = await server.executeOperation({ + const { errors, data } = await server.executeOperation({ query: GET_ARTILCE_COMMENTS, variables: { nodeInput: { id: ARTICLE_ID }, commentsInput: { sort: 'newest' }, }, }) + expect(errors).toBeUndefined() const comments = _get(data, 'node.comments.edges') const commentTimestamps = comments.map( @@ -150,6 +163,66 @@ describe('query comment list on article', () => { ) expect(isDesc(commentTimestamps)).toBe(true) }) + + test('pagination', async () => { + const atomService = new AtomService(connections) + const { id: targetTypeId } = await atomService.findFirst({ + table: 'entity_type', + where: { table: 'article' }, + }) + await atomService.create({ + table: 'comment', + data: { + type: 'article', + targetId: '1', + targetTypeId, + parentCommentId: null, + state: 'active', + uuid: uuidv4(), + authorId: '1', + }, + }) + await atomService.create({ + table: 'comment', + data: { + type: 'article', + targetId: '1', + targetTypeId, + parentCommentId: null, + state: 'collapsed', + uuid: uuidv4(), + authorId: '1', + }, + }) + const server = await testClient({ connections }) + const { errors, data } = await server.executeOperation({ + query: GET_ARTILCE_COMMENTS, + variables: { + nodeInput: { id: ARTICLE_ID }, + commentsInput: { first: 1 }, + }, + }) + expect(errors).toBeUndefined() + expect(data.node.comments.edges.length).toBe(1) + expect(data.node.comments.pageInfo.hasPreviousPage).toBe(false) + expect(data.node.comments.pageInfo.hasNextPage).toBe(true) + expect(data.node.comments.totalCount).toBeGreaterThan(1) + + const { errors: errors2, data: data2 } = await server.executeOperation({ + query: GET_ARTILCE_COMMENTS, + variables: { + nodeInput: { id: ARTICLE_ID }, + commentsInput: { + first: 1, + after: data.node.comments.pageInfo.endCursor, + }, + }, + }) + expect(errors2).toBeUndefined() + expect(data2.node.comments.pageInfo.hasPreviousPage).toBe(true) + expect(data2.node.comments.pageInfo.hasNextPage).toBe(true) + expect(data.node.comments.totalCount).toBeGreaterThan(1) + }) }) describe('mutations on comment', () => { @@ -176,7 +249,7 @@ describe('mutations on comment', () => { test('create a article comment', async () => { const server = await testClient({ isAuth: true, connections }) - const result = await server.executeOperation({ + const { errors, data } = await server.executeOperation({ query: PUT_COMMENT, variables: { input: { @@ -190,8 +263,15 @@ describe('mutations on comment', () => { }, }, }) - - expect(_get(result, 'data.putComment.replyTo.id')).toBe(COMMENT_ID) + expect(errors).toBeUndefined() + expect(data.putComment.replyTo.id).toBe(COMMENT_ID) + const id = fromGlobalId(data.putComment.id).id + const atomService = new AtomService(connections) + const comment = await atomService.findUnique({ + table: 'comment', + where: { id }, + }) + expect(comment.articleVersionId).not.toBeNull() }) test('upvote a comment', async () => { @@ -273,3 +353,63 @@ describe('mutations on comment', () => { expect(_get(data, 'togglePinComment.pinned')).toBe(false) }) }) + +describe('query responses list on article', () => { + const GET_ARTILCE_RESPONSES = /* GraphQL */ ` + query ($nodeInput: NodeInput!, $responsesInput: ResponsesInput!) { + node(input: $nodeInput) { + ... on Article { + id + responses(input: $responsesInput) { + edges { + node { + __typename + ... on Comment { + id + content + state + } + ... on Article { + id + title + } + } + } + totalCount + pageInfo { + startCursor + endCursor + hasPreviousPage + hasNextPage + } + } + } + } + } + ` + test('query responses', async () => { + const server = await testClient({ connections }) + const { data, errors } = await server.executeOperation({ + query: GET_ARTILCE_RESPONSES, + variables: { + nodeInput: { id: ARTICLE_ID }, + responsesInput: {}, + }, + }) + expect(data).toBeDefined() + expect(errors).toBeUndefined() + }) + test('query empty responses', async () => { + const articleId = toGlobalId({ type: NODE_TYPES.Article, id: 4 }) + const server = await testClient({ connections }) + const { data, errors } = await server.executeOperation({ + query: GET_ARTILCE_RESPONSES, + variables: { + nodeInput: { id: articleId }, + responsesInput: {}, + }, + }) + expect(data).toBeDefined() + expect(errors).toBeUndefined() + }) +}) diff --git a/src/types/__test__/2/payment.test.ts b/src/types/__test__/2/payment.test.ts index 4e25048bd..a8d80d959 100644 --- a/src/types/__test__/2/payment.test.ts +++ b/src/types/__test__/2/payment.test.ts @@ -52,7 +52,7 @@ describe('donation', () => { const recipientId = toGlobalId({ type: NODE_TYPES.User, id: 2 }) const wrongRecipientId = toGlobalId({ type: NODE_TYPES.User, id: 3 }) const targetId = toGlobalId({ type: NODE_TYPES.Article, id: 2 }) - const chain = BLOCKCHAIN.Polygon + const chain = BLOCKCHAIN.Optimism const txHash = '0xd65dc6bf6dcc111237f9acfbfa6003ea4a4d88f2e071f4307d3af81ae877f7be' test('cannot donate yourself', async () => { diff --git a/src/types/__test__/2/recommendation.test.ts b/src/types/__test__/2/recommendation.test.ts new file mode 100644 index 000000000..f1ed59451 --- /dev/null +++ b/src/types/__test__/2/recommendation.test.ts @@ -0,0 +1,269 @@ +import type { Connections } from 'definitions' + +import { NODE_TYPES, MATTERS_CHOICE_TOPIC_STATE } from 'common/enums' +import { RecommendationService, AtomService } from 'connectors' +import { toGlobalId } from 'common/utils' + +import { testClient, genConnections, closeConnections } from '../utils' + +let connections: Connections +let atomService: AtomService +let recommendationService: RecommendationService + +beforeAll(async () => { + connections = await genConnections() + atomService = new AtomService(connections) + recommendationService = new RecommendationService(connections) +}, 30000) + +afterAll(async () => { + await closeConnections(connections) +}) + +describe('icymi', () => { + const GET_VIEWER_RECOMMENDATION_ICYMI = /* GraphQL */ ` + query ($input: ConnectionArgs!) { + viewer { + recommendation { + icymi(input: $input) { + totalCount + edges { + node { + ... on Article { + id + author { + id + } + slug + state + cover + summary + mediaHash + dataHash + iscnId + createdAt + revisedAt + createdAt + } + } + } + } + } + } + } + ` + test('query', async () => { + const server = await testClient({ connections }) + await atomService.create({ + table: 'matters_choice', + data: { articleId: '1' }, + }) + const { errors, data } = await server.executeOperation({ + query: GET_VIEWER_RECOMMENDATION_ICYMI, + variables: { input: { first: 10 } }, + }) + expect(errors).toBeUndefined() + expect(data.viewer.recommendation.icymi.totalCount).toBeGreaterThan(0) + }) +}) + +describe('icymi topic', () => { + describe('oss', () => { + const PUT_ICYMI_TOPIC = /* GraphQL */ ` + mutation ($input: PutIcymiTopicInput!) { + putIcymiTopic(input: $input) { + id + title + articles { + id + } + note + pinAmount + state + publishedAt + archivedAt + } + } + ` + const GET_OSS_ICYMI_TOPIC = /* GraphQL */ ` + query ($input: NodeInput!) { + node(input: $input) { + id + ... on IcymiTopic { + title + articles { + id + } + note + state + publishedAt + archivedAt + } + } + } + ` + const GET_OSS_ICYMI_TOPICS = /* GraphQL */ ` + query ($input: ConnectionArgs!) { + oss { + icymiTopics(input: $input) { + totalCount + edges { + node { + id + ... on IcymiTopic { + title + articles { + id + } + note + state + publishedAt + archivedAt + } + } + } + } + } + } + ` + const title = 'test title' + const pinAmount = 3 + const articles = ['1', '2', '3'].map((id) => + toGlobalId({ type: NODE_TYPES.Article, id }) + ) + const note = 'test note' + test('only admin can mutate icymit topic', async () => { + const server = await testClient({ connections }) + const { errors: errorsVisitor } = await server.executeOperation({ + query: PUT_ICYMI_TOPIC, + variables: { input: { title, articles, pinAmount, note } }, + }) + expect(errorsVisitor).toBeDefined() + + const authedServer = await testClient({ connections, isAuth: true }) + const { errors: errorsAuthed } = await authedServer.executeOperation({ + query: PUT_ICYMI_TOPIC, + variables: { input: { title, articles, pinAmount, note } }, + }) + expect(errorsAuthed).toBeDefined() + + const adminServer = await testClient({ + connections, + isAuth: true, + isAdmin: true, + }) + const { errors, data } = await adminServer.executeOperation({ + query: PUT_ICYMI_TOPIC, + variables: { input: { title, articles, pinAmount, note } }, + }) + expect(errors).toBeUndefined() + expect(data.putIcymiTopic.state).toBe(MATTERS_CHOICE_TOPIC_STATE.editing) + expect(data.putIcymiTopic.pinAmount).toBe(3) + expect(data.putIcymiTopic.articles.length).toBe(3) + expect(data.putIcymiTopic.publishedAt).toBeNull() + expect(data.putIcymiTopic.archivedAt).toBeNull() + + // only update fields provided + const { data: data2, errors: errors2 } = + await adminServer.executeOperation({ + query: PUT_ICYMI_TOPIC, + variables: { input: { id: data.putIcymiTopic.id, pinAmount: 6 } }, + }) + expect(errors2).toBeUndefined() + expect(data2.putIcymiTopic.pinAmount).toBe(6) + expect(data2.putIcymiTopic.state).toBe(MATTERS_CHOICE_TOPIC_STATE.editing) + expect(data2.putIcymiTopic.articles.length).toBe(3) + expect(data2.putIcymiTopic.publishedAt).toBeNull() + expect(data2.putIcymiTopic.archivedAt).toBeNull() + }) + test('only admin can views icymit topics list', async () => { + const server = await testClient({ connections }) + const { data: dataVisitor } = await server.executeOperation({ + query: GET_OSS_ICYMI_TOPICS, + variables: { input: { first: 10 } }, + }) + expect(dataVisitor).toBeNull() + + const authedServer = await testClient({ connections, isAuth: true }) + const { data: dataAuthed } = await authedServer.executeOperation({ + query: GET_OSS_ICYMI_TOPICS, + variables: { input: { first: 10 } }, + }) + expect(dataAuthed).toBe(null) + + const adminServer = await testClient({ + connections, + isAuth: true, + isAdmin: true, + }) + const { errors, data } = await adminServer.executeOperation({ + query: GET_OSS_ICYMI_TOPICS, + variables: { input: { first: 10 } }, + }) + expect(errors).toBeUndefined() + expect(data.oss.icymiTopics.totalCount).toBeGreaterThan(0) + }) + test('query icymi topic', async () => { + const server = await testClient({ connections }) + const { data } = await server.executeOperation({ + query: GET_OSS_ICYMI_TOPIC, + variables: { + input: { id: toGlobalId({ type: NODE_TYPES.IcymiTopic, id: 1 }) }, + }, + }) + expect(data).toBeDefined() + }) + }) + + const GET_VIEWER_RECOMMENDATION_ICYMI_TOPIC = /* GraphQL */ ` + query { + viewer { + recommendation { + icymiTopic { + id + title + articles { + id + } + note + state + publishedAt + archivedAt + } + } + } + } + ` + test('query null', async () => { + const server = await testClient({ connections }) + await atomService.updateMany({ + table: 'matters_choice_topic', + where: { state: MATTERS_CHOICE_TOPIC_STATE.published }, + data: { state: MATTERS_CHOICE_TOPIC_STATE.archived }, + }) + const { errors, data } = await server.executeOperation({ + query: GET_VIEWER_RECOMMENDATION_ICYMI_TOPIC, + }) + expect(errors).toBeUndefined() + expect(data.viewer.recommendation.icymiTopic).toBeNull() + }) + test('query', async () => { + const title = 'test title 2' + const articleIds = ['1', '2', '3'] + const topic = await recommendationService.createIcymiTopic({ + title, + articleIds, + pinAmount: 3, + }) + await recommendationService.publishIcymiTopic(topic.id) + const server = await testClient({ connections }) + const { errors, data } = await server.executeOperation({ + query: GET_VIEWER_RECOMMENDATION_ICYMI_TOPIC, + }) + expect(errors).toBeUndefined() + expect(data.viewer.recommendation.icymiTopic.title).toBe(title) + expect(data.viewer.recommendation.icymiTopic.articles.length).toBe( + articleIds.length + ) + }) +}) diff --git a/src/types/__test__/2/system.test.ts b/src/types/__test__/2/system.test.ts index c3a04e543..5d49e48d6 100644 --- a/src/types/__test__/2/system.test.ts +++ b/src/types/__test__/2/system.test.ts @@ -49,11 +49,11 @@ beforeAll(async () => { }, connections ) -}, 30000) +}, 50000) afterAll(async () => { await closeConnections(connections) -}) +}, 50000) const GET_USER = /* GraphQL */ ` query ($input: NodeInput!) { @@ -98,6 +98,19 @@ const GET_ARTICLE = /* GraphQL */ ` } } ` +const GET_ARTICLE_VERSION = /* GraphQL */ ` + query ($input: NodeInput!) { + node(input: $input) { + ... on ArticleVersion { + id + contents { + html + markdown + } + } + } + } +` const GET_COMMENT = /* GraphQL */ ` query ($input: NodeInput!) { node(input: $input) { @@ -287,7 +300,19 @@ describe('query nodes of different type', () => { variables: { input: { id } }, }) const node = data && data.node - expect(node).toEqual({ id, title: 'test draft 1' }) + expect(node).toEqual({ id, title: 'test article 1' }) + }) + + test('query article version node', async () => { + const id = toGlobalId({ type: NODE_TYPES.ArticleVersion, id: 1 }) + const server = await testClient({ connections }) + const { errors, data } = await server.executeOperation({ + query: GET_ARTICLE_VERSION, + variables: { input: { id } }, + }) + expect(errors).toBeUndefined() + expect(data.node.id).toBe(id) + expect(data.node.contents.html).toBeDefined() }) test('query comment node', async () => { @@ -300,6 +325,7 @@ describe('query nodes of different type', () => { const node = data && data.node expect(node.id).toBe(id) }) + test('query nodes', async () => { const userId = toGlobalId({ type: NODE_TYPES.User, id: 1 }) const commentId = toGlobalId({ type: NODE_TYPES.Comment, id: 1 }) @@ -841,3 +867,77 @@ describe('manage user restrictions', () => { ).toEqual(['articleHottest']) }) }) + +describe('submitReport', () => { + const SUBMIT_REPORT = /* GraphQL */ ` + mutation ($input: SubmitReportInput!) { + submitReport(input: $input) { + id + reporter { + id + } + target { + ... on Article { + id + state + } + } + } + } + ` + const GET_REPORTS = /* GraphQL */ ` + query ($input: ConnectionArgs!) { + oss { + reports(input: $input) { + totalCount + edges { + node { + id + reporter { + id + } + target { + ... on Article { + id + } + } + } + } + } + } + } + ` + + test('submit report successfully', async () => { + const server = await testClient({ + isAuth: true, + isAdmin: true, + connections, + }) + const { data } = await server.executeOperation({ + query: SUBMIT_REPORT, + variables: { + input: { + targetId: toGlobalId({ type: NODE_TYPES.Article, id: 1 }), + reason: 'other', + }, + }, + }) + expect(data.submitReport.id).toBeDefined() + expect(data.submitReport.reporter.id).toBeDefined() + expect(data.submitReport.target.id).toBeDefined() + + // query reports + const { data: data2 } = await server.executeOperation({ + query: GET_REPORTS, + variables: { + input: { + first: null, + }, + }, + }) + expect(data2.oss.reports.totalCount).toBe(1) + expect(data2.oss.reports.edges[0].node.reporter.id).toBeDefined() + expect(data2.oss.reports.edges[0].node.target.id).toBeDefined() + }) +}) diff --git a/src/types/__test__/2/user.test.ts b/src/types/__test__/2/user.test.ts index 6dc1aadf8..b93b29bfa 100644 --- a/src/types/__test__/2/user.test.ts +++ b/src/types/__test__/2/user.test.ts @@ -15,8 +15,8 @@ import { } from 'common/enums' import { fromGlobalId, toGlobalId } from 'common/utils' import { refreshView, UserService, PaymentService } from 'connectors' +import { createDonationTx, createTx } from 'connectors/__test__/utils' -import { createDonationTx, createTx } from '../../../connectors/__test__/utils' import { defaultTestUser, getUserContext, @@ -286,6 +286,16 @@ query($input: ConnectionArgs!) { author { id } + slug + state + cover + summary + mediaHash + dataHash + iscnId + createdAt + revisedAt + createdAt } } } @@ -339,37 +349,6 @@ const GET_VIEWER_BADGES = /* GraphQL */ ` } ` -const GET_VIEWER_TOPICS = /* GraphQL */ ` - query { - viewer { - topics(input: { first: 10 }) { - totalCount - edges { - node { - id - cover - author { - id - } - chapterCount - articleCount - chapters { - id - articles { - id - } - } - articles { - id - } - public - } - } - } - } - } -` - const GET_VIEWER_WALLET_TRANSACTIONS = /* GraphQL */ ` query ($input: TransactionsArgs!) { viewer { @@ -388,45 +367,6 @@ const GET_VIEWER_WALLET_TRANSACTIONS = /* GraphQL */ ` } } ` - -const PUT_TOPIC = /* GraphQL */ ` - mutation PutTopic($input: PutTopicInput!) { - putTopic(input: $input) { - id - title - articles { - id - } - } - } -` - -const PUT_CHAPTER = /* GraphQL */ ` - mutation PutChapter($input: PutChapterInput!) { - putChapter(input: $input) { - id - title - articles { - id - } - } - } -` - -const DELETE_TOPICS = /* GraphQL */ ` - mutation DeleteTopics($input: DeleteTopicsInput!) { - deleteTopics(input: $input) - } -` - -const SORT_TOPICS = /* GraphQL */ ` - mutation SortTopics($input: SortTopicsInput!) { - sortTopics(input: $input) { - id - } - } -` - const SEND_VERIFICATION_CODE = /* GraphQL */ ` mutation SendVerificationCode($input: SendVerificationCodeInput!) { sendVerificationCode(input: $input) @@ -1056,11 +996,11 @@ describe('user recommendations', () => { connections, }) - const result = await serverNew.executeOperation({ + const { data, errors } = await serverNew.executeOperation({ query: GET_VIEWER_RECOMMENDATION(list), variables: { input: { first: 1 } }, }) - const { data } = result + expect(errors).toBeUndefined() const article = _get(data, `viewer.recommendation.${list}.edges.0.node`) expect(fromGlobalId(article.id).type).toBe('Article') const count = _get(data, `viewer.recommendation.${list}.totalCount`) @@ -1321,150 +1261,6 @@ describe('verification code', () => { }) }) -const TOPIC_ID_1 = toGlobalId({ type: NODE_TYPES.Topic, id: 1 }) -const TOPIC_ID_2 = toGlobalId({ type: NODE_TYPES.Topic, id: 2 }) - -describe('topics & chapters', () => { - test('get user topics', async () => { - const server = await testClient({ - isAuth: true, - connections, - }) - const { data } = await server.executeOperation({ - query: GET_VIEWER_TOPICS, - variables: {}, - }) - - expect(_get(data, 'viewer.topics.totalCount')).toBeGreaterThan(0) - - const firstTopic = _get(data, 'viewer.topics.edges.0.node') - - expect(_get(firstTopic, 'id')).toBeDefined() - expect(_get(firstTopic, 'chapterCount')).toBeGreaterThan(0) - expect(_get(firstTopic, 'articleCount')).toBeGreaterThan(0) - expect(_get(firstTopic, 'chapters.0.id')).toBeDefined() - expect(_get(firstTopic, 'chapters.0.articles.0.id')).toBeDefined() - expect(_get(firstTopic, 'articles.0.id')).toBeDefined() - }) - - test('create topic', async () => { - const server = await testClient({ - isAuth: true, - connections, - }) - - // create - const title = 'topic 123' - const { data: created } = await server.executeOperation({ - query: PUT_TOPIC, - variables: { input: { title } }, - }) - - expect(_get(created, 'putTopic.title')).toBe(title) - }) - - test('update topic', async () => { - const server = await testClient({ - isAuth: true, - connections, - }) - - const title = 'topic 345' - const articles = ['QXJ0aWNsZTox', 'QXJ0aWNsZTo0'] - const { data: updated } = await server.executeOperation({ - query: PUT_TOPIC, - variables: { - input: { - id: TOPIC_ID_1, - title, - articles, - }, - }, - }) - - expect(_get(updated, 'putTopic.title')).toEqual(title) - expect( - (_get(updated, 'putTopic.articles') as [{ id: string }]).map( - ({ id }) => id - ) - ).toEqual(articles) - }) - - test('create chapter', async () => { - const server = await testClient({ - isAuth: true, - connections, - }) - - // create - const title = 'chapter 123' - const { data: created } = await server.executeOperation({ - query: PUT_CHAPTER, - variables: { - input: { title, topic: TOPIC_ID_1 }, - }, - }) - - expect(_get(created, 'putChapter.title')).toBe(title) - }) - - test('update chapter', async () => { - const server = await testClient({ - isAuth: true, - connections, - }) - - const title = 'chapter 345' - const articles = ['QXJ0aWNsZTox', 'QXJ0aWNsZTo0'] - const { data: updated } = await server.executeOperation({ - query: PUT_CHAPTER, - variables: { - input: { id: 'Q2hhcHRlcjox', title, articles }, - }, - }) - - expect(_get(updated, 'putChapter.title')).toEqual(title) - expect( - (_get(updated, 'putChapter.articles') as [{ id: string }]).map( - ({ id }) => id - ) - ).toEqual(articles) - }) - - test('sort topics', async () => { - const server = await testClient({ - isAuth: true, - connections, - }) - - const { data } = await server.executeOperation({ - query: SORT_TOPICS, - variables: { - input: { ids: [TOPIC_ID_2, TOPIC_ID_1] }, - }, - }) - - expect(_get(data, 'sortTopics.0.id')).toBe(TOPIC_ID_2) - expect(_get(data, 'sortTopics.1.id')).toBe(TOPIC_ID_1) - }) - - test('delete topics', async () => { - const server = await testClient({ - isAuth: true, - connections, - }) - - const { data } = await server.executeOperation({ - query: DELETE_TOPICS, - variables: { - input: { ids: [] }, - }, - }) - - expect(_get(data, 'deleteTopics')).toBe(true) - }) -}) - describe('likecoin', () => { test('reset liker id', async () => { const server = await testClient({ diff --git a/src/types/__test__/utils.ts b/src/types/__test__/utils.ts index d7567d379..d95ff89cf 100644 --- a/src/types/__test__/utils.ts +++ b/src/types/__test__/utils.ts @@ -22,13 +22,13 @@ import { TagService, UserService, CollectionService, + RecommendationService, } from 'connectors' import { PublicationQueue, RevisionQueue, AssetQueue, AppreciationQueue, - IPFSQueue, MigrationQueue, PayToByBlockchainQueue, PayToByMattersQueue, @@ -201,7 +201,6 @@ export const testClient = async ({ const payToByMattersQueue = new PayToByMattersQueue(connections) const payoutQueue = new PayoutQueue(connections) const userQueue = new UserQueue(connections) - const ipfsQueue = new IPFSQueue(connections) const queues = { publicationQueue, revisionQueue, @@ -212,7 +211,6 @@ export const testClient = async ({ payToByMattersQueue, payoutQueue, userQueue, - ipfsQueue, } const genContext = () => ({ @@ -229,6 +227,7 @@ export const testClient = async ({ oauthService: new OAuthService(connections), paymentService: new PaymentService(connections), collectionService: new CollectionService(connections), + recommendationService: new RecommendationService(connections), connections, queues, ...dataSources, diff --git a/src/types/article.ts b/src/types/article.ts index fa81cac63..7d52f8ecb 100644 --- a/src/types/article.ts +++ b/src/types/article.ts @@ -27,20 +27,6 @@ export default /* GraphQL */ ` "Read an article." readArticle(input: ReadArticleInput!): Article! - ###################### - # Article Containers # - ###################### - "Create a Topic when no id is given, update fields when id is given. Throw error if no id & no title." - putTopic(input: PutTopicInput!): Topic! @purgeCache(type: "${NODE_TYPES.Topic}") - - "Create a Chapter when no id is given, update fields when id is given. Throw error if no id & no title, or no id & no topic." - putChapter(input: PutChapterInput!): Chapter! @purgeCache(type: "${NODE_TYPES.Chapter}") - - "Delete topics" - deleteTopics(input: DeleteTopicsInput!): Boolean! @complexity(value: 10, multipliers: ["input.ids"]) - - "Sort topics" - sortTopics(input: SortTopicsInput!): [Topic!]! @complexity(value: 10, multipliers: ["input.ids"]) @purgeCache(type: "${NODE_TYPES.Topic}") ############## # Tag # @@ -133,6 +119,9 @@ export default /* GraphQL */ ` "Media hash, composed of cid encoding, of this article." mediaHash: String! + "Short hash for shorter url addressing" + shortHash: String! ## add non-null after all rows filled + "Content (HTML) of this article." content: String! @@ -227,6 +216,9 @@ export default /* GraphQL */ ` "License Type" license: ArticleLicenseType! + "whether current viewer has donated to this article" + donated: Boolean! @privateCache + "creator message asking for support" requestForDonation: String @@ -239,6 +231,9 @@ export default /* GraphQL */ ` "whether readers can comment" canComment: Boolean! + "history versions" + versions(input: ArticleVersionsInput!): ArticleVersionsConnection! @complexity(multipliers: ["input.first"], value: 1) + ############## # OSS # ############## @@ -246,63 +241,35 @@ export default /* GraphQL */ ` remark: String @auth(mode: "${AUTH_MODE.admin}") } - "This type contains metadata, content and related data of Chapter type, which is a container for Article type. A Chapter belong to a Topic." - type Chapter implements Node { - "Unique id of this chapter." - id: ID! - - "Title of this chapter." - title: String! - - "Description of this chapter." - description: String - - "Number articles included in this chapter." - articleCount: Int! + input ArticleVersionsInput { + after: String + first: Int @constraint(min: 0) + } - "Articles included in this Chapter" - articles: [Article!] + type ArticleVersionsConnection implements Connection { + totalCount: Int! + pageInfo: PageInfo! + edges: [ArticleVersionEdge]! + } - "The topic that this Chapter belongs to." - topic: Topic! @logCache(type: "${NODE_TYPES.Topic}") + type ArticleVersionEdge { + node: ArticleVersion! + cursor: String! } - "This type contains metadata, content and related data of a topic, which is a container for Article and Chapter types." - type Topic implements Node { - "Unique id of this topic." + type ArticleVersion implements Node { id: ID! - - "Title of this topic." + dataHash: String + mediaHash: String title: String! - - "Cover of this topic." - cover: String - - "Description of this topic." + summary: String! + contents: ArticleContents! + translation(input: TranslationArgs): ArticleTranslation + createdAt: DateTime! description: String - - "Number of chapters included in this topic." - chapterCount: Int! - - "Number articles included in this topic." - articleCount: Int! - - "List of chapters included in this topic." - chapters: [Chapter!] - - "List of articles included in this topic." - articles: [Article!] - - "Author of this topic." - author: User! - - "Whether this topic is public or not." - public: Boolean! - - "Latest published article on this topic" - latestArticle: Article } + "This type contains content, count and related data of an article tag." type Tag implements Node { "Unique id of this tag." @@ -414,17 +381,6 @@ export default /* GraphQL */ ` node: Article! @logCache(type: "${NODE_TYPES.Article}") } - type TopicConnection implements Connection { - totalCount: Int! - pageInfo: PageInfo! - edges: [TopicEdge!] - } - - type TopicEdge { - cursor: String! - node: Topic! @logCache(type: "${NODE_TYPES.Topic}") - } - type TagConnection implements Connection { totalCount: Int! pageInfo: PageInfo! @@ -453,7 +409,8 @@ export default /* GraphQL */ ` } input ArticleInput { - mediaHash: String! + mediaHash: String + shortHash: String } input PublishArticleInput { @@ -469,6 +426,7 @@ export default /* GraphQL */ ` "deprecated, use pinned instead" sticky: Boolean pinned: Boolean + title: String summary: String tags: [String!] content: String @@ -482,6 +440,9 @@ export default /* GraphQL */ ` requestForDonation: String @constraint(maxLength: 140) replyToDonator: String @constraint(maxLength: 140) + "revision description" + description: String @constraint(maxLength: 140) + "whether publish to ISCN" iscnPublish: Boolean @@ -500,32 +461,6 @@ export default /* GraphQL */ ` id: ID! } - input PutTopicInput { - id: ID - title: String - description: String - cover: ID - public: Boolean - articles: [ID!] - chapters: [ID!] - } - - input PutChapterInput { - id: ID - title: String - description: String - topic: ID - articles: [ID!] - } - - input DeleteTopicsInput { - ids: [ID!]! - } - - input SortTopicsInput { - ids: [ID!]! - } - input ToggleRecommendInput { id: ID! enabled: Boolean! diff --git a/src/types/system.ts b/src/types/system.ts index a2e35e4bb..a36a1c228 100644 --- a/src/types/system.ts +++ b/src/types/system.ts @@ -32,6 +32,9 @@ export default /* GraphQL */ ` "Delete blocked search keywords from search_history db" deleteBlockedSearchKeywords(input:KeywordsInput!): Boolean @auth(mode: "${AUTH_MODE.admin}") + "Submit inappropriate content report" + submitReport(input: SubmitReportInput!): Report! @auth(mode: "${AUTH_MODE.oauth}") + ############## # OSS # ############## @@ -43,6 +46,7 @@ export default /* GraphQL */ ` putAnnouncement(input: PutAnnouncementInput!): Announcement! @auth(mode: "${AUTH_MODE.admin}") deleteAnnouncements(input: DeleteAnnouncementsInput!): Boolean! @auth(mode: "${AUTH_MODE.admin}") putRestrictedUsers(input: PutRestrictedUsersInput!): [User!]! @complexity(value: 1, multipliers: ["input.ids"]) @auth(mode: "${AUTH_MODE.admin}") + putIcymiTopic(input:PutIcymiTopicInput!): IcymiTopic @auth(mode: "${AUTH_MODE.admin}") } input KeywordsInput { @@ -130,6 +134,8 @@ export default /* GraphQL */ ` seedingUsers(input: ConnectionArgs!): UserConnection! badgedUsers(input: BadgedUsersInput!): UserConnection! restrictedUsers(input: ConnectionArgs!): UserConnection! + reports(input: ConnectionArgs!): ReportConnection! + icymiTopics(input:ConnectionArgs!): IcymiTopicConnection! } @@ -208,6 +214,25 @@ export default /* GraphQL */ ` createdAt: DateTime! } + type Report implements Node { + id: ID! + reporter: User! + target: Response! + reason: ReportReason! + createdAt: DateTime! + } + + type ReportConnection implements Connection { + totalCount: Int! + pageInfo: PageInfo! + edges: [ReportEdge!] + } + + type ReportEdge { + cursor: String! + node: Report! + } + input NodeInput { id: ID! } @@ -350,6 +375,11 @@ export default /* GraphQL */ ` restrictions: [UserRestrictionType!]! } + input SubmitReportInput { + targetId: ID! + reason: ReportReason! + } + enum SearchTypes { Article User @@ -402,7 +432,6 @@ export default /* GraphQL */ ` circleCover collectionCover announcementCover - topicCover } enum EntityType { @@ -412,7 +441,6 @@ export default /* GraphQL */ ` user circle announcement - topic collection } @@ -462,6 +490,52 @@ export default /* GraphQL */ ` articleNewest } + enum ReportReason { + tort + illegal_advertising + discrimination_insult_hatred + pornography_involving_minors + other + } + + type IcymiTopic implements Node { + id: ID! + title: String! + articles: [Article!]! + pinAmount: Int! + note: String + state: IcymiTopicState! + publishedAt: DateTime + archivedAt: DateTime + } + + enum IcymiTopicState { + published + editing + archived + } + + type IcymiTopicConnection implements Connection { + totalCount: Int! + pageInfo: PageInfo! + edges: [IcymiTopicEdge!]! + } + + type IcymiTopicEdge { + cursor: String! + node: IcymiTopic! + } + + input PutIcymiTopicInput { + id: ID + title: String + articles: [ID!] + pinAmount: Int + note: String + state: IcymiTopicState + } + + #################### # Directives # #################### diff --git a/src/types/user.ts b/src/types/user.ts index 29ffce94e..7aeb3f1ff 100644 --- a/src/types/user.ts +++ b/src/types/user.ts @@ -155,12 +155,15 @@ export default /* GraphQL */ ` "Articles authored by current user." articles(input: UserArticlesInput!): ArticleConnection! @complexity(multipliers: ["input.first"], value: 1) - "Topics created by current user." - topics(input: TopicInput!): TopicConnection! @complexity(multipliers: ["input.first"], value: 1) - "collections authored by current user." collections(input: ConnectionArgs!): CollectionConnection! @complexity(multipliers: ["input.first"], value: 1) + + """user latest articles or collections""" + latestWorks: [PinnableWork!]! + + """user pinned articles or collections""" pinnedWorks: [PinnableWork!]! + "Tags by by usage order of current user." tags(input: ConnectionArgs!): TagConnection! @complexity(multipliers: ["input.first"], value: 1) @@ -232,6 +235,9 @@ export default /* GraphQL */ ` "'In case you missed it' recommendation." icymi(input: ConnectionArgs!): ArticleConnection! @complexity(multipliers: ["input.first"], value: 1) @cacheControl(maxAge: ${CACHE_TTL.PUBLIC_FEED_ARTICLE}) + "'In case you missed it' topic." + icymiTopic: IcymiTopic @cacheControl(maxAge: ${CACHE_TTL.PUBLIC_FEED_ARTICLE}) + "Global tag list, sort by activities in recent 14 days." tags(input: RecommendInput!): TagConnection! @complexity(multipliers: ["input.first"], value: 1) @cacheControl(maxAge: ${CACHE_TTL.PUBLIC_FEED_TAG}) @@ -259,12 +265,6 @@ export default /* GraphQL */ ` type: AuthorsType } - input TopicInput { - after: String - first: Int @constraint(min: 0) - filter: FilterInput - } - input FilterInput { "index of list, min: 0, max: 49" random: Int @constraint(min: 0, max: 49) @@ -272,9 +272,6 @@ export default /* GraphQL */ ` "Used in RecommendInput" followed: Boolean - "Used in User.topics" - public: Boolean - "Used in User Articles filter, by tags or by time range, or both" tagIds: [ID!] inRangeStart: DateTime