diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..37acec8 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,31 @@ +name: Test + +on: + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + services: + postgres: + image: postgres:17 + env: + POSTGRES_HOST_AUTH_METHOD: trust + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U postgres" + --health-interval 5s + --health-timeout 5s + --health-retries 10 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + # The box's version (see .nvmrc); upgrading it is separate work. + node-version-file: .nvmrc + - name: Install libpq headers (pg-native) + run: sudo apt-get update && sudo apt-get install -y libpq-dev + - run: yarn install --frozen-lockfile + - run: yarn format + - run: yarn test diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..cb406c6 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +16.20.2 diff --git a/package.json b/package.json index f056035..d2503c2 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,8 @@ "main": "src/app.js", "scripts": { "start": "standard ./src/ && node src/app.js", - "format": "standard ./src/", - "test": "echo \"Error: no test specified\" && exit 1" + "format": "standard --env mocha ./src/ ./test/", + "test": "mocha --exit test/*.test.js" }, "author": "Julien Lucca", "license": "MIT", @@ -19,6 +19,7 @@ "pg-native": "^3.0.0" }, "devDependencies": { + "mocha": "^10.8.2", "standard": "^12.0.1" } } diff --git a/src/app.js b/src/app.js index 0afa301..6836152 100644 --- a/src/app.js +++ b/src/app.js @@ -6,6 +6,7 @@ const { GetActionsHandler } = require('./GetActionsHandler') const { logInit, logExit } = require('./logging') const updaters = require('./updaters') +const { checkDrawFunction } = require('./updaters/community') const effects = [] const http = require('http') @@ -42,6 +43,10 @@ async function init () { )` ) + // S5: log 'S5 draw function missing' if the backend's draw function or + // order_items.draw_outcome isn't there; indexing carries on either way. + await checkDrawFunction(db) + const actionHandler = new GetActionsHandler( updaters, effects, diff --git a/src/updaters/community.js b/src/updaters/community.js index ad273de..2eee973 100644 --- a/src/updaters/community.js +++ b/src/updaters/community.js @@ -7,6 +7,70 @@ const config = require(`../config/${process.env.NODE_ENV || 'dev'}`) const { resolveClaimId, resolveCreatedActionId, resolveCreatedObjectiveId, ResolveError } = require('../chain') const { toUTC } = require('../dates') +// S5a-es (plan §9): an Elm purchase of a batch offer takes its units from the live batch +// through the backend's frozen DB function. Transient database errors are retried; anything +// else fails soft (order recorded unassigned) so a draw problem never halts the indexer. +const DRAW_TRANSIENT_CODES = ['40P01', '40001', '55P03'] +const DRAW_MAX_RETRIES = 3 +const DRAW_RETRY_DELAY_MS = 50 + +const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) + +// Startup check, re-run after every draw error: the draw function and the +// order_items.draw_outcome column must exist (massive reads the schema once at startup and +// rejects unknown columns). The hourly cron greps the log for 'S5 draw function missing'; +// indexing carries on regardless. +async function checkDrawFunction (db) { + try { + const row = await db.instance.one( + `SELECT to_regprocedure('public.shop_draw_from_batch(bigint,integer,timestamp)') IS NOT NULL AS has_function, + EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' AND table_name = 'order_items' + AND column_name = 'draw_outcome') AS has_column` + ) + if (!row.has_function || !row.has_column) { + console.error(`S5 draw function missing (function: ${row.has_function}, order_items.draw_outcome: ${row.has_column})`) + return false + } + return true + } catch (e) { + logError('S5 draw function check failed', e) + return false + } +} + +// Draw `units` from the batch live at `at` (the block's time, never the wall clock). +// Each attempt is a savepoint (a nested pg-promise transaction), so a failed draw rolls +// back only itself and the outer transaction carries on. +// Resolves { batchId, outcome } with outcome 'drawn' | 'no_fit' | 'error'. +async function drawFromBatch (tx, productId, units, at) { + for (let attempt = 0; ; attempt++) { + try { + const row = await tx.instance.tx(async sp => { + const { previous } = await sp.one(`SELECT current_setting('lock_timeout') AS previous`) + await sp.none(`SET LOCAL lock_timeout = '2s'`) + const drawn = await sp.oneOrNone( + 'SELECT public.shop_draw_from_batch($1, $2, $3) AS batch_id', + [productId, units, at] + ) + // SET LOCAL outlives a released savepoint; put the caller's timeout back. + await sp.one(`SELECT set_config('lock_timeout', $1, true)`, [previous]) + return drawn + }) + const batchId = row && row.batch_id != null ? row.batch_id : null + return { batchId, outcome: batchId === null ? 'no_fit' : 'drawn' } + } catch (e) { + if (DRAW_TRANSIENT_CODES.includes(e.code) && attempt < DRAW_MAX_RETRIES) { + await sleep(DRAW_RETRY_DELAY_MS) + continue + } + logError('S5 draw failed; order recorded unassigned', e) + await checkDrawFunction(tx) + return { batchId: null, outcome: 'error' } + } + } +} + async function createCommunity (db, payload, blockInfo) { console.log(`Cambiatus >>> Create Community`, blockInfo.blockNumber) @@ -245,17 +309,25 @@ async function transferSale (db, payload, blockInfo, context) { }) if (Number(existing) > 0) return - const whereArg = { - id: payload.data.sale_id, - is_deleted: false - } + const whereArg = { id: payload.data.sale_id } - // Find sale + // Find sale. Deleted offers are still found: a purchase signed just as the seller + // deleted the offer is recorded like any other. A product that truly doesn't exist + // is a ResolveError (skipped, paged, re-indexable) rather than a crash loop. const sale = await tx.products.findOne(whereArg) if (sale === null) { - throw new Error('No sale data available') + throw new ResolveError(`transfersale: product ${payload.data.sale_id} not found`) } + const quantity = parseInt(payload.data.units) || 1 + + // Batch offers draw from the live batch at the block's time (a batch offer has + // track_stock false, so the units decrement below is skipped). The replay guard above + // already ran, so re-indexing a block never draws twice. + const draw = sale.availability === 'batches' + ? await drawFromBatch(tx, sale.id, quantity, toUTC(blockInfo.timestamp)) + : null + if (sale.track_stock) { const newUnits = sale.units - parseInt(payload.data.units) @@ -290,8 +362,7 @@ async function transferSale (db, payload, blockInfo, context) { // Write the matching line item. Kept inside the same guarded path so a // replay that hits orders_dedup_idx above never reaches this insert. - const quantity = parseInt(payload.data.units) || 1 - await tx.order_items.insert({ + const item = { order_id: order.id, product_id: sale.id, quantity: quantity, @@ -299,7 +370,13 @@ async function transferSale (db, payload, blockInfo, context) { title_snapshot: sale.title, inserted_at: toUTC(blockInfo.timestamp), updated_at: toUTC(blockInfo.timestamp) - }) + } + // Only batch offers carry the new keys; every other insert is exactly as before. + if (draw !== null) { + item.batch_id = draw.batchId + item.draw_outcome = draw.outcome + } + await tx.order_items.insert(item) } // Return the transaction so ledgered() can await it. Without the return the updater @@ -936,6 +1013,7 @@ async function assignRole (db, payload, blockInfo, _context) { } module.exports = { + checkDrawFunction, createCommunity, updateCommunity, netlink, diff --git a/test/fixtures/generate_s5_schema.js b/test/fixtures/generate_s5_schema.js new file mode 100644 index 0000000..93e3a06 --- /dev/null +++ b/test/fixtures/generate_s5_schema.js @@ -0,0 +1,82 @@ +// Generates test/fixtures/s5_schema.sql: the tables transferSale writes, built from +// written_columns.json (the manifest shared with the backend repo), plus the fixed +// definitions copied from the backend's S5a-core migration (20261017100000_s5a_batches.exs). +// Run `node test/fixtures/generate_s5_schema.js > test/fixtures/s5_schema.sql` after the +// manifest changes; the test suite fails if the committed file is stale. +const manifest = require('./written_columns.json') + +// Not in the manifest (the indexer only UPDATEs products.units and reads the rest), so +// declared by hand: the columns transferSale reads. +const PRODUCTS = `CREATE TABLE products ( + id bigserial PRIMARY KEY, + title character varying(255), + units integer, + is_deleted boolean DEFAULT false, + track_stock boolean NOT NULL DEFAULT true, + availability character varying(255) +);` + +// Copied from the backend migration; the draw function is a frozen API. +const FIXED = `CREATE EXTENSION IF NOT EXISTS btree_gist; + +CREATE TABLE product_batches ( + id bigserial PRIMARY KEY, + product_id bigint NOT NULL REFERENCES products(id), + quantity integer NOT NULL, + remaining integer NOT NULL, + label character varying(255), + opens_at timestamp(0) without time zone NOT NULL, + closes_at timestamp(0) without time zone NOT NULL, + inserted_at timestamp(0) without time zone NOT NULL DEFAULT now(), + updated_at timestamp(0) without time zone NOT NULL DEFAULT now(), + delivery_on date, + closed_at timestamp(0) without time zone, + CONSTRAINT product_batches_quantity_range CHECK (quantity BETWEEN 1 AND 200), + CONSTRAINT product_batches_remaining_non_negative CHECK (remaining >= 0), + CONSTRAINT product_batches_remaining_lte_quantity CHECK (remaining <= quantity), + CONSTRAINT product_batches_window_order CHECK (closes_at > opens_at) +); + +ALTER TABLE product_batches ADD CONSTRAINT product_batches_no_overlap + EXCLUDE USING gist (product_id WITH =, tsrange(opens_at, closes_at, '[)') WITH &&) + WHERE (closed_at IS NULL); + +CREATE TABLE _processed_actions ( + global_seq bigint PRIMARY KEY, + processed_at timestamptz NOT NULL DEFAULT now() +); + +CREATE FUNCTION public.shop_draw_from_batch(p_product_id bigint, p_quantity integer, p_at timestamp) +RETURNS bigint LANGUAGE plpgsql AS $$ +DECLARE v_batch_id bigint; +BEGIN + PERFORM 1 FROM public.products WHERE id = p_product_id FOR NO KEY UPDATE; + UPDATE public.product_batches SET remaining = remaining - p_quantity + WHERE product_id = p_product_id AND closed_at IS NULL + AND opens_at <= p_at AND closes_at > p_at + AND remaining >= p_quantity AND p_quantity > 0 + RETURNING id INTO v_batch_id; + RETURN v_batch_id; +END $$;` + +function table (name, spec) { + const columns = Object.entries(spec.columns).map(([column, type]) => ` ${column} ${type}`) + return `CREATE TABLE ${name} (\n id bigserial PRIMARY KEY,\n${columns.join(',\n')}\n);` +} + +function generate () { + return [ + '-- GENERATED by test/fixtures/generate_s5_schema.js from written_columns.json. Do not edit.', + PRODUCTS, + FIXED, + table('orders', manifest.orders), + 'CREATE UNIQUE INDEX orders_dedup_idx ON orders (created_tx, from_id, product_id);', + table('order_items', manifest.order_items), + 'ALTER TABLE order_items ADD CONSTRAINT order_items_draw_outcome_check CHECK (draw_outcome IN (\'drawn\', \'no_fit\', \'error\'));', + '' + ].join('\n\n') +} + +if (require.main === module) process.stdout.write(generate()) + +module.exports = { generate } diff --git a/test/fixtures/s5_schema.sql b/test/fixtures/s5_schema.sql new file mode 100644 index 0000000..8aab9b0 --- /dev/null +++ b/test/fixtures/s5_schema.sql @@ -0,0 +1,89 @@ +-- GENERATED by test/fixtures/generate_s5_schema.js from written_columns.json. Do not edit. + +CREATE TABLE products ( + id bigserial PRIMARY KEY, + title character varying(255), + units integer, + is_deleted boolean DEFAULT false, + track_stock boolean NOT NULL DEFAULT true, + availability character varying(255) +); + +CREATE EXTENSION IF NOT EXISTS btree_gist; + +CREATE TABLE product_batches ( + id bigserial PRIMARY KEY, + product_id bigint NOT NULL REFERENCES products(id), + quantity integer NOT NULL, + remaining integer NOT NULL, + label character varying(255), + opens_at timestamp(0) without time zone NOT NULL, + closes_at timestamp(0) without time zone NOT NULL, + inserted_at timestamp(0) without time zone NOT NULL DEFAULT now(), + updated_at timestamp(0) without time zone NOT NULL DEFAULT now(), + delivery_on date, + closed_at timestamp(0) without time zone, + CONSTRAINT product_batches_quantity_range CHECK (quantity BETWEEN 1 AND 200), + CONSTRAINT product_batches_remaining_non_negative CHECK (remaining >= 0), + CONSTRAINT product_batches_remaining_lte_quantity CHECK (remaining <= quantity), + CONSTRAINT product_batches_window_order CHECK (closes_at > opens_at) +); + +ALTER TABLE product_batches ADD CONSTRAINT product_batches_no_overlap + EXCLUDE USING gist (product_id WITH =, tsrange(opens_at, closes_at, '[)') WITH &&) + WHERE (closed_at IS NULL); + +CREATE TABLE _processed_actions ( + global_seq bigint PRIMARY KEY, + processed_at timestamptz NOT NULL DEFAULT now() +); + +CREATE FUNCTION public.shop_draw_from_batch(p_product_id bigint, p_quantity integer, p_at timestamp) +RETURNS bigint LANGUAGE plpgsql AS $$ +DECLARE v_batch_id bigint; +BEGIN + PERFORM 1 FROM public.products WHERE id = p_product_id FOR NO KEY UPDATE; + UPDATE public.product_batches SET remaining = remaining - p_quantity + WHERE product_id = p_product_id AND closed_at IS NULL + AND opens_at <= p_at AND closes_at > p_at + AND remaining >= p_quantity AND p_quantity > 0 + RETURNING id INTO v_batch_id; + RETURN v_batch_id; +END $$; + +CREATE TABLE orders ( + id bigserial PRIMARY KEY, + product_id bigint, + from_id character varying, + to_id character varying, + amount double precision, + units integer, + community_id character varying, + status character varying, + origin character varying, + total_token numeric, + created_block integer, + created_tx character varying, + created_at timestamp without time zone, + created_eos_account character varying, + inserted_at timestamp without time zone, + updated_at timestamp without time zone +); + +CREATE UNIQUE INDEX orders_dedup_idx ON orders (created_tx, from_id, product_id); + +CREATE TABLE order_items ( + id bigserial PRIMARY KEY, + order_id bigint, + product_id bigint, + batch_id bigint, + quantity integer, + unit_price_token numeric, + title_snapshot character varying, + draw_outcome character varying, + inserted_at timestamp without time zone, + updated_at timestamp without time zone +); + +ALTER TABLE order_items ADD CONSTRAINT order_items_draw_outcome_check CHECK (draw_outcome IN ('drawn', 'no_fit', 'error')); + diff --git a/test/fixtures/written_columns.json b/test/fixtures/written_columns.json new file mode 100644 index 0000000..d41cafa --- /dev/null +++ b/test/fixtures/written_columns.json @@ -0,0 +1,43 @@ +{ + "_comment": "Columns event-source writes (transferSale). The same file is committed in the event-source repo. orders and order_items are inserted by the indexer, so any NOT NULL column without a default must be listed; products is only updated. batch_id and draw_outcome are written by S5a-es, only for batch offers.", + "orders": { + "insert": true, + "columns": { + "product_id": "bigint", + "from_id": "character varying", + "to_id": "character varying", + "amount": "double precision", + "units": "integer", + "community_id": "character varying", + "status": "character varying", + "origin": "character varying", + "total_token": "numeric", + "created_block": "integer", + "created_tx": "character varying", + "created_at": "timestamp without time zone", + "created_eos_account": "character varying", + "inserted_at": "timestamp without time zone", + "updated_at": "timestamp without time zone" + } + }, + "order_items": { + "insert": true, + "columns": { + "order_id": "bigint", + "product_id": "bigint", + "batch_id": "bigint", + "quantity": "integer", + "unit_price_token": "numeric", + "title_snapshot": "character varying", + "draw_outcome": "character varying", + "inserted_at": "timestamp without time zone", + "updated_at": "timestamp without time zone" + } + }, + "products": { + "insert": false, + "columns": { + "units": "integer" + } + } +} diff --git a/test/transfer_sale_draw.test.js b/test/transfer_sale_draw.test.js new file mode 100644 index 0000000..9ec2909 --- /dev/null +++ b/test/transfer_sale_draw.test.js @@ -0,0 +1,443 @@ +/* eslint-env mocha */ +// S5a-es (plan §9.2): transferSale draws Elm purchases of batch offers from the live batch. +// Runs against a scratch database (s5a_es_test) it creates and drops itself. +process.env.NODE_ENV = process.env.NODE_ENV || 'dev' + +const assert = require('assert') +const fs = require('fs') +const path = require('path') +// The pg that massive's pg-promise runs on (the top-level `pg` is an older, unused copy). +const { Client } = require(require.resolve('pg', { paths: [require.resolve('pg-promise')] })) +const massive = require('massive') + +// community.js destructures logError at require time, so stub it first. +const logging = require('../src/logging') +const loggedErrors = [] +logging.logError = (message, error) => loggedErrors.push({ message, error }) + +const { transferSale, checkDrawFunction } = require('../src/updaters/community') +const { ResolveError } = require('../src/chain') +const { generate } = require('./fixtures/generate_s5_schema') +const manifest = require('./fixtures/written_columns.json') + +const SCRATCH_DB = 's5a_es_test' +const PG = { host: 'localhost', port: 5432, user: 'postgres', password: '' } +const SCHEMA_SQL = fs.readFileSync(path.join(__dirname, 'fixtures/s5_schema.sql'), 'utf8') + +const DRAW_FUNCTION_SQL = SCHEMA_SQL.match(/CREATE FUNCTION public\.shop_draw_from_batch[\s\S]*?END \$\$;/)[0] + +let db +let queries +let txCounter = 0 + +async function adminQuery (sql) { + const client = new Client({ ...PG, database: 'postgres' }) + await client.connect() + try { + await client.query(sql) + } finally { + await client.end() + } +} + +function scratchClient () { + return new Client({ ...PG, database: SCRATCH_DB }) +} + +async function insertProduct (fields = {}) { + return db.products.insert({ + title: 'Batch cake', + units: null, + track_stock: false, + availability: 'batches', + ...fields + }) +} + +async function insertBatch (productId, fields = {}) { + return db.product_batches.insert({ + product_id: productId, + quantity: 10, + remaining: 10, + opens_at: '2026-10-20 03:00:00', + closes_at: '2026-10-22 03:00:00', + ...fields + }) +} + +function purchase (productId, { units = 2, at = '2026-10-21T12:00:00Z' } = {}) { + txCounter += 1 + const payload = { + transactionId: `tx${txCounter}`, + authorization: [{ actor: 'buyer' }], + data: { from: 'buyer', to: 'seller', quantity: '10.0000 TST', units, sale_id: productId } + } + const blockInfo = { blockNumber: 100 + txCounter, timestamp: new Date(at) } + return { payload, blockInfo } +} + +async function buy (productId, opts) { + const { payload, blockInfo } = purchase(productId, opts) + await transferSale(db, payload, blockInfo, {}) + return payload +} + +async function itemFor (payload) { + const [item] = await db.instance.query( + `SELECT oi.* FROM order_items oi JOIN orders o ON o.id = oi.order_id WHERE o.created_tx = $1`, + [payload.transactionId] + ) + return item +} + +async function remaining (batchId) { + return (await db.product_batches.findOne(batchId)).remaining +} + +describe('transferSale draws from the live batch (S5a-es)', function () { + this.timeout(20000) + + before(async function () { + await adminQuery(`DROP DATABASE IF EXISTS ${SCRATCH_DB}`) + await adminQuery(`CREATE DATABASE ${SCRATCH_DB}`) + // Short deadlock detection so the forced-deadlock case is fast. + await adminQuery(`ALTER DATABASE ${SCRATCH_DB} SET deadlock_timeout = '500ms'`) + const client = scratchClient() + await client.connect() + await client.query(SCHEMA_SQL) + await client.end() + + queries = [] + db = await massive({ ...PG, database: SCRATCH_DB, max: 3 }, {}, { + query: e => queries.push(e.query) + }) + }) + + after(async function () { + if (db) await db.instance.$pool.end() + await adminQuery(`DROP DATABASE IF EXISTS ${SCRATCH_DB}`) + }) + + beforeEach(async function () { + await db.instance.none('TRUNCATE order_items, orders, product_batches, products, _processed_actions RESTART IDENTITY CASCADE') + queries.length = 0 + loggedErrors.length = 0 + }) + + // AT-e13: the schema stub is generated from written_columns.json + it('the committed schema stub matches the manifest generator', function () { + assert.strictEqual(SCHEMA_SQL, generate()) + }) + + // AT-e10: a purchase that fits is drawn from the live batch + it('draws a purchase that fits', async function () { + const product = await insertProduct() + const batch = await insertBatch(product.id) + const item = await itemFor(await buy(product.id, { units: 3 })) + + assert.strictEqual(item.draw_outcome, 'drawn') + assert.strictEqual(String(item.batch_id), String(batch.id)) + assert.strictEqual(item.quantity, 3) + assert.strictEqual(await remaining(batch.id), 7) + assert.strictEqual((await db.products.findOne(product.id)).units, null) + assert.strictEqual(loggedErrors.length, 0) + }) + + // AT-e10: a purchase larger than what remains is recorded unassigned + it('records a purchase larger than the remainder as no_fit', async function () { + const product = await insertProduct() + const batch = await insertBatch(product.id, { remaining: 1 }) + const item = await itemFor(await buy(product.id, { units: 2 })) + + assert.strictEqual(item.draw_outcome, 'no_fit') + assert.strictEqual(item.batch_id, null) + assert.strictEqual(await remaining(batch.id), 1) + assert.strictEqual(await db.orders.count({ product_id: product.id }) | 0, 1) + }) + + // AT-e10: the window is judged at the block's time in the batch's local day, São Paulo (UTC-3) + // and Costa Rica (UTC-6): last day's closes_at is local midnight, stored as UTC wall time. + const zones = [ + { name: 'São Paulo', closesAt: '2026-10-22 03:00:00', before: '2026-10-22T02:59:58Z', after: '2026-10-22T03:00:02Z' }, + { name: 'Costa Rica', closesAt: '2026-10-22 06:00:00', before: '2026-10-22T05:59:58Z', after: '2026-10-22T06:00:02Z' } + ] + zones.forEach(zone => { + it(`draws at 23:59:58 local on the last day and not at 00:00:02 next day (${zone.name})`, async function () { + const product = await insertProduct() + const batch = await insertBatch(product.id, { + opens_at: zone.closesAt.replace('22', '20'), + closes_at: zone.closesAt + }) + + const inside = await itemFor(await buy(product.id, { units: 1, at: zone.before })) + assert.strictEqual(inside.draw_outcome, 'drawn') + assert.strictEqual(String(inside.batch_id), String(batch.id)) + + const outside = await itemFor(await buy(product.id, { units: 1, at: zone.after })) + assert.strictEqual(outside.draw_outcome, 'no_fit') + assert.strictEqual(outside.batch_id, null) + assert.strictEqual(await remaining(batch.id), 9) + }) + }) + + // AT-e10: a closed batch is never drawn from + it('records a purchase against a closed batch as no_fit', async function () { + const product = await insertProduct() + const batch = await insertBatch(product.id, { closed_at: '2026-10-21 10:00:00' }) + const item = await itemFor(await buy(product.id)) + + assert.strictEqual(item.draw_outcome, 'no_fit') + assert.strictEqual(item.batch_id, null) + assert.strictEqual(await remaining(batch.id), 10) + }) + + // AT-e11: a deadlock inside the savepoint is retried and the purchase is then drawn. + // Connection B holds the batch row; the draw (A) takes the product lock then waits on that + // row; B then asks for the product lock. Postgres aborts A (B's own deadlock_timeout is + // huge, so A's check is the one that finds the cycle), the savepoint rolls back, and + // A's retry succeeds once B has committed. + it('retries a forced deadlock and then draws', async function () { + const product = await insertProduct() + const batch = await insertBatch(product.id) + + const b = scratchClient() + await b.connect() + try { + await b.query(`SET deadlock_timeout = '1h'`) + await b.query('BEGIN') + await b.query('UPDATE product_batches SET label = $1 WHERE id = $2', ['held', batch.id]) + + const sale = buy(product.id, { units: 2 }) + + // Wait until the draw is blocked on B's row lock, then close the cycle. + const deadline = Date.now() + 10000 + for (;;) { + const { rows } = await b.query( + `SELECT 1 FROM pg_stat_activity + WHERE datname = $1 AND wait_event_type = 'Lock' AND pid <> pg_backend_pid()`, + [SCRATCH_DB] + ) + if (rows.length > 0) break + assert.ok(Date.now() < deadline, 'the draw never blocked') + await new Promise(resolve => setTimeout(resolve, 5)) + } + const bLock = b.query('SELECT id FROM products WHERE id = $1 FOR UPDATE', [product.id]) + .then(() => b.query('COMMIT')) + + const payload = await sale + await bLock + + const item = await itemFor(payload) + assert.strictEqual(item.draw_outcome, 'drawn') + assert.strictEqual(String(item.batch_id), String(batch.id)) + assert.strictEqual(await remaining(batch.id), 8) + const draws = queries.filter(q => /SELECT public\.shop_draw_from_batch\(/.test(q)) + assert.ok(draws.length >= 2, `expected a retried draw, saw ${draws.length} call(s)`) + assert.strictEqual(loggedErrors.length, 0) + assert.strictEqual(await db.orders.count({ product_id: product.id }) | 0, 1) + } finally { + await b.end() + } + }) + + // Keeps connection B's batch-row lock and, every time the draw is blocked on it, closes a + // deadlock cycle (B asks for the product lock, the draw is the victim), then releases the + // product lock again so the next attempt can start. Runs until `until` settles. + async function deadlockEveryAttempt (b, productId, until) { + let finished = false + until.then(() => { finished = true }, () => { finished = true }) + while (!finished) { + const { rows } = await b.query( + `SELECT 1 FROM pg_stat_activity + WHERE datname = $1 AND wait_event_type = 'Lock' AND pid <> pg_backend_pid()`, + [SCRATCH_DB] + ) + if (rows.length === 0) { + await new Promise(resolve => setTimeout(resolve, 5)) + continue + } + await b.query('SAVEPOINT attempt') + await b.query('SELECT id FROM products WHERE id = $1 FOR UPDATE', [productId]) + await b.query('ROLLBACK TO attempt') + } + } + + // AT-e11: with the deadlock forced on every attempt the draw is retried three times, then the + // order is recorded with 'error', the outer transaction commits and nothing throws + it('records error after three retries when every attempt deadlocks', async function () { + const product = await insertProduct() + const batch = await insertBatch(product.id) + + const b = scratchClient() + await b.connect() + try { + await b.query(`SET deadlock_timeout = '1h'`) + await b.query('BEGIN') + await b.query('UPDATE product_batches SET label = $1 WHERE id = $2', ['held', batch.id]) + + const sale = buy(product.id, { units: 2 }) + const driver = deadlockEveryAttempt(b, product.id, sale) + const payload = await sale + await driver + await b.query('COMMIT') + + const item = await itemFor(payload) + assert.strictEqual(item.draw_outcome, 'error') + assert.strictEqual(item.batch_id, null) + assert.strictEqual(await remaining(batch.id), 10) + assert.strictEqual(await db.orders.count({ product_id: product.id }) | 0, 1) + assert.strictEqual(queries.filter(q => /SELECT public\.shop_draw_from_batch\(/.test(q)).length, 4) + assert.strictEqual(loggedErrors.length, 1) + assert.strictEqual(loggedErrors[0].message, 'S5 draw failed; order recorded unassigned') + assert.strictEqual(loggedErrors[0].error.code, '40P01') + } finally { + await b.end() + } + }) + + // AT-e18: a product lock held by another connection makes the draw fail with 55P03 after 2s, + // retry, and end 'error' without stalling the indexer + it('fails soft with error when the product lock is held (55P03)', async function () { + this.timeout(30000) + const product = await insertProduct() + const batch = await insertBatch(product.id) + + const b = scratchClient() + await b.connect() + try { + await b.query('BEGIN') + await b.query('SELECT id FROM products WHERE id = $1 FOR UPDATE', [product.id]) + + const started = Date.now() + const payload = await buy(product.id) + const elapsed = Date.now() - started + await b.query('ROLLBACK') + + // four attempts at 2s each plus three 50 ms pauses: bounded, not stalled + assert.ok(elapsed >= 7500 && elapsed < 15000, `took ${elapsed}ms`) + const item = await itemFor(payload) + assert.strictEqual(item.draw_outcome, 'error') + assert.strictEqual(item.batch_id, null) + assert.strictEqual(await remaining(batch.id), 10) + assert.strictEqual(queries.filter(q => /SELECT public\.shop_draw_from_batch\(/.test(q)).length, 4) + assert.strictEqual(loggedErrors[0].error.code, '55P03') + } finally { + await b.end() + } + }) + + // AT-e10: the same payload twice leaves one order and one draw + it('records one order and one draw for a replayed payload', async function () { + const product = await insertProduct() + const batch = await insertBatch(product.id) + const { payload, blockInfo } = purchase(product.id, { units: 4 }) + + await transferSale(db, payload, blockInfo, {}) + await transferSale(db, payload, blockInfo, {}) + + assert.strictEqual(await db.orders.count({ product_id: product.id }) | 0, 1) + assert.strictEqual(await db.order_items.count({ product_id: product.id }) | 0, 1) + assert.strictEqual(await remaining(batch.id), 6) + }) + + // AT-e18: a purchase of a soft-deleted offer is still recorded (drawn if it fits); a missing product is a ResolveError + it('records a purchase of a deleted offer and raises ResolveError for a missing one', async function () { + const product = await insertProduct({ is_deleted: true }) + await insertBatch(product.id) + const item = await itemFor(await buy(product.id, { units: 1 })) + assert.strictEqual(item.draw_outcome, 'drawn') + + await assert.rejects(buy(999999), ResolveError) + assert.strictEqual(await db.orders.count({ product_id: 999999 }) | 0, 0) + }) + + // AT-e18: a non-batch offer's order_items insert carries no batch_id or draw_outcome key + it('writes no batch_id or draw_outcome for a stock offer and still decrements units', async function () { + const product = await insertProduct({ availability: 'stock', track_stock: true, units: 5 }) + await buy(product.id, { units: 2 }) + + assert.strictEqual((await db.products.findOne(product.id)).units, 3) + const itemInsert = queries.find(q => /INSERT INTO "?order_items"?/i.test(q)) + assert.ok(itemInsert, 'no order_items insert seen') + assert.ok(!/batch_id|draw_outcome/.test(itemInsert), itemInsert) + assert.ok(!queries.some(q => /shop_draw_from_batch/.test(q))) + const [item] = await db.order_items.find({ product_id: product.id }) + assert.strictEqual(item.draw_outcome, null) + assert.strictEqual(item.batch_id, null) + }) + + // AT-e13: transferSale writes only columns listed in written_columns.json + it('writes only manifest columns', async function () { + const stockProduct = await insertProduct({ availability: 'stock', track_stock: true, units: 5 }) + const batchProduct = await insertProduct() + await insertBatch(batchProduct.id) + await buy(stockProduct.id) + await buy(batchProduct.id) + + const written = { orders: new Set(), order_items: new Set(), products: new Set() } + queries.forEach(q => { + let m = q.match(/INSERT INTO "?(orders|order_items)"?\s*\(([^)]*)\)/i) + if (m) m[2].split(',').forEach(c => written[m[1]].add(c.replace(/["\s]/g, ''))) + m = q.match(/UPDATE "?products"?\s+SET\s+(.*?)\s+WHERE/is) + if (m) m[1].split(',').forEach(c => written.products.add(c.split('=')[0].replace(/["\s]/g, ''))) + }) + assert.ok(written.orders.size > 0 && written.order_items.size > 0 && written.products.size > 0, 'no writes captured') + Object.keys(written).forEach(table => { + written[table].forEach(column => { + assert.ok(column in manifest[table].columns, `${table}.${column} is written but not in written_columns.json`) + }) + }) + }) + + // AT-e09: with the draw function dropped the order is recorded unassigned with 'error', + // the error is logged, nothing is thrown, and the startup check reports the function missing + describe('draw function missing', function () { + afterEach(async function () { + await db.instance.none(`DROP FUNCTION IF EXISTS public.shop_draw_from_batch(bigint, integer, timestamp)`) + await db.instance.none(DRAW_FUNCTION_SQL) + await db.instance.none('ALTER TABLE order_items DROP COLUMN IF EXISTS draw_outcome') + await db.instance.none('ALTER TABLE order_items ADD COLUMN draw_outcome character varying') + await db.instance.none(`ALTER TABLE order_items ADD CONSTRAINT order_items_draw_outcome_check CHECK (draw_outcome IN ('drawn', 'no_fit', 'error'))`) + }) + + // AT-e18: with order_items.draw_outcome missing, the startup check reports it + it('the startup check reports a missing draw_outcome column', async function () { + await db.instance.none('ALTER TABLE order_items DROP COLUMN draw_outcome') + const lines = [] + const originalLog = console.error + console.error = (...args) => lines.push(args.join(' ')) + try { + assert.strictEqual(await checkDrawFunction(db), false) + } finally { + console.error = originalLog + } + assert.ok(lines.some(l => l.includes('S5 draw function missing')), lines.join('\n')) + }) + + it('fails soft with draw_outcome error and the startup check says so', async function () { + const product = await insertProduct() + await insertBatch(product.id) + assert.strictEqual(await checkDrawFunction(db), true) + + await db.instance.none('DROP FUNCTION public.shop_draw_from_batch(bigint, integer, timestamp)') + const lines = [] + const originalLog = console.error + console.error = (...args) => lines.push(args.join(' ')) + let payload + try { + payload = await buy(product.id) + assert.strictEqual(await checkDrawFunction(db), false) + } finally { + console.error = originalLog + } + + const item = await itemFor(payload) + assert.strictEqual(item.draw_outcome, 'error') + assert.strictEqual(item.batch_id, null) + assert.strictEqual(await db.orders.count({ product_id: product.id }) | 0, 1) + assert.ok(loggedErrors.some(e => e.message === 'S5 draw failed; order recorded unassigned')) + // once from the re-check after the draw error, once from the explicit startup check + assert.ok(lines.filter(l => l.includes('S5 draw function missing')).length >= 2, lines.join('\n')) + }) + }) +}) diff --git a/yarn.lock b/yarn.lock index 47f21e0..4ed7392 100644 --- a/yarn.lock +++ b/yarn.lock @@ -98,6 +98,11 @@ ajv@^6.0.1, ajv@^6.12.3, ajv@^6.5.0: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ansi-colors@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" + integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== + ansi-escapes@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" @@ -113,6 +118,11 @@ ansi-regex@^3.0.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" @@ -125,6 +135,21 @@ ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -132,6 +157,11 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + array-includes@^3.0.3, array-includes@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.1.tgz#cdd67e6852bdf9c1215460786732255ed2459348" @@ -199,6 +229,11 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + bindings@1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" @@ -222,6 +257,25 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.1.7" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.1.7.tgz#14ea3836731823bb24740225ea43b941b436f9d1" + integrity sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g== + dependencies: + balanced-match "^1.0.0" + +braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +browser-stdout@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + bson@^1.1.4: version "1.1.5" resolved "https://registry.yarnpkg.com/bson/-/bson-1.1.5.tgz#2aaae98fcdf6750c0848b0cba1ddec3c73060a34" @@ -249,6 +303,11 @@ callsites@^0.2.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca" integrity sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo= +camelcase@^6.0.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" @@ -274,11 +333,34 @@ chalk@^2.0.0, chalk@^2.1.0: escape-string-regexp "^1.0.5" supports-color "^5.3.0" +chalk@^4.1.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + chardet@^0.4.0: version "0.4.2" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2" integrity sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I= +chokidar@^3.5.3: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + circular-json@^0.3.1: version "0.3.3" resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66" @@ -296,6 +378,15 @@ cli-width@^2.0.0: resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48" integrity sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw== +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" @@ -303,11 +394,23 @@ color-convert@^1.9.0: dependencies: color-name "1.1.3" +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + color-name@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + combined-stream@^1.0.6, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -382,6 +485,18 @@ debug@^3.1.0: dependencies: ms "^2.1.1" +debug@^4.3.5: + version "4.4.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + +decamelize@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" + integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== + deep-is@~0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" @@ -440,6 +555,11 @@ denque@^1.4.1: resolved "https://registry.yarnpkg.com/denque/-/denque-1.4.1.tgz#6744ff7641c148c3f8a69c307e51235c1f4a37cf" integrity sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ== +diff@^5.2.0: + version "5.2.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.2.tgz#0a4742797281d09cfa699b79ea32d27723623bad" + integrity sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A== + doctrine@1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" @@ -463,6 +583,11 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + error-ex@^1.2.0, error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" @@ -526,11 +651,21 @@ es6-promisify@^5.0.0: dependencies: es6-promise "^4.0.3" +escalade@^3.1.1: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + eslint-config-standard-jsx@6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/eslint-config-standard-jsx/-/eslint-config-standard-jsx-6.0.2.tgz#90c9aa16ac2c4f8970c13fc7efc608bacd02da70" @@ -780,6 +915,13 @@ file-uri-to-path@1.0.0: resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + find-root@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" @@ -792,6 +934,14 @@ find-up@^2.0.0, find-up@^2.1.0: dependencies: locate-path "^2.0.0" +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + flat-cache@^1.2.1: version "1.3.4" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.3.4.tgz#2c2ef77525cc2929007dfffa1dd314aa9c9dee6f" @@ -802,6 +952,11 @@ flat-cache@^1.2.1: rimraf "~2.6.2" write "^0.2.1" +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" @@ -821,6 +976,11 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" @@ -831,6 +991,11 @@ functional-red-black-tree@^1.0.1: resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + get-stdin@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b" @@ -843,6 +1008,13 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + glob@7.1.4: version "7.1.4" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" @@ -867,6 +1039,17 @@ glob@7.1.6, glob@^7.0.5, glob@^7.1.2, glob@^7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + globals@^11.7.0: version "11.12.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" @@ -902,6 +1085,11 @@ has-flag@^3.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + has-symbols@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" @@ -914,6 +1102,11 @@ has@^1.0.1, has@^1.0.3: dependencies: function-bind "^1.1.1" +he@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + hosted-git-info@^2.1.4: version "2.8.9" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" @@ -995,6 +1188,13 @@ is-arrayish@^0.2.1: resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + is-callable@^1.1.4, is-callable@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.1.tgz#4d1e21a4f437509d25ce55f8184350771421c96d" @@ -1005,16 +1205,43 @@ is-date-object@^1.0.1: resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + is-fullwidth-code-point@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + is-negative-zero@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.0.tgz#9553b121b0fac28869da9ed459e20c7543788461" integrity sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE= +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-plain-obj@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + is-regex@^1.1.0, is-regex@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9" @@ -1044,6 +1271,11 @@ is-typedarray@~1.0.0: resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + isarray@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" @@ -1082,6 +1314,13 @@ js-yaml@^3.11.0: argparse "^1.0.7" esprima "^4.0.0" +js-yaml@^4.1.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.3.2.tgz#8e44fb14a2643c59726bb15787b5f1512cb3d3fb" + integrity sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA== + dependencies: + argparse "^2.0.1" + jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" @@ -1174,6 +1413,13 @@ locate-path@^2.0.0: p-locate "^2.0.0" path-exists "^3.0.0" +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + lodash@4.17.14: version "4.17.14" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba" @@ -1189,6 +1435,14 @@ lodash@^4.17.19, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -1268,6 +1522,13 @@ minimatch@^3.0.3, minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" +minimatch@^5.0.1, minimatch@^5.1.6: + version "5.1.9" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.9.tgz#1293ef15db0098b394540e8f9f744f9fda8dee4b" + integrity sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw== + dependencies: + brace-expansion "^2.0.1" + minimist@^1.1.0, minimist@^1.2.5: version "1.2.6" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" @@ -1280,6 +1541,32 @@ mkdirp@^0.5.1: dependencies: minimist "^1.2.5" +mocha@^10.8.2: + version "10.8.2" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.8.2.tgz#8d8342d016ed411b12a429eb731b825f961afb96" + integrity sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg== + dependencies: + ansi-colors "^4.1.3" + browser-stdout "^1.3.1" + chokidar "^3.5.3" + debug "^4.3.5" + diff "^5.2.0" + escape-string-regexp "^4.0.0" + find-up "^5.0.0" + glob "^8.1.0" + he "^1.2.0" + js-yaml "^4.1.0" + log-symbols "^4.1.0" + minimatch "^5.1.6" + ms "^2.1.3" + serialize-javascript "^6.0.2" + strip-json-comments "^3.1.1" + supports-color "^8.1.1" + workerpool "^6.5.1" + yargs "^16.2.0" + yargs-parser "^20.2.9" + yargs-unparser "^2.0.0" + mongodb@^3.1.3: version "3.6.2" resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.6.2.tgz#1154a4ac107bf1375112d83a29c5cf97704e96b6" @@ -1303,6 +1590,11 @@ ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + murmurhash@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/murmurhash/-/murmurhash-1.0.0.tgz#d0e4556892317d984af632b6a88d5605beef26a7" @@ -1338,6 +1630,11 @@ normalize-package-data@^2.3.2: semver "2 || 3 || 4 || 5" validate-npm-package-license "^3.0.1" +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + oauth-sign@~0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" @@ -1406,6 +1703,13 @@ p-limit@^1.1.0: dependencies: p-try "^1.0.0" +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + p-locate@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" @@ -1413,6 +1717,13 @@ p-locate@^2.0.0: dependencies: p-limit "^1.1.0" +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + p-try@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" @@ -1448,6 +1759,11 @@ path-exists@^3.0.0: resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" @@ -1679,6 +1995,11 @@ pgpass@1.x: dependencies: split "^1.0.0" +picomatch@^2.0.4, picomatch@^2.2.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.2.tgz#5a942915e26b372dc0f0e6753149a16e6b1c5601" + integrity sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA== + pify@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -1784,6 +2105,13 @@ qs@~6.5.2: resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + react-is@^16.8.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -1829,6 +2157,13 @@ readable-stream@^2.3.5: string_decoder "~1.1.1" util-deprecate "~1.0.1" +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + regexpp@^2.0.0, regexpp@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" @@ -1876,6 +2211,11 @@ request@^2.87.0: tunnel-agent "^0.6.0" uuid "^3.3.2" +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + require-uncached@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" @@ -1941,7 +2281,7 @@ rxjs@^5.5.2: dependencies: symbol-observable "1.0.1" -safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2: +safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -1973,6 +2313,13 @@ semver@4.3.2: resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.2.tgz#c7a07158a80bedd052355b770d82d6640f803be7" integrity sha1-x6BxWKgL7dBSNVt3DYLWZA+AO+c= +serialize-javascript@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" + integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== + dependencies: + randombytes "^2.1.0" + shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -2115,6 +2462,15 @@ string-width@^2.1.0, string-width@^2.1.1: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" +string-width@^4.1.0, string-width@^4.2.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string.prototype.trimend@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913" @@ -2157,6 +2513,13 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" @@ -2167,6 +2530,11 @@ strip-json-comments@^2.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" @@ -2179,6 +2547,20 @@ supports-color@^5.3.0: dependencies: has-flag "^3.0.0" +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + symbol-observable@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.1.tgz#8340fc4702c3122df5d22288f88283f513d3fdd4" @@ -2213,6 +2595,13 @@ tmp@^0.0.33: dependencies: os-tmpdir "~1.0.2" +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + tough-cookie@^2.3.3, tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" @@ -2296,6 +2685,20 @@ word-wrap@~1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== +workerpool@^6.5.1: + version "6.5.1" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544" + integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -2312,3 +2715,41 @@ xtend@^4.0.0, xtend@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yargs-parser@^20.2.2, yargs-parser@^20.2.9: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs-unparser@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" + integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== + dependencies: + camelcase "^6.0.0" + decamelize "^4.0.0" + flat "^5.0.2" + is-plain-obj "^2.1.0" + +yargs@^16.2.0: + version "16.2.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.2.tgz#c56731dca0d2788ae0866dd3c83907d6bab85f7d" + integrity sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==