From ef5786ecd376bd39c1119ae3e0918339ad9cbbfb Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Mon, 13 Feb 2023 15:55:23 -0500 Subject: [PATCH 01/43] EDSC-3612: moved and refactored jsonToCsv.js --- .../src/util/__tests__/jsonToCsv.test.js | 69 +++++++++++++++++++ serverless/src/util/jsonToCsv.js | 25 +++++++ 2 files changed, 94 insertions(+) create mode 100644 serverless/src/util/__tests__/jsonToCsv.test.js create mode 100644 serverless/src/util/jsonToCsv.js diff --git a/serverless/src/util/__tests__/jsonToCsv.test.js b/serverless/src/util/__tests__/jsonToCsv.test.js new file mode 100644 index 0000000000..34d758037e --- /dev/null +++ b/serverless/src/util/__tests__/jsonToCsv.test.js @@ -0,0 +1,69 @@ +import { jsonToCsv } from '../jsonToCsv' + +describe('jsonToCsv', () => { + const columns = [ + { name: 'Data Provider', path: 'provider' }, + { name: 'Short Name', path: 'shortName' }, + { name: 'Version', path: 'versionId' }, + { name: 'Entry Title', path: 'title' }, + { name: 'Processing Level', path: 'processingLevelId' }, + { name: 'Platform', path: 'platforms.shortName' }, + { name: 'Start Time', path: 'timeStart' }, + { name: 'End Time', path: 'timeEnd' } + ] + + test('converts a JSON array to CSV', () => { + const json = [{ + provider: 'test provider', + shortName: 'test shortName', + versionId: 'test versionId', + title: 'test title', + processingLevelId: 'test processingLevelId', + platforms: [{ shortName: 'test platforms' }], + timeStart: 'test timeStart', + timeEnd: 'test timeEnd' + }] + + const csv = 'Data Provider,Short Name,Version,Entry Title,Processing Level,Platform,Start Time,End Time\r\ntest provider,test shortName,test versionId,test title,test processingLevelId,test platforms,test timeStart,test timeEnd\r\n' + + expect(jsonToCsv(json, columns)).toEqual(csv) + }) + + test('converts an array of platforms to a string', () => { + const json = [{ + provider: 'test provider', + shortName: 'test shortName', + versionId: 'test versionId', + title: 'test title', + processingLevelId: 'test processingLevelId', + platforms: [{ + shortName: 'test platform 1' + }, { + shortName: 'test platform 2' + }], + timeStart: 'test timeStart', + timeEnd: null + }] + + const csv = 'Data Provider,Short Name,Version,Entry Title,Processing Level,Platform,Start Time,End Time\r\ntest provider,test shortName,test versionId,test title,test processingLevelId,"test platform 1, test platform 2",test timeStart,\r\n' + + expect(jsonToCsv(json, columns)).toEqual(csv) + }) + + test('replaces null values with empty strings', () => { + const json = [{ + provider: 'test provider', + shortName: 'test shortName', + versionId: 'test versionId', + title: 'test title', + processingLevelId: 'test processingLevelId', + platforms: [{ shortName: 'test platforms' }], + timeStart: 'test timeStart', + timeEnd: null + }] + + const csv = 'Data Provider,Short Name,Version,Entry Title,Processing Level,Platform,Start Time,End Time\r\ntest provider,test shortName,test versionId,test title,test processingLevelId,test platforms,test timeStart,\r\n' + + expect(jsonToCsv(json, columns)).toEqual(csv) + }) +}) diff --git a/serverless/src/util/jsonToCsv.js b/serverless/src/util/jsonToCsv.js new file mode 100644 index 0000000000..8ac55a5f63 --- /dev/null +++ b/serverless/src/util/jsonToCsv.js @@ -0,0 +1,25 @@ +import { get } from 'sendero'; +import { unparse } from 'papaparse'; + +/** + * @name jsonToCsv + * @description converts an array of JSON objects to a CSV + * @param {Array} jsonArray array of JSON objects + * @param {Array} columns - array of column objects with name and dot path to value, such as [{ name: "Platform", path: "platforms.shortName" }, ...] + */ +export const jsonToCsv = (jsonArray, columns) => { + const data = jsonArray.map((item) => ( + columns.reduce((row, { name, path }) => { + row[name] = get(item, path, { clean: true, sort: true, stringify: true, unique: true }).join(', ') + return row + }, {}) + )); + + const config = { + columns: columns.map(({ name }) => name) + } + + // adding line break at end to ensure backwards compatability + // but may remove it in the future + return unparse(data, config) + '\r\n'; +} From 73eefad558c45ed0185672571b769a13803ffba5 Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Mon, 13 Feb 2023 15:57:01 -0500 Subject: [PATCH 02/43] EDSC-3612: removed old version of jsonToCsv --- .../exportSearch/__tests__/jsonToCsv.test.js | 58 ------------------ serverless/src/exportSearch/jsonToCsv.js | 59 ------------------- 2 files changed, 117 deletions(-) delete mode 100644 serverless/src/exportSearch/__tests__/jsonToCsv.test.js delete mode 100644 serverless/src/exportSearch/jsonToCsv.js diff --git a/serverless/src/exportSearch/__tests__/jsonToCsv.test.js b/serverless/src/exportSearch/__tests__/jsonToCsv.test.js deleted file mode 100644 index 157519e124..0000000000 --- a/serverless/src/exportSearch/__tests__/jsonToCsv.test.js +++ /dev/null @@ -1,58 +0,0 @@ -import { jsonToCsv } from '../jsonToCsv' - -describe('jsonToCsv', () => { - test('converts a JSON array to CSV', () => { - const json = [{ - provider: 'test provider', - shortName: 'test shortName', - versionId: 'test versionId', - title: 'test title', - processingLevelId: 'test processingLevelId', - platforms: [{ shortName: 'test platforms' }], - timeStart: 'test timeStart', - timeEnd: 'test timeEnd' - }] - - const csv = 'Data Provider,Short Name,Version,Entry Title,Processing Level,Platform,Start Time,End Time\r\n"test provider","test shortName","test versionId","test title","test processingLevelId","test platforms","test timeStart","test timeEnd"\r\n' - - expect(jsonToCsv(json)).toEqual(csv) - }) - - test('converts an array of platforms to a string', () => { - const json = [{ - provider: 'test provider', - shortName: 'test shortName', - versionId: 'test versionId', - title: 'test title', - processingLevelId: 'test processingLevelId', - platforms: [{ - shortName: 'test platform 1' - }, { - shortName: 'test platform 2' - }], - timeStart: 'test timeStart', - timeEnd: null - }] - - const csv = 'Data Provider,Short Name,Version,Entry Title,Processing Level,Platform,Start Time,End Time\r\n"test provider","test shortName","test versionId","test title","test processingLevelId","test platform 1, test platform 2","test timeStart",\r\n' - - expect(jsonToCsv(json)).toEqual(csv) - }) - - test('replaces null values with empty strings', () => { - const json = [{ - provider: 'test provider', - shortName: 'test shortName', - versionId: 'test versionId', - title: 'test title', - processingLevelId: 'test processingLevelId', - platforms: [{ shortName: 'test platforms' }], - timeStart: 'test timeStart', - timeEnd: null - }] - - const csv = 'Data Provider,Short Name,Version,Entry Title,Processing Level,Platform,Start Time,End Time\r\n"test provider","test shortName","test versionId","test title","test processingLevelId","test platforms","test timeStart",\r\n' - - expect(jsonToCsv(json)).toEqual(csv) - }) -}) diff --git a/serverless/src/exportSearch/jsonToCsv.js b/serverless/src/exportSearch/jsonToCsv.js deleted file mode 100644 index 69505ce24c..0000000000 --- a/serverless/src/exportSearch/jsonToCsv.js +++ /dev/null @@ -1,59 +0,0 @@ -// Pretty headers for the CSV file -const headers = [ - 'Data Provider', - 'Short Name', - 'Version', - 'Entry Title', - 'Processing Level', - 'Platform', - 'Start Time', - 'End Time' -] - -// Collection metadata keys to loop through for building the CSV data -const keysToMap = [ - 'provider', - 'shortName', - 'versionId', - 'title', - 'processingLevelId', - 'platforms', - 'timeStart', - 'timeEnd' -] - -/** - * Replacement function for JSON.stringify, replaces null values with an empty string - */ -const replacer = (_key, value) => (value === null ? undefined : value) - -/** - * Converts JSON array to CSV for search exports - * @param {Array} jsonArray JSON to convert - */ -export const jsonToCsv = (jsonArray) => { - // Build the header line - let csvString = `${headers.join(',')}\r\n` - - // Loop through the JSON array and builds a line of CSV data for each collection - jsonArray.forEach((collection) => { - const collectionValues = [] - - // Loop through the metadata keys to build the collection data - keysToMap.forEach((key) => { - // If the key is platforms, join the array of shortnames - if (key === 'platforms') { - const shortNames = collection[key].map((platform) => platform.shortName) - - collectionValues.push(JSON.stringify(shortNames.join(', '), replacer)) - } else { - collectionValues.push(JSON.stringify(collection[key], replacer)) - } - }) - - // Add this collection onto the CSV string - csvString += `${collectionValues.join(',')}\r\n` - }) - - return csvString -} From e57d59b039aad6ab3c86d76ca420d1708decdc2c Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Mon, 13 Feb 2023 17:06:01 -0500 Subject: [PATCH 03/43] EDSC-3612: added exportSearchRequest --- .../__tests__/handler.test.js | 194 ++++++++++++++++++ serverless/src/exportSearchRequest/handler.js | 98 +++++++++ 2 files changed, 292 insertions(+) create mode 100644 serverless/src/exportSearchRequest/__tests__/handler.test.js create mode 100644 serverless/src/exportSearchRequest/handler.js diff --git a/serverless/src/exportSearchRequest/__tests__/handler.test.js b/serverless/src/exportSearchRequest/__tests__/handler.test.js new file mode 100644 index 0000000000..8df7cf48ac --- /dev/null +++ b/serverless/src/exportSearchRequest/__tests__/handler.test.js @@ -0,0 +1,194 @@ +import AWS from 'aws-sdk' +import MockDate from 'mockdate' +import nock from 'nock' + +import * as deployedEnvironment from '../../../../sharedUtils/deployedEnvironment' +import * as getJwtToken from '../../util/getJwtToken' +import * as getEchoToken from '../../util/urs/getEchoToken' +import * as getEarthdataConfig from '../../../../sharedUtils/config' + +import exportSearch from '../handler' + +const OLD_ENV = process.env + +const AWS_TEST_REGION = 'us-east-1' +const SQS_TEST_PORT = 9324 +const SQS_TEST_HOST = `0.0.0.0:${SQS_TEST_PORT}` +const SQS_TEST_LOCALHOST = `localhost:${SQS_TEST_PORT}` +const SQS_TEST_QUEUE_NAME = 'REQUEST_SEARCH_EXPORT_TEST_QUEUE' +const SQS_TEST_ENDPOINT = `http://${SQS_TEST_HOST}` +const MOCK_ECHO_TOKEN = '1234-abcd-5678-efgh' + +// need to configure here because the aws-sdk expects it +// without it, the handler will throw an error +AWS.config.update({ + accessKeyId: Math.random().toString(), // this will be ignored by ElasticMQ + secretAccessKey: Math.random().toString(), // this will be ignored by ElasticMQ + region: AWS_TEST_REGION +}) + +const sqs = new AWS.SQS({ endpoint: SQS_TEST_ENDPOINT }) + +let testSearchExportQueueUrl +beforeAll(async () => { + // explicitly allow network connections to ElasticMQ (SQS-Compatible) server + nock.enableNetConnect(host => host === SQS_TEST_HOST || host === SQS_TEST_LOCALHOST) + + // create a queue and save the url to it + const { QueueUrl } = await sqs.createQueue({ QueueName: SQS_TEST_QUEUE_NAME }).promise() + + // we save the url here, so we can pass it to the handler via an environmental variable + testSearchExportQueueUrl = QueueUrl +}) + +afterAll(async () => { + await sqs.deleteQueue({ QueueUrl: testSearchExportQueueUrl }).promise() + + // re-disable all network connections, including those to localhost and 0.0.0.0 + nock.disableNetConnect() +}) + +beforeEach(async () => { + jest.clearAllMocks() + + jest.spyOn(deployedEnvironment, 'deployedEnvironment').mockImplementation(() => 'prod') + jest.spyOn(getJwtToken, 'getJwtToken').mockImplementation(() => 'mockJwt') + + // Manage resetting ENV variables + // TODO: This is causing problems with mocking knex but is noted as important for managing process.env + // jest.resetModules() + process.env = { ...OLD_ENV } + delete process.env.NODE_ENV + + // MockDate is used here to overwrite the js Date object. This allows us to + // mock changes needed to test the moment functions + MockDate.set('1988-09-03T10:00:00.000Z') +}) + +afterEach(() => { + // Restore any ENV variables overwritten in tests + process.env = OLD_ENV + + // reset hacks on built-ins + MockDate.reset() + jest.spyOn(global.Math, 'random').mockRestore(); +}) + +describe('exportSearch', () => { + test('returns csv response correctly', async () => { + process.env.searchExportQueueUrl = testSearchExportQueueUrl + + jest.spyOn(getEarthdataConfig, 'getEarthdataConfig').mockImplementationOnce(() => ({ + graphQlHost: 'https://graphql.example.com' + })) + + const format = 'csv' + + const event = { + body: JSON.stringify({ + data: { + format, + variables: {}, + query: {} + }, + requestId: 'asdf-1234-qwer-5678' + }) + } + + const result = await exportSearch(event, {}) + + const expectedKey = '66241fe6c79c644cfc52b7f39644f5b7394ce1f30d4a0dd4b2237c8ca669ddee'; + + expect(result.body).toEqual(`{"key":"${expectedKey}"}`) + + const { Messages } = await sqs.receiveMessage({ QueueUrl: testSearchExportQueueUrl }).promise() + expect(Messages).toHaveLength(1) + + const message = JSON.parse(Messages[0].Body); + + expect(message).toEqual({ + params: { + format, + query: {}, + variables: {} + }, + extra: { + earthdataEnvironment: "prod", + jwt: 'mockJwt', + key: expectedKey, + requestId: 'asdf-1234-qwer-5678' + } + }) + }) + + test('returns json response correctly', async () => { + process.env.searchExportQueueUrl = testSearchExportQueueUrl + + jest.spyOn(getEarthdataConfig, 'getEarthdataConfig').mockImplementationOnce(() => ({ + graphQlHost: 'https://graphql.example.com' + })) + + const format = 'json' + const event = { + body: JSON.stringify({ + data: { + format, + variables: {}, + query: {} + }, + requestId: 'asdf-1234-qwer-5678' + }) + } + + const result = await exportSearch(event, {}) + + expect(result.body).toEqual("{\"key\":\"6d5ae367c8c99d6bdf0fe7c4bfb56fc5306991c59a0d6c316386598f6711716b\"}") + + const { Messages } = await sqs.receiveMessage({ QueueUrl: testSearchExportQueueUrl }).promise() + expect(Messages).toHaveLength(1) + + const message = JSON.parse(Messages[0].Body); + expect(message).toEqual({ + extra: { + earthdataEnvironment: "prod", + key: "6d5ae367c8c99d6bdf0fe7c4bfb56fc5306991c59a0d6c316386598f6711716b", + jwt: "mockJwt", + requestId: "asdf-1234-qwer-5678" + }, + params: { + format: format, + query: {}, + variables: {} + } + }) + }) + + test('responds correctly on malformed input', async () => { + process.env.searchExportQueueUrl = testSearchExportQueueUrl + + jest.spyOn(getEarthdataConfig, 'getEarthdataConfig').mockImplementationOnce(() => ({ + graphQlHost: 'https://graphql.example.com' + })) + + const event = { + body: "" + } + + const response = await exportSearch(event, {}) + + expect(response.statusCode).toEqual(500) + + const { body } = response + const parsedBody = JSON.parse(body) + const { errors } = parsedBody + const [errorMessage] = errors + + expect(errorMessage).toEqual('SyntaxError: Unexpected end of JSON input') + + // retrieve message from queue + const { Messages = [] } = await sqs.receiveMessage({ QueueUrl: testSearchExportQueueUrl }).promise() + + // check that no message was created + expect(Messages).toHaveLength(0) + }) +}) diff --git a/serverless/src/exportSearchRequest/handler.js b/serverless/src/exportSearchRequest/handler.js new file mode 100644 index 0000000000..09e9c3624e --- /dev/null +++ b/serverless/src/exportSearchRequest/handler.js @@ -0,0 +1,98 @@ +import { createHash } from 'node:crypto' + +import AWS from 'aws-sdk' + +import { determineEarthdataEnvironment } from '../util/determineEarthdataEnvironment' +import { getApplicationConfig } from '../../../sharedUtils/config' +import { getJwtToken } from '../util/getJwtToken' +import { getSqsConfig } from '../util/aws/getSqsConfig' +import { parseError } from '../../../sharedUtils/parseError' + +// adapter for Amazon Simple Queue Service (SQS) +let sqs + +/** + * @description Request an export from cmr-graphql + * @param {Object} event Details about the HTTP request that it received + */ +const exportSearchRequest = async (event, context) => { + // https://stackoverflow.com/questions/49347210/why-aws-lambda-keeps-timing-out-when-using-knex-js + // eslint-disable-next-line no-param-reassign + context.callbackWaitsForEmptyEventLoop = false + + sqs ??= new AWS.SQS(getSqsConfig()) + + const { body, headers } = event + + const { defaultResponseHeaders } = getApplicationConfig() + + const earthdataEnvironment = determineEarthdataEnvironment(headers) + + const jwt = getJwtToken(event) + + try { + const { data, requestId } = JSON.parse(body) + + if (!requestId) throw Error("missing requestId"); + + const { columns, cursorpath, format = 'json', itempath, query, variables } = data + + if (!['csv', 'json'].includes(format)) throw Error("invalid format"); + + const params = { + columns, + cursorpath, + format, + itempath, + query, + variables + } + + // hash the params + const key = createHash('sha256').update(JSON.stringify(Object.entries(params))).digest('hex'); + + const filename = `search_results_export_${key.substring(0, 5)}.${format}` + + const extra = { + earthdataEnvironment, + key, + filename, + jwt, + requestId + } + + const message = { params, extra } + + const messageBody = JSON.stringify(message); + + if (!process.env.IS_OFFLINE) { + await sqs.sendMessage({ + QueueUrl: process.env.searchExportQueueUrl, + MessageBody: messageBody + }).promise() + } + + // maybe have a URL param to directly invoke the processing lambda instead of posting to a queue + // this would be done for local development + + return { + isBase64Encoded: false, + statusCode: 200, + headers: { + ...defaultResponseHeaders, + 'jwt-token': jwt + }, + body: JSON.stringify({ + key + }) + } + } catch (e) { + return { + isBase64Encoded: false, + headers: defaultResponseHeaders, + ...parseError(e) + } + } +} + +export default exportSearchRequest From 59018aad2113e188bcf118bd321441d405e3c7d1 Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Mon, 13 Feb 2023 17:07:45 -0500 Subject: [PATCH 04/43] EDSC-3612: added deleteBucket.js --- serverless/src/util/aws/deleteBucket.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 serverless/src/util/aws/deleteBucket.js diff --git a/serverless/src/util/aws/deleteBucket.js b/serverless/src/util/aws/deleteBucket.js new file mode 100644 index 0000000000..12889ccf62 --- /dev/null +++ b/serverless/src/util/aws/deleteBucket.js @@ -0,0 +1,19 @@ +/** + * @name deleteBucket + * @param {Object} s3 - instance of AWS.S3 from 'aws-sdk' + * @param {String} bucketName + * @description in order to delete a bucket from S3, you first have to + * delete all the items in the bucket + */ +export default async function deleteBucket(s3, bucketName) { + const { Contents = [] } = await s3.listObjects({ Bucket: bucketName }).promise(); + + await s3.deleteObjects({ + Bucket: bucketName, + Delete: { + Objects: Contents.map(({ Key }) => ({ Key })) + } + }).promise() + + await s3.deleteBucket({ Bucket: bucketName }).promise() +} \ No newline at end of file From b1aa728e7bafb098478c0a79a66951818632206f Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Mon, 13 Feb 2023 17:11:12 -0500 Subject: [PATCH 05/43] EDSC-3612: refactored exportSearch to only work when triggered by SQS message --- .../exportSearch/__tests__/handler.test.js | 186 +++++++++++------ serverless/src/exportSearch/handler.js | 188 ++++++++++-------- 2 files changed, 227 insertions(+), 147 deletions(-) diff --git a/serverless/src/exportSearch/__tests__/handler.test.js b/serverless/src/exportSearch/__tests__/handler.test.js index 1a5a25112e..11e304fcab 100644 --- a/serverless/src/exportSearch/__tests__/handler.test.js +++ b/serverless/src/exportSearch/__tests__/handler.test.js @@ -1,22 +1,73 @@ +import AWS from 'aws-sdk' import nock from 'nock' -import * as deployedEnvironment from '../../../../sharedUtils/deployedEnvironment' -import * as getJwtToken from '../../util/getJwtToken' -import * as getEchoToken from '../../util/urs/getEchoToken' import * as getEarthdataConfig from '../../../../sharedUtils/config' +import deleteBucket from '../../util/aws/deleteBucket' import exportSearch from '../handler' +const OLD_ENV = process.env + +const AWS_TEST_REGION = 'us-east-1' +const S3_TEST_PORT = 5000 +const S3_TEST_HOST = `0.0.0.0:${S3_TEST_PORT}` +const S3_TEST_LOCALHOST = `localhost:${S3_TEST_PORT}` +const S3_TEST_BUCKET_NAME = 'S3_TEST_BUCKET_NAME' +const S3_TEST_ENDPOINT = `http://${S3_TEST_HOST}` +const MOCK_REQUEST_ID = 'MOCK_REQUEST_ID' + +// need to configure here because the aws-sdk expects it +// without it, the handler will throw an error +AWS.config.update({ + accessKeyId: Math.random().toString(), // this will be ignored + secretAccessKey: Math.random().toString(), // this will be ignored + region: AWS_TEST_REGION +}) + +const s3 = new AWS.S3({ endpoint: S3_TEST_ENDPOINT }) + +beforeAll(async () => { + // explicitly allow network connections to local mock S3 server + nock.enableNetConnect(host => host === S3_TEST_HOST || host === S3_TEST_LOCALHOST) + + await s3.createBucket({ + Bucket: S3_TEST_BUCKET_NAME, + CreateBucketConfiguration: { + LocationConstraint: "antarctica" + } + }).promise(); +}) + +afterAll(async () => { + await deleteBucket(s3, S3_TEST_BUCKET_NAME) + + // re-disable all network connections, including those to localhost and 0.0.0.0 + nock.disableNetConnect() +}) + beforeEach(() => { jest.clearAllMocks() - jest.spyOn(deployedEnvironment, 'deployedEnvironment').mockImplementation(() => 'prod') - jest.spyOn(getJwtToken, 'getJwtToken').mockImplementation(() => 'mockJwt') - jest.spyOn(getEchoToken, 'getEchoToken').mockImplementation(() => '1234-abcd-5678-efgh') + // Manage resetting ENV variables + // TODO: This is causing problems with mocking knex but is noted as important for managing process.env + // jest.resetModules() + process.env = { ...OLD_ENV } + delete process.env.NODE_ENV +}) + +afterEach(() => { + // Restore any ENV variables overwritten in tests + process.env = OLD_ENV + + // just in case, for safety + jest.clearAllMocks() }) describe('exportSearch', () => { test('returns csv response correctly', async () => { + process.env.searchExportS3Endpoint = S3_TEST_ENDPOINT + process.env.searchExportBucket = S3_TEST_BUCKET_NAME + jest.spyOn(getEarthdataConfig, 'getEarthdataConfig').mockImplementationOnce(() => ({ graphQlHost: 'https://graphql.example.com' })) @@ -51,23 +102,49 @@ describe('exportSearch', () => { } }) + const key = 'mock-csv-hash-key-123456789' + const event = { - body: JSON.stringify({ - data: { - format: 'csv', - variables: {}, - query: {} - }, - requestId: 'asdf-1234-qwer-5678' - }) + Records: [{ + body: JSON.stringify({ + params: { + columns: [ + { name: 'Data Provider', path: 'provider' }, + { name: 'Short Name', path: 'shortName' }, + { name: 'Version', path: 'versionId' }, + { name: 'Entry Title', path: 'title' }, + { name: 'Processing Level', path: 'processingLevelId' }, + { name: 'Platform', path: 'platforms.shortName' }, + { name: 'Start Time', path: 'timeStart' }, + { name: 'End Time', path: 'timeEnd' } + ], + cursorpath: 'collections.cursor', + format: 'csv', + itempath: 'collections.items', + query: {}, + variables: {} + }, + extra: { + earthdataEnvironment: 'dev', + filename: 'test-export-search-results-12345', + key, + requestId: MOCK_REQUEST_ID + } + }) + }] } - const result = await exportSearch(event, {}) + await exportSearch(event, {}) - expect(result.body).toEqual('Data Provider,Short Name,Version,Entry Title,Processing Level,Platform,Start Time,End Time\r\n,,,"Test collection",,"platform",,\r\n,,,"Test collection 1",,"platform",,\r\n') + const obj = await s3.getObject({ Bucket: S3_TEST_BUCKET_NAME, Key: key }).promise() + expect(obj.ContentType).toEqual('text/csv'); + expect(obj.Body.toString()).toEqual('Data Provider,Short Name,Version,Entry Title,Processing Level,Platform,Start Time,End Time\r\n,,,Test collection,,platform,,\r\n,,,Test collection 1,,platform,,\r\n') }) test('returns json response correctly', async () => { + process.env.searchExportS3Endpoint = S3_TEST_ENDPOINT + process.env.searchExportBucket = S3_TEST_BUCKET_NAME + jest.spyOn(getEarthdataConfig, 'getEarthdataConfig').mockImplementationOnce(() => ({ graphQlHost: 'https://graphql.example.com' })) @@ -102,55 +179,42 @@ describe('exportSearch', () => { } }) - const event = { - body: JSON.stringify({ - data: { - format: 'json', - variables: {}, - query: {} - }, - requestId: 'asdf-1234-qwer-5678' - }) - } - - const result = await exportSearch(event, {}) - - expect(result.body).toEqual('[{"conceptId":"C100000-EDSC","title":"Test collection","platforms":[{"shortName":"platform"}]},{"conceptId":"C100001-EDSC","title":"Test collection 1","platforms":[{"shortName":"platform"}]}]') - }) - - test('responds correctly on http error', async () => { - jest.spyOn(getEarthdataConfig, 'getEarthdataConfig').mockImplementationOnce(() => ({ - graphQlHost: 'https://graphql.example.com' - })) - - nock(/graphql/) - .post(/api/) - .reply(500, { - errors: [ - 'Test error message' - ] - }) + const key = 'mock-json-hash-key-123456789' const event = { - body: JSON.stringify({ - data: { - format: 'json', - variables: {}, - query: {} - }, - requestId: 'asdf-1234-qwer-5678' - }) + Records: [{ + body: JSON.stringify({ + params: { + columns: [ + { name: 'Data Provider', path: 'provider' }, + { name: 'Short Name', path: 'shortName' }, + { name: 'Version', path: 'versionId' }, + { name: 'Entry Title', path: 'title' }, + { name: 'Processing Level', path: 'processingLevelId' }, + { name: 'Platform', path: 'platforms.shortName' }, + { name: 'Start Time', path: 'timeStart' }, + { name: 'End Time', path: 'timeEnd' } + ], + cursorpath: 'collections.cursor', + format: 'json', + itempath: 'collections.items', + query: {}, + variables: {} + }, + extra: { + earthdataEnvironment: 'dev', + filename: 'test-export-search-results-12345', + key, + requestId: MOCK_REQUEST_ID + } + }) + }] } - const response = await exportSearch(event, {}) - - expect(response.statusCode).toEqual(500) - - const { body } = response - const parsedBody = JSON.parse(body) - const { errors } = parsedBody - const [errorMessage] = errors + await exportSearch(event, {}) - expect(errorMessage).toEqual('Test error message') + const obj = await s3.getObject({ Bucket: S3_TEST_BUCKET_NAME, Key: key }).promise() + expect(obj.ContentType).toEqual('application/json'); + expect(JSON.parse(obj.Body.toString())).toEqual([{"conceptId":"C100000-EDSC","title":"Test collection","platforms":[{"shortName":"platform"}]},{"conceptId":"C100001-EDSC","title":"Test collection 1","platforms":[{"shortName":"platform"}]}]) }) }) diff --git a/serverless/src/exportSearch/handler.js b/serverless/src/exportSearch/handler.js index 481e856332..e4e62359c0 100644 --- a/serverless/src/exportSearch/handler.js +++ b/serverless/src/exportSearch/handler.js @@ -1,16 +1,18 @@ +import crypto from 'node:crypto' + +import AWS from 'aws-sdk' import axios from 'axios' +import { get } from 'sendero'; -import { determineEarthdataEnvironment } from '../util/determineEarthdataEnvironment' -import { getApplicationConfig, getEarthdataConfig } from '../../../sharedUtils/config' -import { getJwtToken } from '../util/getJwtToken' -import { parseError } from '../../../sharedUtils/parseError' -import { getEchoToken } from '../util/urs/getEchoToken' -import { prepareExposeHeaders } from '../util/cmr/prepareExposeHeaders' -import { jsonToCsv } from './jsonToCsv' +import { getEarthdataConfig } from '../../../sharedUtils/config' +import { jsonToCsv } from '../util/jsonToCsv' import { wrapAxios } from '../util/wrapAxios' const wrappedAxios = wrapAxios(axios) +// AWS S3 adapter +let s3 + /** * Perform a loop through collection results and return the full results in the requested format. * @param {Object} event Details about the HTTP request that it received @@ -20,105 +22,119 @@ const exportSearch = async (event, context) => { // eslint-disable-next-line no-param-reassign context.callbackWaitsForEmptyEventLoop = false - const { body, headers } = event + // we set the concurrency to 1, so we only process one at a time + // this way, if there's an issue, we can retry only the one that failed + const { Records: [sqsRecord] } = event - const { defaultResponseHeaders } = getApplicationConfig() + if (!sqsRecord) return - const { data, requestId } = JSON.parse(body) + const { body } = sqsRecord - const { format, variables, query } = data + const { extra, params } = JSON.parse(body) - const earthdataEnvironment = determineEarthdataEnvironment(headers) + const { earthdataEnvironment, filename, key, jwt, requestId } = extra + if (!filename) throw new Error("missing filename") + if (!requestId) throw new Error("missing requestId") + // create request header for X-Request-Id + // so we can track a request across applications const requestHeaders = { 'X-Request-Id': requestId } - const jwtToken = getJwtToken(event) - if (jwtToken) { + if (jwt) { // Support endpoints that have optional authentication - const token = await getEchoToken(jwtToken, earthdataEnvironment) + const token = await getEchoToken(jwt, earthdataEnvironment) requestHeaders.Authorization = `Bearer ${token}` } const { graphQlHost } = getEarthdataConfig(earthdataEnvironment) - const graphQlUrl = `${graphQlHost}/api` + const { columns, cursorpath, format, itempath, query, variables } = params; - try { - let cursor - let finished = false - const returnItems = [] - let status - let responseHeaders - - // Loop until the request comes back with no items - while (!finished) { - // We need this await inside the loop because we have to wait on the response from the previous - // call before making the next request - // eslint-disable-next-line no-await-in-loop - const response = await wrappedAxios({ - url: graphQlUrl, - method: 'post', - data: { - query, - variables: { - ...variables, - cursor - } - }, - headers: requestHeaders - }) - - const { - data: responseData, - config - } = response; - ({ headers: responseHeaders, status } = response) - - const { elapsedTime } = config - - const { collections = {} } = responseData.data - const { cursor: responseCursor, items = [] } = collections - - console.log(`Request for ${items.length} exportSearch collections successfully completed in ${elapsedTime} ms`) - - // Set the cursor returned from GraphQl so the next loop will use it - cursor = responseCursor - - // If there are no items returned, set finished to true to exit the loop - if (!items || !items.length) { - finished = true - break - } - - // Push the items returned onto the returnItems array - returnItems.push(...items) - } + const graphQlUrl = `${graphQlHost}/api` - // Format the returnItems into the requested format - let returnBody = null - if (format === 'json') returnBody = JSON.stringify(returnItems) - if (format === 'csv') returnBody = jsonToCsv(returnItems) - - return { - isBase64Encoded: false, - statusCode: status, - headers: { - ...defaultResponseHeaders, - 'access-control-allow-origin': responseHeaders['access-control-allow-origin'], - 'access-control-expose-headers': prepareExposeHeaders(responseHeaders), - 'jwt-token': jwtToken + let cursor + let finished = false + const returnItems = [] + let responseHeaders + + // Loop until the request comes back with no items + while (!finished) { + // We need this await inside the loop because we have to wait on the response from the previous + // call before making the next request + // eslint-disable-next-line no-await-in-loop + const response = await wrappedAxios({ + url: graphQlUrl, + method: 'post', + data: { + query, + variables: { + ...variables, + cursor + } }, - body: returnBody - } - } catch (e) { - return { - isBase64Encoded: false, - headers: defaultResponseHeaders, - ...parseError(e) + headers: requestHeaders + }) + + const { + data: responseData, + config + } = response; + ({ headers: responseHeaders, status } = response) + + const { elapsedTime } = config + + const { data } = responseData; + + const responseCursor = get(data, cursorpath)[0]; + + const items = get(data, itempath, { clean: true }) + + console.log(`Request for ${items.length} exportSearch collections successfully completed in ${elapsedTime} ms`) + + // Set the cursor returned from GraphQl so the next loop will use it + cursor = responseCursor + + // If there are no items returned, set finished to true to exit the loop + if (!items || !items.length) { + finished = true + break } + + // Push the items returned onto the returnItems array + returnItems.push(...items) + } + + // Format the returnItems into the requested format + let returnBody, contentType + if (format === 'json') { + returnBody = JSON.stringify(returnItems) + contentType = 'application/json' + } else if (format === 'csv') { + returnBody = jsonToCsv(returnItems, columns) + contentType = 'text/csv' + } else { + throw new Error("invalid format") + } + + s3 ??= new AWS.S3({ + endpoint: process.env.searchExportS3Endpoint + }) + + if (!process.env.IS_OFFLINE) { + await s3.upload({ + Bucket: process.env.searchExportBucket, + Key: key, + Body: returnBody, + ACL: 'authenticated-read', + ContentDisposition: `attachment; filename="${filename}"`, + ContentType: contentType, + + // Content-MD5 recommended by https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#putObject-property + ContentMD5: crypto.createHash("md5").update(returnBody).digest("base64") + }).promise() } } From 2831db4692991a98d1882b102b0eaab8ecb884bd Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Mon, 13 Feb 2023 17:12:14 -0500 Subject: [PATCH 06/43] EDSC-3612: added papaparse and sendero to dependencies --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index a348405a05..84f50dec48 100644 --- a/package.json +++ b/package.json @@ -144,6 +144,7 @@ "node-pre-gyp": "^0.12.0", "node-sass": "^8.0.0", "number-abbreviate": "^2.0.0", + "papaparse": "^5.3.2", "pg": "^8.7.3", "postcss": "^8.4.19", "postcss-cli": "^10.0.0", @@ -188,6 +189,7 @@ "sanitize-html": "^2.7.0", "sass-loader": "^13.2.0", "scatter-swap": "^0.1.0", + "sendero": "^0.1.0", "serverless": "^3.24.1", "serverless-finch": "^4.0.0", "serverless-offline": "^11.3.0", From ba410425b0e709e65a6f781af9587dcec5a27c9e Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Tue, 14 Feb 2023 11:26:32 -0500 Subject: [PATCH 07/43] EDSC-3612: added database support to exportSearchRequest --- .../__tests__/handler.test.js | 57 +++++++++++++++++++ serverless/src/exportSearchRequest/handler.js | 20 ++++++- 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/serverless/src/exportSearchRequest/__tests__/handler.test.js b/serverless/src/exportSearchRequest/__tests__/handler.test.js index 8df7cf48ac..3076ab06da 100644 --- a/serverless/src/exportSearchRequest/__tests__/handler.test.js +++ b/serverless/src/exportSearchRequest/__tests__/handler.test.js @@ -1,9 +1,12 @@ import AWS from 'aws-sdk' import MockDate from 'mockdate' import nock from 'nock' +import { newDb } from "pg-mem"; import * as deployedEnvironment from '../../../../sharedUtils/deployedEnvironment' +import * as getDbConnection from '../../util/database/getDbConnection' import * as getJwtToken from '../../util/getJwtToken' +import * as getVerifiedJwtToken from '../../util/getVerifiedJwtToken' import * as getEchoToken from '../../util/urs/getEchoToken' import * as getEarthdataConfig from '../../../../sharedUtils/config' @@ -18,6 +21,7 @@ const SQS_TEST_LOCALHOST = `localhost:${SQS_TEST_PORT}` const SQS_TEST_QUEUE_NAME = 'REQUEST_SEARCH_EXPORT_TEST_QUEUE' const SQS_TEST_ENDPOINT = `http://${SQS_TEST_HOST}` const MOCK_ECHO_TOKEN = '1234-abcd-5678-efgh' +const MOCK_USER_ID = 1234 // need to configure here because the aws-sdk expects it // without it, the handler will throw an error @@ -29,6 +33,8 @@ AWS.config.update({ const sqs = new AWS.SQS({ endpoint: SQS_TEST_ENDPOINT }) +let mockDb, mockDbConnection + let testSearchExportQueueUrl beforeAll(async () => { // explicitly allow network connections to ElasticMQ (SQS-Compatible) server @@ -39,6 +45,24 @@ beforeAll(async () => { // we save the url here, so we can pass it to the handler via an environmental variable testSearchExportQueueUrl = QueueUrl + + // create test database + mockDb = newDb() + + // create exports table + mockDb.public.none(`create table exports ( + user_id integer, + request_id varchar(1000), + state varchar(1000), + filename varchar(1000), + updated_at timestamp NOT NULL DEFAULT NOW(), + created_at timestamp NOT NULL DEFAULT NOW() + )`) + + // create mock database connection + // it's asynchronous and getConnection is synchronous, + // so we have to intialize it outside the mock implementation call + mockDbConnection = await mockDb.adapters.createKnex() }) afterAll(async () => { @@ -52,7 +76,9 @@ beforeEach(async () => { jest.clearAllMocks() jest.spyOn(deployedEnvironment, 'deployedEnvironment').mockImplementation(() => 'prod') + jest.spyOn(getDbConnection, 'getDbConnection').mockImplementationOnce(() => mockDbConnection) jest.spyOn(getJwtToken, 'getJwtToken').mockImplementation(() => 'mockJwt') + jest.spyOn(getVerifiedJwtToken, 'getVerifiedJwtToken').mockImplementation(() => ({ id: MOCK_USER_ID })) // Manage resetting ENV variables // TODO: This is causing problems with mocking knex but is noted as important for managing process.env @@ -72,6 +98,9 @@ afterEach(() => { // reset hacks on built-ins MockDate.reset() jest.spyOn(global.Math, 'random').mockRestore(); + + // clear in-memory database table + mockDb.public.none('DELETE FROM exports') }) describe('exportSearch', () => { @@ -114,11 +143,23 @@ describe('exportSearch', () => { }, extra: { earthdataEnvironment: "prod", + filename: "search_results_export_66241fe6c7.csv", jwt: 'mockJwt', key: expectedKey, requestId: 'asdf-1234-qwer-5678' } }) + + // check if the correct information was saved to the database + const databaseTableRows = await mockDbConnection('exports') + expect(databaseTableRows).toEqual([{ + filename: 'search_results_export_66241fe6c7.csv', + request_id: '66241fe6c79c644cfc52b7f39644f5b7394ce1f30d4a0dd4b2237c8ca669ddee', + state: 'REQUESTED', + user_id: MOCK_USER_ID, + updated_at: new Date('1988-09-03T10:00:00.000Z'), + created_at: new Date('1988-09-03T10:00:00.000Z') + }]) }) test('returns json response correctly', async () => { @@ -152,6 +193,7 @@ describe('exportSearch', () => { extra: { earthdataEnvironment: "prod", key: "6d5ae367c8c99d6bdf0fe7c4bfb56fc5306991c59a0d6c316386598f6711716b", + filename: "search_results_export_6d5ae367c8.json", jwt: "mockJwt", requestId: "asdf-1234-qwer-5678" }, @@ -161,6 +203,17 @@ describe('exportSearch', () => { variables: {} } }) + + // check if the correct information was saved to the database + const databaseTableRows = await mockDbConnection('exports') + expect(databaseTableRows).toEqual([{ + filename: 'search_results_export_6d5ae367c8.json', + request_id: '6d5ae367c8c99d6bdf0fe7c4bfb56fc5306991c59a0d6c316386598f6711716b', + state: 'REQUESTED', + user_id: MOCK_USER_ID, + updated_at: new Date('1988-09-03T10:00:00.000Z'), + created_at: new Date('1988-09-03T10:00:00.000Z') + }]) }) test('responds correctly on malformed input', async () => { @@ -190,5 +243,9 @@ describe('exportSearch', () => { // check that no message was created expect(Messages).toHaveLength(0) + + // check that no information was saved to the database + const databaseTableRows = await mockDbConnection('exports') + expect(databaseTableRows).toEqual([]) }) }) diff --git a/serverless/src/exportSearchRequest/handler.js b/serverless/src/exportSearchRequest/handler.js index 09e9c3624e..7b674d4a1b 100644 --- a/serverless/src/exportSearchRequest/handler.js +++ b/serverless/src/exportSearchRequest/handler.js @@ -4,7 +4,9 @@ import AWS from 'aws-sdk' import { determineEarthdataEnvironment } from '../util/determineEarthdataEnvironment' import { getApplicationConfig } from '../../../sharedUtils/config' +import { getDbConnection } from '../util/database/getDbConnection' import { getJwtToken } from '../util/getJwtToken' +import { getVerifiedJwtToken } from '../util/getVerifiedJwtToken' import { getSqsConfig } from '../util/aws/getSqsConfig' import { parseError } from '../../../sharedUtils/parseError' @@ -26,10 +28,15 @@ const exportSearchRequest = async (event, context) => { const { defaultResponseHeaders } = getApplicationConfig() + const earthdataEnvironment = determineEarthdataEnvironment(headers) const jwt = getJwtToken(event) + const { id: userId } = getVerifiedJwtToken(jwt, earthdataEnvironment) + + if (!userId) throw Error("failed getting userId from jwt") + try { const { data, requestId } = JSON.parse(body) @@ -51,7 +58,7 @@ const exportSearchRequest = async (event, context) => { // hash the params const key = createHash('sha256').update(JSON.stringify(Object.entries(params))).digest('hex'); - const filename = `search_results_export_${key.substring(0, 5)}.${format}` + const filename = `search_results_export_${key.substring(0, 10)}.${format}` const extra = { earthdataEnvironment, @@ -66,13 +73,22 @@ const exportSearchRequest = async (event, context) => { const messageBody = JSON.stringify(message); if (!process.env.IS_OFFLINE) { + const dbConnection = await getDbConnection() + + await dbConnection('exports').insert({ + user_id: userId, + request_id: key, + state: "REQUESTED", + filename + }) + await sqs.sendMessage({ QueueUrl: process.env.searchExportQueueUrl, MessageBody: messageBody }).promise() } - // maybe have a URL param to directly invoke the processing lambda instead of posting to a queue + // maybe have a environmental variable to directly invoke the processing lambda instead of posting to a queue // this would be done for local development return { From 0246617d4916a0b49ab9d1651a148102593ee135 Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Tue, 14 Feb 2023 12:55:01 -0500 Subject: [PATCH 08/43] EDSC-3612: added pg support to exportSearch and cleaned up exportSearchRequest --- .../exportSearch/__tests__/handler.test.js | 148 +++++++++++++----- serverless/src/exportSearch/handler.js | 17 +- .../__tests__/handler.test.js | 13 +- serverless/src/exportSearchRequest/handler.js | 5 +- 4 files changed, 136 insertions(+), 47 deletions(-) diff --git a/serverless/src/exportSearch/__tests__/handler.test.js b/serverless/src/exportSearch/__tests__/handler.test.js index 11e304fcab..71dde94d09 100644 --- a/serverless/src/exportSearch/__tests__/handler.test.js +++ b/serverless/src/exportSearch/__tests__/handler.test.js @@ -1,31 +1,36 @@ -import AWS from 'aws-sdk' +import AWS, { S3 } from 'aws-sdk' import nock from 'nock' +import { newDb } from "pg-mem"; import * as getEarthdataConfig from '../../../../sharedUtils/config' +import * as getDbConnection from '../../util/database/getDbConnection' import deleteBucket from '../../util/aws/deleteBucket' import exportSearch from '../handler' const OLD_ENV = process.env -const AWS_TEST_REGION = 'us-east-1' +const MOCK_REGION = 'antarctica' const S3_TEST_PORT = 5000 const S3_TEST_HOST = `0.0.0.0:${S3_TEST_PORT}` const S3_TEST_LOCALHOST = `localhost:${S3_TEST_PORT}` const S3_TEST_BUCKET_NAME = 'S3_TEST_BUCKET_NAME' const S3_TEST_ENDPOINT = `http://${S3_TEST_HOST}` const MOCK_REQUEST_ID = 'MOCK_REQUEST_ID' +const MOCK_USER_ID = 1234 // need to configure here because the aws-sdk expects it // without it, the handler will throw an error AWS.config.update({ accessKeyId: Math.random().toString(), // this will be ignored secretAccessKey: Math.random().toString(), // this will be ignored - region: AWS_TEST_REGION + region: MOCK_REGION }) const s3 = new AWS.S3({ endpoint: S3_TEST_ENDPOINT }) +let mockDb, mockDbConnection + beforeAll(async () => { // explicitly allow network connections to local mock S3 server nock.enableNetConnect(host => host === S3_TEST_HOST || host === S3_TEST_LOCALHOST) @@ -33,9 +38,27 @@ beforeAll(async () => { await s3.createBucket({ Bucket: S3_TEST_BUCKET_NAME, CreateBucketConfiguration: { - LocationConstraint: "antarctica" + LocationConstraint: MOCK_REGION } }).promise(); + + // create test database + mockDb = newDb() + + // create exports table + mockDb.public.none(`create table exports ( + user_id integer, + key varchar(1000), + state varchar(1000), + filename varchar(1000), + updated_at timestamp NOT NULL DEFAULT NOW(), + created_at timestamp NOT NULL DEFAULT NOW() + )`) + + // create mock database connection + // it's asynchronous and getConnection is synchronous, + // so we have to intialize it outside the mock implementation call + mockDbConnection = await mockDb.adapters.createKnex() }) afterAll(async () => { @@ -48,6 +71,8 @@ afterAll(async () => { beforeEach(() => { jest.clearAllMocks() + jest.spyOn(getDbConnection, 'getDbConnection').mockImplementationOnce(() => mockDbConnection) + // Manage resetting ENV variables // TODO: This is causing problems with mocking knex but is noted as important for managing process.env // jest.resetModules() @@ -61,6 +86,9 @@ afterEach(() => { // just in case, for safety jest.clearAllMocks() + + // clear in-memory database table + mockDb.public.none('DELETE FROM exports') }) describe('exportSearch', () => { @@ -108,37 +136,59 @@ describe('exportSearch', () => { Records: [{ body: JSON.stringify({ params: { - columns: [ - { name: 'Data Provider', path: 'provider' }, - { name: 'Short Name', path: 'shortName' }, - { name: 'Version', path: 'versionId' }, - { name: 'Entry Title', path: 'title' }, - { name: 'Processing Level', path: 'processingLevelId' }, - { name: 'Platform', path: 'platforms.shortName' }, - { name: 'Start Time', path: 'timeStart' }, - { name: 'End Time', path: 'timeEnd' } - ], - cursorpath: 'collections.cursor', - format: 'csv', - itempath: 'collections.items', - query: {}, - variables: {} + columns: [ + { name: 'Data Provider', path: 'provider' }, + { name: 'Short Name', path: 'shortName' }, + { name: 'Version', path: 'versionId' }, + { name: 'Entry Title', path: 'title' }, + { name: 'Processing Level', path: 'processingLevelId' }, + { name: 'Platform', path: 'platforms.shortName' }, + { name: 'Start Time', path: 'timeStart' }, + { name: 'End Time', path: 'timeEnd' } + ], + cursorpath: 'collections.cursor', + format: 'csv', + itempath: 'collections.items', + query: {}, + variables: {} }, extra: { earthdataEnvironment: 'dev', filename: 'test-export-search-results-12345', key, - requestId: MOCK_REQUEST_ID + requestId: MOCK_REQUEST_ID, + userId: MOCK_USER_ID } }) }] } + // seed database + await mockDbConnection('exports').insert({ + created_at: new Date('1988-09-03T10:00:00.000Z'), + filename: 'search_results_export_66241fe6c7.csv', + key, + state: 'REQUESTED', + updated_at: new Date('1988-09-03T10:00:00.000Z'), + user_id: MOCK_USER_ID + }) + await exportSearch(event, {}) const obj = await s3.getObject({ Bucket: S3_TEST_BUCKET_NAME, Key: key }).promise() expect(obj.ContentType).toEqual('text/csv'); expect(obj.Body.toString()).toEqual('Data Provider,Short Name,Version,Entry Title,Processing Level,Platform,Start Time,End Time\r\n,,,Test collection,,platform,,\r\n,,,Test collection 1,,platform,,\r\n') + + // check if the correct information was saved to the database + const databaseTableRows = await mockDbConnection('exports') + expect(databaseTableRows).toEqual([{ + created_at: new Date('1988-09-03T10:00:00.000Z'), + filename: 'search_results_export_66241fe6c7.csv', + key, + state: 'DONE', + updated_at: new Date('1988-09-03T10:00:00.000Z'), + user_id: MOCK_USER_ID + }]) }) test('returns json response correctly', async () => { @@ -180,41 +230,63 @@ describe('exportSearch', () => { }) const key = 'mock-json-hash-key-123456789' + const filename = 'search_results_export_66241fe6c7.json' const event = { Records: [{ body: JSON.stringify({ params: { - columns: [ - { name: 'Data Provider', path: 'provider' }, - { name: 'Short Name', path: 'shortName' }, - { name: 'Version', path: 'versionId' }, - { name: 'Entry Title', path: 'title' }, - { name: 'Processing Level', path: 'processingLevelId' }, - { name: 'Platform', path: 'platforms.shortName' }, - { name: 'Start Time', path: 'timeStart' }, - { name: 'End Time', path: 'timeEnd' } - ], - cursorpath: 'collections.cursor', - format: 'json', - itempath: 'collections.items', - query: {}, - variables: {} + columns: [ + { name: 'Data Provider', path: 'provider' }, + { name: 'Short Name', path: 'shortName' }, + { name: 'Version', path: 'versionId' }, + { name: 'Entry Title', path: 'title' }, + { name: 'Processing Level', path: 'processingLevelId' }, + { name: 'Platform', path: 'platforms.shortName' }, + { name: 'Start Time', path: 'timeStart' }, + { name: 'End Time', path: 'timeEnd' } + ], + cursorpath: 'collections.cursor', + format: 'json', + itempath: 'collections.items', + query: {}, + variables: {} }, extra: { earthdataEnvironment: 'dev', - filename: 'test-export-search-results-12345', + filename, key, - requestId: MOCK_REQUEST_ID + requestId: MOCK_REQUEST_ID, + userId: MOCK_USER_ID } }) }] } + // seed database + await mockDbConnection('exports').insert({ + created_at: new Date('1988-09-03T10:00:00.000Z'), + filename, + key, + state: 'REQUESTED', + updated_at: new Date('1988-09-03T10:00:00.000Z'), + user_id: MOCK_USER_ID + }) + await exportSearch(event, {}) const obj = await s3.getObject({ Bucket: S3_TEST_BUCKET_NAME, Key: key }).promise() expect(obj.ContentType).toEqual('application/json'); - expect(JSON.parse(obj.Body.toString())).toEqual([{"conceptId":"C100000-EDSC","title":"Test collection","platforms":[{"shortName":"platform"}]},{"conceptId":"C100001-EDSC","title":"Test collection 1","platforms":[{"shortName":"platform"}]}]) + expect(JSON.parse(obj.Body.toString())).toEqual([{ "conceptId": "C100000-EDSC", "title": "Test collection", "platforms": [{ "shortName": "platform" }] }, { "conceptId": "C100001-EDSC", "title": "Test collection 1", "platforms": [{ "shortName": "platform" }] }]) + + const databaseTableRows = await mockDbConnection('exports') + expect(databaseTableRows).toEqual([{ + filename, + key, + state: 'DONE', + user_id: MOCK_USER_ID, + updated_at: new Date('1988-09-03T10:00:00.000Z'), + created_at: new Date('1988-09-03T10:00:00.000Z') + }]) }) }) diff --git a/serverless/src/exportSearch/handler.js b/serverless/src/exportSearch/handler.js index e4e62359c0..82c92d054b 100644 --- a/serverless/src/exportSearch/handler.js +++ b/serverless/src/exportSearch/handler.js @@ -5,6 +5,7 @@ import axios from 'axios' import { get } from 'sendero'; import { getEarthdataConfig } from '../../../sharedUtils/config' +import { getDbConnection } from '../util/database/getDbConnection' import { jsonToCsv } from '../util/jsonToCsv' import { wrapAxios } from '../util/wrapAxios' @@ -22,6 +23,8 @@ const exportSearch = async (event, context) => { // eslint-disable-next-line no-param-reassign context.callbackWaitsForEmptyEventLoop = false + const dbConnection = await getDbConnection() + // we set the concurrency to 1, so we only process one at a time // this way, if there's an issue, we can retry only the one that failed const { Records: [sqsRecord] } = event @@ -32,9 +35,17 @@ const exportSearch = async (event, context) => { const { extra, params } = JSON.parse(body) - const { earthdataEnvironment, filename, key, jwt, requestId } = extra + const { earthdataEnvironment, filename, key, jwt, requestId, userId } = extra if (!filename) throw new Error("missing filename") + if (!key) throw new Error("missing key") if (!requestId) throw new Error("missing requestId") + if (!userId) throw new Error("missing userId") + + if ((await dbConnection('exports').where({ user_id: userId, key })).length !== 1) { + throw Error("invalid number of rows matching user_id and key") + } + + await dbConnection('exports').where({ user_id: userId, key }).update({ state: "PROCESSING" }).returning(['user_id', 'key']) // create request header for X-Request-Id // so we can track a request across applications @@ -135,6 +146,10 @@ const exportSearch = async (event, context) => { // Content-MD5 recommended by https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#putObject-property ContentMD5: crypto.createHash("md5").update(returnBody).digest("base64") }).promise() + console.log(`uploaded object to bucket "${process.env.searchExportBucket}" with key "${key}"`) + + await dbConnection('exports').where({ user_id: userId, key }).update({ state: "DONE" }) + console.log('updated export status in the database') } } diff --git a/serverless/src/exportSearchRequest/__tests__/handler.test.js b/serverless/src/exportSearchRequest/__tests__/handler.test.js index 3076ab06da..7408dd5b00 100644 --- a/serverless/src/exportSearchRequest/__tests__/handler.test.js +++ b/serverless/src/exportSearchRequest/__tests__/handler.test.js @@ -7,7 +7,6 @@ import * as deployedEnvironment from '../../../../sharedUtils/deployedEnvironmen import * as getDbConnection from '../../util/database/getDbConnection' import * as getJwtToken from '../../util/getJwtToken' import * as getVerifiedJwtToken from '../../util/getVerifiedJwtToken' -import * as getEchoToken from '../../util/urs/getEchoToken' import * as getEarthdataConfig from '../../../../sharedUtils/config' import exportSearch from '../handler' @@ -52,7 +51,7 @@ beforeAll(async () => { // create exports table mockDb.public.none(`create table exports ( user_id integer, - request_id varchar(1000), + key varchar(1000), state varchar(1000), filename varchar(1000), updated_at timestamp NOT NULL DEFAULT NOW(), @@ -146,7 +145,8 @@ describe('exportSearch', () => { filename: "search_results_export_66241fe6c7.csv", jwt: 'mockJwt', key: expectedKey, - requestId: 'asdf-1234-qwer-5678' + requestId: 'asdf-1234-qwer-5678', + userId: MOCK_USER_ID } }) @@ -154,7 +154,7 @@ describe('exportSearch', () => { const databaseTableRows = await mockDbConnection('exports') expect(databaseTableRows).toEqual([{ filename: 'search_results_export_66241fe6c7.csv', - request_id: '66241fe6c79c644cfc52b7f39644f5b7394ce1f30d4a0dd4b2237c8ca669ddee', + key: '66241fe6c79c644cfc52b7f39644f5b7394ce1f30d4a0dd4b2237c8ca669ddee', state: 'REQUESTED', user_id: MOCK_USER_ID, updated_at: new Date('1988-09-03T10:00:00.000Z'), @@ -195,7 +195,8 @@ describe('exportSearch', () => { key: "6d5ae367c8c99d6bdf0fe7c4bfb56fc5306991c59a0d6c316386598f6711716b", filename: "search_results_export_6d5ae367c8.json", jwt: "mockJwt", - requestId: "asdf-1234-qwer-5678" + requestId: "asdf-1234-qwer-5678", + userId: MOCK_USER_ID }, params: { format: format, @@ -208,7 +209,7 @@ describe('exportSearch', () => { const databaseTableRows = await mockDbConnection('exports') expect(databaseTableRows).toEqual([{ filename: 'search_results_export_6d5ae367c8.json', - request_id: '6d5ae367c8c99d6bdf0fe7c4bfb56fc5306991c59a0d6c316386598f6711716b', + key: '6d5ae367c8c99d6bdf0fe7c4bfb56fc5306991c59a0d6c316386598f6711716b', state: 'REQUESTED', user_id: MOCK_USER_ID, updated_at: new Date('1988-09-03T10:00:00.000Z'), diff --git a/serverless/src/exportSearchRequest/handler.js b/serverless/src/exportSearchRequest/handler.js index 7b674d4a1b..9020c91cba 100644 --- a/serverless/src/exportSearchRequest/handler.js +++ b/serverless/src/exportSearchRequest/handler.js @@ -65,7 +65,8 @@ const exportSearchRequest = async (event, context) => { key, filename, jwt, - requestId + requestId, + userId } const message = { params, extra } @@ -77,7 +78,7 @@ const exportSearchRequest = async (event, context) => { await dbConnection('exports').insert({ user_id: userId, - request_id: key, + key, state: "REQUESTED", filename }) From b23ef3ab5fccbf3ad2a8b532f4ed04f6f3d29439 Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Wed, 15 Feb 2023 09:41:59 -0500 Subject: [PATCH 09/43] EDSC-3612: added clearBucket.js --- serverless/src/util/aws/clearBucket.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 serverless/src/util/aws/clearBucket.js diff --git a/serverless/src/util/aws/clearBucket.js b/serverless/src/util/aws/clearBucket.js new file mode 100644 index 0000000000..2655b286d7 --- /dev/null +++ b/serverless/src/util/aws/clearBucket.js @@ -0,0 +1,22 @@ +/** + * @name clearBucket + * @param {Object} s3 - instance of AWS.S3 from 'aws-sdk' + * @param {String} bucketName + * @description delete all objects in an S3 Bucket + */ +export default async function clearBucket(s3, bucketName) { + try { + const { Contents = [] } = await s3.listObjects({ Bucket: bucketName }).promise(); + + if (Contents.length > 0) { + await s3.deleteObjects({ + Bucket: bucketName, + Delete: { + Objects: Contents.map(({ Key }) => ({ Key })) + } + }).promise() + } + } catch (error) { + throw Error(`failed to clear bucket "${bucketName}"`, { cause: error }) + } +} \ No newline at end of file From f49564212675be0407ad3a4509de82603924a43b Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Wed, 15 Feb 2023 09:43:33 -0500 Subject: [PATCH 10/43] EDSC-3612: added doesObjectExist.js to serverless/src/util/aws --- serverless/src/util/aws/doesObjectExist.js | 23 ++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 serverless/src/util/aws/doesObjectExist.js diff --git a/serverless/src/util/aws/doesObjectExist.js b/serverless/src/util/aws/doesObjectExist.js new file mode 100644 index 0000000000..9e0e12b1c3 --- /dev/null +++ b/serverless/src/util/aws/doesObjectExist.js @@ -0,0 +1,23 @@ +/** + * @name doesObjectExist + * @param {Object} s3 - instance of AWS.S3 from 'aws-sdk' + * @param {String} bucketName - name of the S3 bucket + * @param {String} key - object key + * @description check if an object exists + * see https://stackoverflow.com/questions/26726862/how-to-determine-if-object-exists-aws-s3-node-js-sdk + */ +export default async function doesObjectExist(s3, bucketName, key) { + if (!bucketName) throw Error("missing bucketName") + if (!key) throw Error("missing key") + + const params = { + Bucket: bucketName, + Key: key + } + return s3.headObject(params).promise().then( + () => true, + err => { + if (err.code === 'NotFound') return false; + else throw err + }); +} \ No newline at end of file From dbefb403a4aaec20d2c69cade94553904a3d94b5 Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Wed, 15 Feb 2023 09:45:04 -0500 Subject: [PATCH 11/43] EDSC-3612: updated deleteBucket to use clearBucket --- serverless/src/util/aws/deleteBucket.js | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/serverless/src/util/aws/deleteBucket.js b/serverless/src/util/aws/deleteBucket.js index 12889ccf62..da281ae71a 100644 --- a/serverless/src/util/aws/deleteBucket.js +++ b/serverless/src/util/aws/deleteBucket.js @@ -1,3 +1,5 @@ +import clearBucket from './clearBucket' + /** * @name deleteBucket * @param {Object} s3 - instance of AWS.S3 from 'aws-sdk' @@ -6,14 +8,7 @@ * delete all the items in the bucket */ export default async function deleteBucket(s3, bucketName) { - const { Contents = [] } = await s3.listObjects({ Bucket: bucketName }).promise(); + await clearBucket(s3, bucketName) - await s3.deleteObjects({ - Bucket: bucketName, - Delete: { - Objects: Contents.map(({ Key }) => ({ Key })) - } - }).promise() - await s3.deleteBucket({ Bucket: bucketName }).promise() -} \ No newline at end of file +} From 2a604fc30599fbe9ffd9aa3febd46e35eaa0ac53 Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Wed, 15 Feb 2023 10:40:50 -0500 Subject: [PATCH 12/43] EDSC-3612: added exportSearchCheck lambda --- .../__tests__/handler.test.js | 264 ++++++++++++++++++ serverless/src/exportSearchCheck/handler.js | 106 +++++++ 2 files changed, 370 insertions(+) create mode 100644 serverless/src/exportSearchCheck/__tests__/handler.test.js create mode 100644 serverless/src/exportSearchCheck/handler.js diff --git a/serverless/src/exportSearchCheck/__tests__/handler.test.js b/serverless/src/exportSearchCheck/__tests__/handler.test.js new file mode 100644 index 0000000000..f5babd4abf --- /dev/null +++ b/serverless/src/exportSearchCheck/__tests__/handler.test.js @@ -0,0 +1,264 @@ +import axios from 'axios' +import AWS from 'aws-sdk' +import nock from 'nock' +import { newDb } from "pg-mem" + +import * as getDbConnection from '../../util/database/getDbConnection' +import * as deployedEnvironment from '../../../../sharedUtils/deployedEnvironment' +import * as getJwtToken from '../../util/getJwtToken' +import * as getVerifiedJwtToken from '../../util/getVerifiedJwtToken' +import clearBucket from '../../util/aws/clearBucket' +import deleteBucket from '../../util/aws/deleteBucket' + + +import exportSearchCheck from '../handler' + +const OLD_ENV = process.env + +const MOCK_REGION = 'antarctica' +const S3_TEST_PORT = 5000 +const S3_TEST_HOST = `0.0.0.0:${S3_TEST_PORT}` +const S3_TEST_LOCALHOST = `localhost:${S3_TEST_PORT}` +const S3_TEST_BUCKET_NAME = 'S3_TEST_BUCKET_NAME' +const S3_TEST_ENDPOINT = `http://${S3_TEST_HOST}` + +const MOCK_EXPORT_REQUEST_KEY = '123456789abcdefg' +const MOCK_FILENAME_CSV = 'search_results_export_123456789.csv' +const MOCK_FILENAME_JSON = 'search_results_export_123456789.json' + +const CONTENT_TYPE_CSV = 'text/csv'; +const CONTENT_TYPE_JSON = 'application/json'; + +// need to configure here because the aws-sdk expects it +// without it, the handler will throw an error +AWS.config.update({ + accessKeyId: Math.random().toString(), // this will be ignored + secretAccessKey: Math.random().toString(), // this will be ignored + region: MOCK_REGION +}) + +const s3 = new AWS.S3({ endpoint: S3_TEST_ENDPOINT }) + +let mockDb, mockDbConnection + +beforeAll(async () => { + // explicitly allow network connections to local mock S3 server + nock.enableNetConnect(host => host === S3_TEST_HOST || host === S3_TEST_LOCALHOST) + + // see http://docs.getmoto.org/en/latest/docs/server_mode.html#reset-api + axios.post(`${S3_TEST_ENDPOINT}/moto-api/reset`) + + await s3.createBucket({ + Bucket: S3_TEST_BUCKET_NAME, + CreateBucketConfiguration: { + LocationConstraint: MOCK_REGION + } + }).promise(); + + // create test database + mockDb = newDb() + + // create exports table + mockDb.public.none(`create table exports ( + user_id integer, + key varchar(1000), + state varchar(1000), + filename varchar(1000), + updated_at timestamp NOT NULL DEFAULT NOW(), + created_at timestamp NOT NULL DEFAULT NOW() + )`) + + // create mock database connection + // it's asynchronous and getConnection is synchronous, + // so we have to intialize it outside the mock implementation call + mockDbConnection = await mockDb.adapters.createKnex() +}) + +afterAll(async () => { + await deleteBucket(s3, S3_TEST_BUCKET_NAME) + + // re-disable all network connections, including those to localhost and 0.0.0.0 + nock.disableNetConnect() +}) + +beforeEach(() => { + process.env.searchExportBucket = S3_TEST_BUCKET_NAME + process.env.searchExportS3Endpoint = S3_TEST_ENDPOINT + + jest.clearAllMocks() + + jest.spyOn(deployedEnvironment, 'deployedEnvironment').mockImplementation(() => 'prod') + jest.spyOn(getDbConnection, 'getDbConnection').mockImplementationOnce(() => mockDbConnection) + jest.spyOn(getJwtToken, 'getJwtToken').mockImplementation(() => 'mockJwt') + + + // Manage resetting ENV variables + // TODO: This is causing problems with mocking knex but is noted as important for managing process.env + // jest.resetModules() + process.env = { ...OLD_ENV } + delete process.env.NODE_ENV +}) + +afterEach(async () => { + // Restore any ENV variables overwritten in tests + process.env = OLD_ENV + + jest.clearAllMocks() + + await clearBucket(s3, S3_TEST_BUCKET_NAME) + + // clear in-memory database table + mockDb.public.none('DELETE FROM exports') +}) + +describe('exportSearchCheck', () => { + test('returns requested response correctly', async () => { + const MOCK_USER_ID = 1234 + const MOCK_EXPORT_REQUEST_KEY = '123k476f76f5ewr32' + + jest.spyOn(getVerifiedJwtToken, 'getVerifiedJwtToken').mockImplementation(() => ({ id: MOCK_USER_ID })) + + // add info to database + await mockDbConnection('exports').insert({ + created_at: new Date('1988-09-03T10:00:00.000Z'), + filename: MOCK_FILENAME_CSV, + key: MOCK_EXPORT_REQUEST_KEY, + state: 'REQUESTED', + updated_at: new Date('1988-09-03T10:00:00.000Z'), + user_id: MOCK_USER_ID + }) + + const event = { + body: JSON.stringify({ + key: MOCK_EXPORT_REQUEST_KEY + }) + } + + const result = await exportSearchCheck(event, {}) + + expect(JSON.parse(result.body)).toEqual({ state: 'REQUESTED' }) + }) + + test('returns processing response correctly', async () => { + const MOCK_USER_ID = 5678 + + jest.spyOn(getVerifiedJwtToken, 'getVerifiedJwtToken').mockImplementation(() => ({ id: MOCK_USER_ID })) + + // add info to database + await mockDbConnection('exports').insert({ + created_at: new Date('1988-09-03T10:00:00.000Z'), + filename: MOCK_FILENAME_CSV, + key: MOCK_EXPORT_REQUEST_KEY, + state: 'PROCESSING', + updated_at: new Date('1988-09-03T10:00:00.000Z'), + user_id: MOCK_USER_ID + }) + + const event = { + body: JSON.stringify({ + key: MOCK_EXPORT_REQUEST_KEY + }) + } + + const result = await exportSearchCheck(event, {}) + + expect(JSON.parse(result.body)).toEqual({ state: 'PROCESSING' }) + }) + + test('returns finished json response correctly', async () => { + const MOCK_USER_ID = 23146795 + const MOCK_EXPORT_REQUEST_KEY = 'i76DFTVY71b26t' + + jest.spyOn(getVerifiedJwtToken, 'getVerifiedJwtToken').mockImplementation(() => ({ id: MOCK_USER_ID })) + + // add info to database + await mockDbConnection('exports').insert({ + created_at: new Date('1988-09-03T10:00:00.000Z'), + filename: MOCK_FILENAME_JSON, + key: MOCK_EXPORT_REQUEST_KEY, + state: 'DONE', + updated_at: new Date('1988-09-03T10:00:00.000Z'), + user_id: MOCK_USER_ID + }) + + const MOCK_JSON = [{ "conceptId": "C100000-EDSC", "title": "Test collection", "platforms": [{ "shortName": "platform" }] }, { "conceptId": "C100001-EDSC", "title": "Test collection 1", "platforms": [{ "shortName": "platform" }] }] + + const contentDisposition = `attachment; filename="${MOCK_FILENAME_JSON}"` + + // add file to mock bucket + await s3.upload({ + Bucket: S3_TEST_BUCKET_NAME, + Key: MOCK_EXPORT_REQUEST_KEY, + Body: JSON.stringify(MOCK_JSON), + ACL: 'authenticated-read', + ContentDisposition: contentDisposition, + ContentType: CONTENT_TYPE_JSON, + }).promise() + + const event = { + body: JSON.stringify({ + key: MOCK_EXPORT_REQUEST_KEY + }) + } + + const result = await exportSearchCheck(event, {}) + + const { state, url } = JSON.parse(result.body) + expect(state).toEqual('DONE') + + const response = await axios.get(url) + expect(response.data).toEqual(MOCK_JSON) + expect(response.status).toEqual(200) + expect(response.headers['content-type']).toEqual(CONTENT_TYPE_JSON) + expect(response.headers['content-disposition']).toEqual(contentDisposition) + }) + + test('returns finished csv response correctly', async () => { + const MOCK_USER_ID = 91011 + + jest.spyOn(getVerifiedJwtToken, 'getVerifiedJwtToken').mockImplementation(() => ({ id: MOCK_USER_ID })) + + // add info to database + await mockDbConnection('exports').insert({ + created_at: new Date('1988-09-03T10:00:00.000Z'), + filename: MOCK_FILENAME_CSV, + key: MOCK_EXPORT_REQUEST_KEY, + state: 'DONE', + updated_at: new Date('1988-09-03T10:00:00.000Z'), + user_id: MOCK_USER_ID + }) + + const MOCK_CSV = 'Data Provider,Short Name,Version,Entry Title,Processing Level,Platform,Start Time,End Time\r\n,,,Test collection,,platform,,\r\n,,,Test collection 1,,platform,,\r\n' + + const contentDisposition = `attachment; filename="${MOCK_FILENAME_CSV}"` + + // add file to mock bucket + await s3.upload({ + Bucket: S3_TEST_BUCKET_NAME, + Key: MOCK_EXPORT_REQUEST_KEY, + Body: MOCK_CSV, + ACL: 'authenticated-read', + ContentDisposition: contentDisposition, + ContentType: CONTENT_TYPE_CSV, + }).promise() + + const event = { + body: JSON.stringify({ + key: MOCK_EXPORT_REQUEST_KEY + }) + } + + const result = await exportSearchCheck(event, {}) + + const { state, url } = JSON.parse(result.body) + expect(state).toEqual('DONE') + + const response = await axios.get(url, { + transformResponse: data => data, // ensure treated as plain text + }); + expect(response.data).toEqual(MOCK_CSV) + expect(response.status).toEqual(200) + expect(response.headers['content-type']).toEqual(CONTENT_TYPE_CSV) + expect(response.headers['content-disposition']).toEqual(contentDisposition) + }) +}) diff --git a/serverless/src/exportSearchCheck/handler.js b/serverless/src/exportSearchCheck/handler.js new file mode 100644 index 0000000000..3eded2123c --- /dev/null +++ b/serverless/src/exportSearchCheck/handler.js @@ -0,0 +1,106 @@ +import AWS from 'aws-sdk' + +import { determineEarthdataEnvironment } from '../util/determineEarthdataEnvironment' +import { getDbConnection } from '../util/database/getDbConnection' +import { getApplicationConfig } from '../../../sharedUtils/config' +import { getJwtToken } from '../util/getJwtToken' +import { getVerifiedJwtToken } from '../util/getVerifiedJwtToken' +import doesObjectExist from '../util/aws/doesObjectExist' + +// AWS S3 adapter +let s3 + + +/** + * Check if Search Export is Ready for Download. + * If it is, return a signed URL. + * @param {Object} event Details about the HTTP request that it received + */ +const exportSearchCheck = async (event, context) => { + // https://stackoverflow.com/questions/49347210/why-aws-lambda-keeps-timing-out-when-using-knex-js + // eslint-disable-next-line no-param-reassign + context.callbackWaitsForEmptyEventLoop = false + + s3 ??= new AWS.S3({ + endpoint: process.env.searchExportS3Endpoint + }) + + const { body, headers } = event + + const { defaultResponseHeaders } = getApplicationConfig() + + const earthdataEnvironment = determineEarthdataEnvironment(headers) + + const jwt = getJwtToken(event) + + const { id: userId } = getVerifiedJwtToken(jwt, earthdataEnvironment) + if (!userId) throw Error("failed getting userId from jwt") + + // adding a little extra validation + if (body.length > 1000) throw Error("body is too long") + + const { key } = JSON.parse(body); + if (!key) { + return { + isBase64Encoded: false, + statusCode: 400, + headers: { + ...defaultResponseHeaders, + 'jwt-token': jwt + }, + body: JSON.stringify({ errors: ["missing key"] }) + } + } + + const dbConnection = await getDbConnection() + + const rows = await dbConnection('exports').where({ user_id: userId, key }) + if (rows.length !== 1) { + throw Error(`expected only one row to match user_id "${userId}" and key "${key}", but instead saw ${rows.length} matches`) + } + + const { state } = rows[0] + + if (state !== "DONE") { + return { + isBase64Encoded: false, + statusCode: 200, + headers: { + ...defaultResponseHeaders, + 'jwt-token': jwt + }, + body: JSON.stringify({ state }) + } + } + + const exists = await doesObjectExist(s3, process.env.searchExportBucket, key) + if (!exists) { + return { + isBase64Encoded: false, + statusCode: 400, + headers: { + ...defaultResponseHeaders, + 'jwt-token': jwt + }, + body: JSON.stringify({ errors: ["export should be ready, but we can't find it"] }) + } + } + + const signedUrl = await s3.getSignedUrlPromise('getObject', { + Bucket: process.env.searchExportBucket, + Key: key, + Expires: 10 * 60 // pre-signed URL expires after 10 minutes + }) + + return { + isBase64Encoded: false, + statusCode: 200, + headers: { + ...defaultResponseHeaders, + 'jwt-token': jwt + }, + body: JSON.stringify({ state, url: signedUrl }) + } +} + +export default exportSearchCheck From 56050f6285289b691256968e5d2cd023992bf3c8 Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Wed, 15 Feb 2023 10:49:08 -0500 Subject: [PATCH 13/43] EDSC-3612: removed extra import from serverless/src/exportSearch/__tests__/handler.test.js --- serverless/src/exportSearch/__tests__/handler.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/serverless/src/exportSearch/__tests__/handler.test.js b/serverless/src/exportSearch/__tests__/handler.test.js index 71dde94d09..c4bbb57008 100644 --- a/serverless/src/exportSearch/__tests__/handler.test.js +++ b/serverless/src/exportSearch/__tests__/handler.test.js @@ -1,4 +1,4 @@ -import AWS, { S3 } from 'aws-sdk' +import AWS from 'aws-sdk' import nock from 'nock' import { newDb } from "pg-mem"; From 5785311c9234809cf128d938c72f39b22eb0e0ab Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Thu, 23 Feb 2023 09:14:39 -0500 Subject: [PATCH 14/43] EDSC-3612: updated package.json, adding pg-mem and serverless-offline-sqs, adding scripts for installing and running sqs and s3 simulations --- package.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 84f50dec48..a71f27fcb2 100644 --- a/package.json +++ b/package.json @@ -18,10 +18,12 @@ } ], "scripts": { + "api": "npm run sls -- start --httpPort=3001", "build": "NODE_ENV=production NODE_OPTIONS=--max-old-space-size=6144 webpack --config static.webpack.config.prod.js", "build-sit": "NODE_ENV=production webpack --mode production", "copy-secrets": "cp secret.config.json.example secret.config.json", "migrate": "node-pg-migrate", + "sls": "serverless offline", "start": "NODE_ENV=development webpack-dev-server --open --config static.webpack.config.dev.js", "test": "NODE_OPTIONS=--max-old-space-size=6144 jest", "silent-test": "NODE_OPTIONS=--max-old-space-size=8192 jest --silent", @@ -31,7 +33,11 @@ "cypress:open": "cypress open", "cypress:run": "cypress run -c video=false", "cypress:ci": "start-server-and-test start:ci http-get://localhost:8080 cypress:run", - "cypress:prepare-ci": "bin/cypress-prepare-ci" + "cypress:prepare-ci": "bin/cypress-prepare-ci", + "s3": "EXTRA_CORS_ALLOWED_ORIGINS=http://localhost:5000 MOTO_ALLOW_NONEXISTENT_REGION=True moto_server -H 0.0.0.0", + "s3:install": "brew install moto", + "sqs": "java -jar elasticmq-server-1.3.9.jar", + "sqs:install": "curl https://s3-eu-west-1.amazonaws.com/softwaremill-public/elasticmq-server-1.3.9.jar --output elasticmq-server-1.3.9.jar" }, "devDependencies": { "@cypress/code-coverage": "^3.10.0", @@ -57,9 +63,11 @@ "nock": "^12.0.3", "nyc": "^15.1.0", "parse-multipart": "^1.0.4", + "pg-mem": "^2.6.4", "process": "^0.11.10", "redux-logger": "^3.0.6", "redux-mock-store": "^1.5.4", + "serverless-offline-sqs": "^7.3.2", "snyk": "^1.913.0", "start-server-and-test": "^1.14.0", "three": "^0.127.0", From 461830e05582d3c1814d72bf4e0408f85950b71e Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Thu, 23 Feb 2023 09:17:43 -0500 Subject: [PATCH 15/43] EDSC-3612: added elasticmq-server-1.3.9.jar to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8b7419d437..5e363000b7 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ cypress/videos overrideStatic.config.json cypress-coverage .nyc_output +elasticmq-server-1.3.9.jar From a501304edc31769ea8b133f5474b40714dd6ed54 Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Thu, 23 Feb 2023 09:25:00 -0500 Subject: [PATCH 16/43] EDSC-3612: added sleep and retry --- serverless/src/util/retry.js | 22 ++++++++++++++++++++++ serverless/src/util/sleep.js | 12 ++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 serverless/src/util/retry.js create mode 100644 serverless/src/util/sleep.js diff --git a/serverless/src/util/retry.js b/serverless/src/util/retry.js new file mode 100644 index 0000000000..a27964ed78 --- /dev/null +++ b/serverless/src/util/retry.js @@ -0,0 +1,22 @@ +import sleep from './sleep' + +/** + * + * @param {Function} func - function to attempt to run + * @param {object} options - options object + * @param {number} options.attempts - number of attempts to try + * @param {number} options.backoff - base for exponential backoff + * @description attempts to run the function the given number of times. + * if it throws an error every time, retry will throw the last error + * @returns + */ +export default async function retry(func, { attempts = 3, backoff = 5 }) { + for (let i = 0; i < attempts; i++) { + try { + return await func() + } catch (err) { + if (i === attempts - 1) throw err // last attempt + else await sleep(Math.pow(backoff, i + 1)) + } + } +} diff --git a/serverless/src/util/sleep.js b/serverless/src/util/sleep.js new file mode 100644 index 0000000000..a60e50a055 --- /dev/null +++ b/serverless/src/util/sleep.js @@ -0,0 +1,12 @@ +/** + * @name sleep + * @param {number} seconds - time in seconds + * @return Promise + * @description sleep for the given number of seconds + * @example await sleep(5) // sleep for 5 seconds +*/ +export default function sleep(seconds) { + console.log(`sleeping ${seconds} seconds`) + const ms = seconds * 1000 + return new Promise((resolve) => setTimeout(resolve, ms)) +} From 8a7de7f1a3aaf9871f77e4d61e6c57448a4473ca Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Thu, 23 Feb 2023 09:26:52 -0500 Subject: [PATCH 17/43] EDSC-3612: updated README.md with S3 and SQS simulations --- README.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index bb673f36d1..61fa93885b 100644 --- a/README.md +++ b/README.md @@ -24,11 +24,12 @@ It builds upon several public-facing services provided by EOSDIS, including the ## Application Installation and Usage -The Earthdata Search application uses Node v14 and Webpack 5 to generate static assets. The serverless application utilizes the following AWS services (important to note if deploying to an AWS environment): +The Earthdata Search application uses Node.js and Webpack to generate static assets. The serverless application utilizes the following AWS services (important to note if deploying to an AWS environment): - S3 - - We highly recommend using CloudFront in front of S3. + - We highly recommend using CloudFront in front of S3. We use [moto](https://github.com/getmoto/moto) to simulate S3 during local development. If you have [homebrew](https://brew.sh/) installed, you can install moto by running `npm s3:install` or by following the guide [here](http://docs.getmoto.org/en/latest/docs/getting_started.html#installing-moto). - SQS + - We use [ElasticMQ](https://github.com/softwaremill/elasticmq) to simulate SQS during local development. If you have curl installed, you can install ElasticMQ by running `npm sqs:install` or by following the instructions [here](https://github.com/softwaremill/elasticmq) - API Gateway - Lambda - Cloudwatch (Events) @@ -62,6 +63,7 @@ Earthdata Search utilizes the [Serverless Framework](https://serverless.com/) fo ##### PostgreSQL Earthdata Search uses PostgreSQL in production on AWS RDS. If you don't already have it installed, [download](https://www.postgresql.org/download/) and install it to your development environment. +We also use [pg-mem](https://github.com/oguimbal/pg-mem) for some tests and may roll it out to more tests in the future. pg-mem is automatically installed along with all the other dependencies when you run `npm install`. **Recommended:** Use Homebrew @@ -131,15 +133,13 @@ The [serverless framework](https://serverless.com/framework/docs/providers/aws/) - SQS - While there is an sqs-offline plugin for serverless it still requires an actual queue be running, we may investigate this in the future but for now sqs functionality isn't available while developing locally which means the following pieces of functionality will not operate locally: - -- Generating Colormaps + We are currently using [ElasticMQ](https://github.com/softwaremill/elasticmq) to simulate SQS during local development. However we haven't yet rolled this functionality out to colormap generation and other parts of the code. #### Running API Gateway and Lambda Locally Running the following command will spin up API Gateway and Lambda locally which will open up a vast majority of the functionality the backend offers. - serverless offline + npm run api This will provide access to API Gateway at [http://localhost:3001](http://localhost:3001) @@ -151,6 +151,8 @@ Once the project is built, you must ensure that the automated unit tests pass: npm run test +You must run `npm run s3` and `npm run sqs` before running the Jest tests for exporting search results. + ### Run the Automated [Cypress](https://www.cypress.io/) tests You must also ensure that the automated integration tests pass: From 8b5c5058762757eb911580339902b188b8d17c7a Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Thu, 23 Feb 2023 09:29:02 -0500 Subject: [PATCH 18/43] EDSC-3612: added doesBucketExist --- serverless/src/util/aws/doesBucketExist.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 serverless/src/util/aws/doesBucketExist.js diff --git a/serverless/src/util/aws/doesBucketExist.js b/serverless/src/util/aws/doesBucketExist.js new file mode 100644 index 0000000000..3a4deed348 --- /dev/null +++ b/serverless/src/util/aws/doesBucketExist.js @@ -0,0 +1,13 @@ +/** + * @name doesBucketExist + * @param {Object} s3 - instance of AWS.S3 from 'aws-sdk' + * @param {String} bucketName + * @returns {Promise} exists - Promise that resolves to true or false + */ +export default async function doesBucketExist(s3, bucketName) { + const { Buckets = [] } = await s3.listBuckets().promise() + + const bucketNames = Buckets.map(({ Name }) => Name) + + return bucketNames.includes(bucketName) +} From 91fd4b1dd856183b2b159368b82ff21ef376c6b8 Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Thu, 23 Feb 2023 09:31:45 -0500 Subject: [PATCH 19/43] EDSC-3612: added serverless/src/util/aws/getSearchExportBucket.js --- serverless/src/util/aws/getSearchExportBucket.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 serverless/src/util/aws/getSearchExportBucket.js diff --git a/serverless/src/util/aws/getSearchExportBucket.js b/serverless/src/util/aws/getSearchExportBucket.js new file mode 100644 index 0000000000..9a8e9001a3 --- /dev/null +++ b/serverless/src/util/aws/getSearchExportBucket.js @@ -0,0 +1,15 @@ +/** + * Returns the search export bucket name from the env variable or the hard-coded offline value + * @return {string} the bucket name + */ +export const getSearchExportBucket = () => { + let bucket = process.env.searchExportBucket + + if (process.env.IS_OFFLINE) { + // If we are running locally offline, this is the queueUrl + bucket = 'mock-search-export-bucket' + } + + console.log('got search export bucket name: ' + bucket) + return bucket +} From fddda4e5412de8e72baaba1c2ec0535b15ce4fcc Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Thu, 23 Feb 2023 09:33:38 -0500 Subject: [PATCH 20/43] EDSC-3612: added serverless/src/util/aws/getSearchExportQueueUrl.js --- serverless/src/util/aws/getSearchExportQueueUrl.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 serverless/src/util/aws/getSearchExportQueueUrl.js diff --git a/serverless/src/util/aws/getSearchExportQueueUrl.js b/serverless/src/util/aws/getSearchExportQueueUrl.js new file mode 100644 index 0000000000..50d5c332de --- /dev/null +++ b/serverless/src/util/aws/getSearchExportQueueUrl.js @@ -0,0 +1,14 @@ +/** + * Returns the search export SQS queue url from the env variable, or the hard coded offline value + * @return {string} The order processing queue URL + */ +export default function getSearchExportQueueUrl() { + let queueUrl = process.env.searchExportQueueUrl + + if (process.env.IS_OFFLINE) { + // If we are running locally offline, this is the queueUrl + queueUrl = 'http://localhost:9324/queue/earthdata-search-dev-SearchExportQueue' + } + + return queueUrl +} From 3bde7a45c5717296ed9d001bd2603051d9e3391b Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Thu, 23 Feb 2023 09:35:42 -0500 Subject: [PATCH 21/43] EDSC-3612: added getSearchExportS3Endpoint --- .../src/util/aws/getSearchExportS3Endpoint.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 serverless/src/util/aws/getSearchExportS3Endpoint.js diff --git a/serverless/src/util/aws/getSearchExportS3Endpoint.js b/serverless/src/util/aws/getSearchExportS3Endpoint.js new file mode 100644 index 0000000000..f13ec98090 --- /dev/null +++ b/serverless/src/util/aws/getSearchExportS3Endpoint.js @@ -0,0 +1,14 @@ +/** + * Returns the search export S3 endpoint from the env variable or the hard coded offline value + * @return {string} url to the S3 endpoint + */ +export default function getSearchExportS3Endpoint () { + let endpoint = process.env.searchExportS3Endpoint + + if (process.env.IS_OFFLINE) { + // if we are running locally offline, this is the local S3-compatible endpoint + endpoint = 'http://0.0.0.0:5000' + } + + return endpoint +} From 4eb6a262c4138e01a9886a4fbab3bbb0ce2eda85 Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Thu, 23 Feb 2023 09:37:05 -0500 Subject: [PATCH 22/43] EDSC-3612: added getS3Config.js --- serverless/src/util/aws/getS3Config.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 serverless/src/util/aws/getS3Config.js diff --git a/serverless/src/util/aws/getS3Config.js b/serverless/src/util/aws/getS3Config.js new file mode 100644 index 0000000000..0b7c42acee --- /dev/null +++ b/serverless/src/util/aws/getS3Config.js @@ -0,0 +1,17 @@ +import getSearchExportS3Endpoint from './getSearchExportS3Endpoint' + +/** + * Returns an environment specific configuration object for S3 + * @return {Object} A configuration object for S3 + */ +export const getS3Config = () => { + const s3config = { + endpoint: getSearchExportS3Endpoint() + } + + if (process.env.IS_OFFLINE || process.env.JEST_WORKER_ID) { + s3config.s3ForcePathStyle = true + } + + return s3config +} From 4d8f1a5c8ca3d2f2f32a45d507da33d7c982e2fb Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Thu, 23 Feb 2023 09:38:52 -0500 Subject: [PATCH 23/43] EDSC-3612: added custom serverless-offline plugin for creating default S3 buckets, used by search exporting --- .../serverless-offline-config-s3.js | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 serverless-plugins/serverless-offline-config-s3.js diff --git a/serverless-plugins/serverless-offline-config-s3.js b/serverless-plugins/serverless-offline-config-s3.js new file mode 100644 index 0000000000..1bef215f7e --- /dev/null +++ b/serverless-plugins/serverless-offline-config-s3.js @@ -0,0 +1,56 @@ +const AWS = require('aws-sdk') + +/** + * This serverless plugin sets up our local AWS S3 simulation + * Basically, it creates the default buckets used by EDSC. + */ +class ServerlessOfflineConfigS3 { + constructor(serverless) { + const config = serverless.configurationInput.custom['serverless-offline-config-s3'] + const { buckets, endpoint, region } = config + + if (!buckets) throw Error('serverless-offline-config-s3 missing buckets') + if (!endpoint) throw Error('serverless-offline-config-s3 missing endpoint') + if (!region) throw Error('serverless-offline-config-s3 missing region') + + this.buckets = buckets + this.endpoint = endpoint + this.region = region + + this.hooks = { + 'offline:start:init': this.start.bind(this), + }; + } + + async start() { + AWS.config.update({ + accessKeyId: Math.random().toString(), + secretAccessKey: Math.random().toString(), + region: this.region + }) + + const s3 = new AWS.S3({ + endpoint: this.endpoint, + s3ForcePathStyle: true + }) + + const { Buckets = [] } = await s3.listBuckets().promise() + + const bucketNames = Buckets.map(({ Name }) => Name) + + for (let i = 0; i < this.buckets.length; i++) { + const bucketName = this.buckets[i] + + if (!bucketNames.includes(bucketName)) { + await s3.createBucket({ + Bucket: bucketName, + CreateBucketConfiguration: { + LocationConstraint: this.region + } + }).promise() + } + } + } +} + +module.exports = ServerlessOfflineConfigS3 From bf4fcd3bb0127bef37b657d665726411c4310f7b Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Thu, 23 Feb 2023 09:40:23 -0500 Subject: [PATCH 24/43] EDSC-3612: created migration for search exports table --- migrations/1676386726943_exports.js | 36 +++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 migrations/1676386726943_exports.js diff --git a/migrations/1676386726943_exports.js b/migrations/1676386726943_exports.js new file mode 100644 index 0000000000..d2a09ff2eb --- /dev/null +++ b/migrations/1676386726943_exports.js @@ -0,0 +1,36 @@ +exports.shorthands = undefined + +exports.up = (pgm) => { + pgm.createTable('exports', { + id: 'id', + user_id: { + type: 'integer', + references: 'users' + }, + key: { + type: 'varchar(1000)', + notNull: true + }, + state: { + type: 'varchar(1000)' // 'REQUESTED', 'PROCESSING', 'DONE', or 'FAILED' + }, + filename: { + type: 'varchar(1000)', + notNull: true + }, + updated_at: { + type: 'timestamp', + notNull: true, + default: pgm.func('current_timestamp') + }, + created_at: { + type: 'timestamp', + notNull: true, + default: pgm.func('current_timestamp') + } + }) +} + +exports.down = (pgm) => { + pgm.dropTable('exports') +} From 5177805ce7716075e55cdba10b5b604f99bf6811 Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Thu, 23 Feb 2023 09:41:55 -0500 Subject: [PATCH 25/43] EDSC-3612: added exportStatusRefreshTime to static.config.json --- static.config.json | 1 + 1 file changed, 1 insertion(+) diff --git a/static.config.json b/static.config.json index 28737a747d..0db2d456ef 100644 --- a/static.config.json +++ b/static.config.json @@ -18,6 +18,7 @@ "width": 85 }, "orderStatusRefreshTime": 60000, + "exportStatusRefreshTime": 5000, "defaultCmrPageSize": 20, "maxCmrPageSize": 2000, "granuleLinksPageSize": "500", From 53a20469b54a344164359ba20e83d8d3f5cc9a67 Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Thu, 23 Feb 2023 09:43:25 -0500 Subject: [PATCH 26/43] EDSC-3612: added SearchExportBucket and SearchExportQueue to serverless-configs/aws-resources.yml --- serverless-configs/aws-resources.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/serverless-configs/aws-resources.yml b/serverless-configs/aws-resources.yml index 6a51091b5a..bb379cb694 100644 --- a/serverless-configs/aws-resources.yml +++ b/serverless-configs/aws-resources.yml @@ -1,4 +1,27 @@ Resources: + SearchExportBucket: + Type: 'AWS::S3::Bucket' + Properties: + BucketName: ${self:custom.siteName}-SearchExportBucket + AccessControl: Private + CorsConfiguration: + CorsRules: + - AllowedMethods: + - GET + - OPTIONS + - HEAD + AllowedOrigins: + - "*" + AllowedHeaders: + - "*" + + SearchExportQueue: + Type: 'AWS::SQS::Queue' + Properties: + QueueName: ${self:custom.siteName}-SearchExportQueue + ReceiveMessageWaitTimeSeconds: 20 + VisibilityTimeout: 300 + # SQS Queue to process Color Map entries ColorMapsProcessingQueue: Type: 'AWS::SQS::Queue' From 54305ee606f9bee6f705eef077cf1ec4ba11f39d Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Thu, 23 Feb 2023 09:54:02 -0500 Subject: [PATCH 27/43] EDSC-3612: updated serveless-configs/aws-functions.yml with search export functions --- serverless-configs/aws-functions.yml | 58 +++++++++++++++++++++------- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/serverless-configs/aws-functions.yml b/serverless-configs/aws-functions.yml index 8c1c957282..3929140dd3 100644 --- a/serverless-configs/aws-functions.yml +++ b/serverless-configs/aws-functions.yml @@ -130,6 +130,50 @@ environment: updateOrderStatusStateMachineArn: ${self:resources.Outputs.UpdateOrderStatusWorkflow.Value} + exportSearchRequest: + handler: serverless/src/exportSearchRequest/handler.default + timeout: 300 # this lambda isn't restricted to 30s because it's triggered by SQS not API Gateway + memorySize: 256 + events: + - http: + method: post + cors: ${file(./serverless-configs/${self:provider.name}-cors-configuration.yml)} + path: collections/export + authorizer: + name: edlOptionalAuthorizer + type: request + resultTtlInSeconds: 0 + + # Lambda that actually creates the .json or .csv file and uploads it to S3 + exportSearch: + handler: serverless/src/exportSearch/handler.default + timeout: 600 + events: + - sqs: + # we set the batch size to 1 for several reasons: + # - protect cmr-graphql from too many requests + # - ensure independent execution of exports + # - prevent an error in one export from failing other exports + batchSize: 1 + arn: + Fn::GetAtt: + - SearchExportQueue # runs when a new request appears in SearchExportQueue + - Arn + + exportSearchCheck: + handler: serverless/src/exportSearchCheck/handler.default + timeout: 300 + memorySize: 192 + events: + - http: + method: post + cors: ${file(./serverless-configs/${self:provider.name}-cors-configuration.yml)} + path: collections/export-check + authorizer: + name: edlOptionalAuthorizer + type: request + resultTtlInSeconds: 0 + fetchCatalogRestOrder: handler: serverless/src/fetchCatalogRestOrder/handler.default timeout: ${env:LAMBDA_TIMEOUT, '30'} @@ -256,20 +300,6 @@ cors: ${file(./serverless-configs/${self:provider.name}-cors-configuration.yml)} path: concepts/metadata - exportSearch: - handler: serverless/src/exportSearch/handler.default - timeout: 300 - memorySize: 192 - events: - - http: - method: post - cors: ${file(./serverless-configs/${self:provider.name}-cors-configuration.yml)} - path: collections/export - authorizer: - name: edlOptionalAuthorizer - type: request - resultTtlInSeconds: 0 - saveShapefile: handler: serverless/src/saveShapefile/handler.default timeout: ${env:LAMBDA_TIMEOUT, '30'} From 666aeaa10c60a9e40a0690bf3c824f79189b6940 Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Thu, 23 Feb 2023 11:00:23 -0500 Subject: [PATCH 28/43] EDSC-3612: require user to be logged-in in order to export search results --- serverless/src/edlOptionalAuthorizer/handler.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/serverless/src/edlOptionalAuthorizer/handler.js b/serverless/src/edlOptionalAuthorizer/handler.js index afc900a333..6092023078 100644 --- a/serverless/src/edlOptionalAuthorizer/handler.js +++ b/serverless/src/edlOptionalAuthorizer/handler.js @@ -31,8 +31,7 @@ const edlOptionalAuthorizer = async (event) => { if (!jwtToken || jwtToken === '') { const authOptionalPaths = [ '/autocomplete', - '/opensearch/granules', - '/collections/export' + '/opensearch/granules' ] // Allow for optional authentication From 450dc53aa3f93ccc618d6ca61b5e577ee70d3e99 Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Thu, 23 Feb 2023 11:03:31 -0500 Subject: [PATCH 29/43] EDSC-3612: updated GraphQlRequest search function to accept extra params --- static/src/js/util/request/graphQlRequest.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/static/src/js/util/request/graphQlRequest.js b/static/src/js/util/request/graphQlRequest.js index 70d0e1867c..f130702265 100644 --- a/static/src/js/util/request/graphQlRequest.js +++ b/static/src/js/util/request/graphQlRequest.js @@ -71,7 +71,7 @@ export default class GraphQlRequest extends Request { return null } - search(query, variables, format = undefined) { + search(query, variables, format = undefined, extra = {}) { this.startTimer() this.setFullUrl(this.searchPath) this.generateRequestId() @@ -83,7 +83,8 @@ export default class GraphQlRequest extends Request { data: { query, variables, - format + format, + ...extra }, transformRequest: [ (data, headers) => this.transformRequest(data, headers) From 7cb67e559a6370c723cc5537c4ab120498a2de82 Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Thu, 23 Feb 2023 15:23:37 -0500 Subject: [PATCH 30/43] EDSC-3612: updated exportSearchRequest handler and tests, including mocking of randomUUID --- __mocks__/crypto.js | 6 ++ .../__tests__/handler.test.js | 34 +++++----- serverless/src/exportSearchRequest/handler.js | 63 +++++++++++-------- 3 files changed, 60 insertions(+), 43 deletions(-) create mode 100644 __mocks__/crypto.js diff --git a/__mocks__/crypto.js b/__mocks__/crypto.js new file mode 100644 index 0000000000..d41bc6f6df --- /dev/null +++ b/__mocks__/crypto.js @@ -0,0 +1,6 @@ +// copy properties of the built-in crypto module to the mock crypto +const crypto = Object.assign({}, require("node:crypto")) + +crypto.randomUUID = () => '00000000-0000-0000-0000-000000000000' + +module.exports = crypto diff --git a/serverless/src/exportSearchRequest/__tests__/handler.test.js b/serverless/src/exportSearchRequest/__tests__/handler.test.js index 7408dd5b00..091c7a5d6b 100644 --- a/serverless/src/exportSearchRequest/__tests__/handler.test.js +++ b/serverless/src/exportSearchRequest/__tests__/handler.test.js @@ -1,7 +1,7 @@ import AWS from 'aws-sdk' import MockDate from 'mockdate' import nock from 'nock' -import { newDb } from "pg-mem"; +import { newDb } from "pg-mem" import * as deployedEnvironment from '../../../../sharedUtils/deployedEnvironment' import * as getDbConnection from '../../util/database/getDbConnection' @@ -19,8 +19,11 @@ const SQS_TEST_HOST = `0.0.0.0:${SQS_TEST_PORT}` const SQS_TEST_LOCALHOST = `localhost:${SQS_TEST_PORT}` const SQS_TEST_QUEUE_NAME = 'REQUEST_SEARCH_EXPORT_TEST_QUEUE' const SQS_TEST_ENDPOINT = `http://${SQS_TEST_HOST}` -const MOCK_ECHO_TOKEN = '1234-abcd-5678-efgh' const MOCK_USER_ID = 1234 +const MOCK_KEY = '00000000-0000-0000-0000-000000000000' // see /__mocks__/crypto.js + +// over-rides randomUUID to return the MOCK_KEY +jest.mock('crypto') // need to configure here because the aws-sdk expects it // without it, the handler will throw an error @@ -91,12 +94,13 @@ beforeEach(async () => { }) afterEach(() => { + jest.clearAllMocks() + // Restore any ENV variables overwritten in tests process.env = OLD_ENV // reset hacks on built-ins MockDate.reset() - jest.spyOn(global.Math, 'random').mockRestore(); // clear in-memory database table mockDb.public.none('DELETE FROM exports') @@ -125,9 +129,7 @@ describe('exportSearch', () => { const result = await exportSearch(event, {}) - const expectedKey = '66241fe6c79c644cfc52b7f39644f5b7394ce1f30d4a0dd4b2237c8ca669ddee'; - - expect(result.body).toEqual(`{"key":"${expectedKey}"}`) + expect(result.body).toEqual(`{"key":"${MOCK_KEY}"}`) const { Messages } = await sqs.receiveMessage({ QueueUrl: testSearchExportQueueUrl }).promise() expect(Messages).toHaveLength(1) @@ -142,9 +144,9 @@ describe('exportSearch', () => { }, extra: { earthdataEnvironment: "prod", - filename: "search_results_export_66241fe6c7.csv", + filename: "search_results_export_00000000.csv", jwt: 'mockJwt', - key: expectedKey, + key: MOCK_KEY, requestId: 'asdf-1234-qwer-5678', userId: MOCK_USER_ID } @@ -153,8 +155,8 @@ describe('exportSearch', () => { // check if the correct information was saved to the database const databaseTableRows = await mockDbConnection('exports') expect(databaseTableRows).toEqual([{ - filename: 'search_results_export_66241fe6c7.csv', - key: '66241fe6c79c644cfc52b7f39644f5b7394ce1f30d4a0dd4b2237c8ca669ddee', + filename: 'search_results_export_00000000.csv', + key: MOCK_KEY, state: 'REQUESTED', user_id: MOCK_USER_ID, updated_at: new Date('1988-09-03T10:00:00.000Z'), @@ -183,7 +185,7 @@ describe('exportSearch', () => { const result = await exportSearch(event, {}) - expect(result.body).toEqual("{\"key\":\"6d5ae367c8c99d6bdf0fe7c4bfb56fc5306991c59a0d6c316386598f6711716b\"}") + expect(result.body).toEqual(`{"key":"${MOCK_KEY}"}`) const { Messages } = await sqs.receiveMessage({ QueueUrl: testSearchExportQueueUrl }).promise() expect(Messages).toHaveLength(1) @@ -192,8 +194,8 @@ describe('exportSearch', () => { expect(message).toEqual({ extra: { earthdataEnvironment: "prod", - key: "6d5ae367c8c99d6bdf0fe7c4bfb56fc5306991c59a0d6c316386598f6711716b", - filename: "search_results_export_6d5ae367c8.json", + key: MOCK_KEY, + filename: "search_results_export_00000000.json", jwt: "mockJwt", requestId: "asdf-1234-qwer-5678", userId: MOCK_USER_ID @@ -208,8 +210,8 @@ describe('exportSearch', () => { // check if the correct information was saved to the database const databaseTableRows = await mockDbConnection('exports') expect(databaseTableRows).toEqual([{ - filename: 'search_results_export_6d5ae367c8.json', - key: '6d5ae367c8c99d6bdf0fe7c4bfb56fc5306991c59a0d6c316386598f6711716b', + filename: 'search_results_export_00000000.json', + key: MOCK_KEY, state: 'REQUESTED', user_id: MOCK_USER_ID, updated_at: new Date('1988-09-03T10:00:00.000Z'), @@ -235,7 +237,7 @@ describe('exportSearch', () => { const { body } = response const parsedBody = JSON.parse(body) const { errors } = parsedBody - const [errorMessage] = errors + const errorMessage = errors[0] expect(errorMessage).toEqual('SyntaxError: Unexpected end of JSON input') diff --git a/serverless/src/exportSearchRequest/handler.js b/serverless/src/exportSearchRequest/handler.js index 9020c91cba..4aed71788d 100644 --- a/serverless/src/exportSearchRequest/handler.js +++ b/serverless/src/exportSearchRequest/handler.js @@ -1,4 +1,4 @@ -import { createHash } from 'node:crypto' +import { randomUUID } from 'crypto' // if we do 'node:crypto', jest.mock won't work import AWS from 'aws-sdk' @@ -8,6 +8,7 @@ import { getDbConnection } from '../util/database/getDbConnection' import { getJwtToken } from '../util/getJwtToken' import { getVerifiedJwtToken } from '../util/getVerifiedJwtToken' import { getSqsConfig } from '../util/aws/getSqsConfig' +import getSearchExportQueueUrl from '../util/aws/getSearchExportQueueUrl' import { parseError } from '../../../sharedUtils/parseError' // adapter for Amazon Simple Queue Service (SQS) @@ -22,29 +23,39 @@ const exportSearchRequest = async (event, context) => { // eslint-disable-next-line no-param-reassign context.callbackWaitsForEmptyEventLoop = false + if (process.env.IS_OFFLINE || process.env.JEST_WORKER_ID) { + // fake/mock values when doing local development or testing + AWS.config.update({ + accessKeyId: Math.random().toString(), + secretAccessKey: Math.random().toString(), + region: 'moon' + }) + } + sqs ??= new AWS.SQS(getSqsConfig()) const { body, headers } = event const { defaultResponseHeaders } = getApplicationConfig() + try { - const earthdataEnvironment = determineEarthdataEnvironment(headers) + const earthdataEnvironment = determineEarthdataEnvironment(headers) - const jwt = getJwtToken(event) + const jwt = getJwtToken(event) + if (!jwt) throw Error("missing jwt") - const { id: userId } = getVerifiedJwtToken(jwt, earthdataEnvironment) + const { id: userId } = getVerifiedJwtToken(jwt, earthdataEnvironment) - if (!userId) throw Error("failed getting userId from jwt") + if (!userId) throw Error("failed getting userId from jwt") - try { const { data, requestId } = JSON.parse(body) if (!requestId) throw Error("missing requestId"); const { columns, cursorpath, format = 'json', itempath, query, variables } = data - if (!['csv', 'json'].includes(format)) throw Error("invalid format"); + if (!['csv', 'json'].includes(format)) throw Error("invalid or missing format"); const params = { columns, @@ -55,10 +66,9 @@ const exportSearchRequest = async (event, context) => { variables } - // hash the params - const key = createHash('sha256').update(JSON.stringify(Object.entries(params))).digest('hex'); + const key = randomUUID() - const filename = `search_results_export_${key.substring(0, 10)}.${format}` + const filename = `search_results_export_${key.split('-')[0]}.${format}` const extra = { earthdataEnvironment, @@ -73,24 +83,23 @@ const exportSearchRequest = async (event, context) => { const messageBody = JSON.stringify(message); - if (!process.env.IS_OFFLINE) { - const dbConnection = await getDbConnection() + const dbConnection = await getDbConnection() - await dbConnection('exports').insert({ - user_id: userId, - key, - state: "REQUESTED", - filename - }) - - await sqs.sendMessage({ - QueueUrl: process.env.searchExportQueueUrl, - MessageBody: messageBody - }).promise() - } - - // maybe have a environmental variable to directly invoke the processing lambda instead of posting to a queue - // this would be done for local development + await dbConnection('exports').insert({ + user_id: userId, + key, + state: "REQUESTED", + filename + }) + + const searchExportQueueUrl = getSearchExportQueueUrl() + console.log('searchExportQueueUrl:', searchExportQueueUrl) + + await sqs.sendMessage({ + QueueUrl: searchExportQueueUrl, + MessageBody: messageBody + }).promise() + console.log('posted to search export queue') return { isBase64Encoded: false, From b5a3c829414a5bd63d6ffd9a9fc343db96a35aa1 Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Thu, 23 Feb 2023 15:41:29 -0500 Subject: [PATCH 31/43] EDSC-3612: updated exportSearch and made running S3 optional for development --- .../serverless-offline-config-s3.js | 60 +++++++----- .../exportSearch/__tests__/handler.test.js | 4 +- serverless/src/exportSearch/handler.js | 95 +++++++++++++------ 3 files changed, 100 insertions(+), 59 deletions(-) diff --git a/serverless-plugins/serverless-offline-config-s3.js b/serverless-plugins/serverless-offline-config-s3.js index 1bef215f7e..85b2bd8b5a 100644 --- a/serverless-plugins/serverless-offline-config-s3.js +++ b/serverless-plugins/serverless-offline-config-s3.js @@ -19,35 +19,43 @@ class ServerlessOfflineConfigS3 { this.hooks = { 'offline:start:init': this.start.bind(this), - }; + } } async start() { - AWS.config.update({ - accessKeyId: Math.random().toString(), - secretAccessKey: Math.random().toString(), - region: this.region - }) - - const s3 = new AWS.S3({ - endpoint: this.endpoint, - s3ForcePathStyle: true - }) - - const { Buckets = [] } = await s3.listBuckets().promise() - - const bucketNames = Buckets.map(({ Name }) => Name) - - for (let i = 0; i < this.buckets.length; i++) { - const bucketName = this.buckets[i] - - if (!bucketNames.includes(bucketName)) { - await s3.createBucket({ - Bucket: bucketName, - CreateBucketConfiguration: { - LocationConstraint: this.region - } - }).promise() + try { + AWS.config.update({ + accessKeyId: Math.random().toString(), + secretAccessKey: Math.random().toString(), + region: this.region + }) + + const s3 = new AWS.S3({ + endpoint: this.endpoint, + s3ForcePathStyle: true + }) + + const { Buckets = [] } = await s3.listBuckets().promise() + + const bucketNames = Buckets.map(({ Name }) => Name) + + for (let i = 0; i < this.buckets.length; i++) { + const bucketName = this.buckets[i] + + if (!bucketNames.includes(bucketName)) { + await s3.createBucket({ + Bucket: bucketName, + CreateBucketConfiguration: { + LocationConstraint: this.region + } + }).promise() + } + } + } catch (error) { + if (error.code === 'UnknownEndpoint') { + console.error('please run: npm run s3') + } else { + throw error } } } diff --git a/serverless/src/exportSearch/__tests__/handler.test.js b/serverless/src/exportSearch/__tests__/handler.test.js index c4bbb57008..31cc99ccca 100644 --- a/serverless/src/exportSearch/__tests__/handler.test.js +++ b/serverless/src/exportSearch/__tests__/handler.test.js @@ -1,6 +1,6 @@ import AWS from 'aws-sdk' import nock from 'nock' -import { newDb } from "pg-mem"; +import { newDb } from "pg-mem" import * as getEarthdataConfig from '../../../../sharedUtils/config' import * as getDbConnection from '../../util/database/getDbConnection' @@ -10,7 +10,7 @@ import exportSearch from '../handler' const OLD_ENV = process.env -const MOCK_REGION = 'antarctica' +const MOCK_REGION = 'moon' const S3_TEST_PORT = 5000 const S3_TEST_HOST = `0.0.0.0:${S3_TEST_PORT}` const S3_TEST_LOCALHOST = `localhost:${S3_TEST_PORT}` diff --git a/serverless/src/exportSearch/handler.js b/serverless/src/exportSearch/handler.js index 82c92d054b..8d3d94665f 100644 --- a/serverless/src/exportSearch/handler.js +++ b/serverless/src/exportSearch/handler.js @@ -5,7 +5,13 @@ import axios from 'axios' import { get } from 'sendero'; import { getEarthdataConfig } from '../../../sharedUtils/config' +import doesBucketExist from '../util/aws/doesBucketExist' +import { getS3Config } from '../util/aws/getS3Config'; +import { getSearchExportBucket } from '../util/aws/getSearchExportBucket' import { getDbConnection } from '../util/database/getDbConnection' +import { getEchoToken } from '../util/urs/getEchoToken' +import retry from '../util/retry' +import sleep from '../util/sleep' import { jsonToCsv } from '../util/jsonToCsv' import { wrapAxios } from '../util/wrapAxios' @@ -15,7 +21,7 @@ const wrappedAxios = wrapAxios(axios) let s3 /** - * Perform a loop through collection results and return the full results in the requested format. + * Perform a loop through collection results and upload the full results in the requested format to S3 * @param {Object} event Details about the HTTP request that it received */ const exportSearch = async (event, context) => { @@ -23,11 +29,20 @@ const exportSearch = async (event, context) => { // eslint-disable-next-line no-param-reassign context.callbackWaitsForEmptyEventLoop = false + if (process.env.IS_OFFLINE || process.env.JEST_WORKER_ID) { + // fake/mock values when doing local development + AWS.config.update({ + accessKeyId: Math.random().toString(), + secretAccessKey: Math.random().toString(), + region: 'moon' + }) + } + const dbConnection = await getDbConnection() - // we set the concurrency to 1, so we only process one at a time + // we set the concurrency to 1 in aws-functions.yml, so we only process one at a time // this way, if there's an issue, we can retry only the one that failed - const { Records: [sqsRecord] } = event + const sqsRecord = event.Records[0] if (!sqsRecord) return @@ -41,11 +56,15 @@ const exportSearch = async (event, context) => { if (!requestId) throw new Error("missing requestId") if (!userId) throw new Error("missing userId") - if ((await dbConnection('exports').where({ user_id: userId, key })).length !== 1) { - throw Error("invalid number of rows matching user_id and key") + const updateState = (state) => dbConnection('exports').where({ user_id: userId, key }).update({ state }) + + const matches = await dbConnection('exports').where({ user_id: userId, key }) + if (matches.length !== 1) { + await updateState("FAILED") + throw Error(`expected only one row to match user_id "${userId}" and key "${key}", but instead saw ${matches.length} matches`) } - await dbConnection('exports').where({ user_id: userId, key }).update({ state: "PROCESSING" }).returning(['user_id', 'key']) + await updateState("PROCESSING") // create request header for X-Request-Id // so we can track a request across applications @@ -69,14 +88,16 @@ const exportSearch = async (event, context) => { let cursor let finished = false const returnItems = [] - let responseHeaders // Loop until the request comes back with no items while (!finished) { - // We need this await inside the loop because we have to wait on the response from the previous + // We need to use await inside the loop because we have to wait on the response from the previous // call before making the next request // eslint-disable-next-line no-await-in-loop - const response = await wrappedAxios({ + + let response + + const options = { url: graphQlUrl, method: 'post', data: { @@ -87,13 +108,23 @@ const exportSearch = async (event, context) => { } }, headers: requestHeaders - }) + } + + // sleep 2 seconds before each request except the first + // so as not to over-load cmr-graphql + if (returnItems.length > 0) await sleep(2); + + try { + response = await retry(() => wrappedAxios(options), { attempts: 3, backoff: 5 }) + } catch (error) { + await updateState("FAILED") + throw error + } const { data: responseData, config } = response; - ({ headers: responseHeaders, status } = response) const { elapsedTime } = config @@ -130,27 +161,29 @@ const exportSearch = async (event, context) => { throw new Error("invalid format") } - s3 ??= new AWS.S3({ - endpoint: process.env.searchExportS3Endpoint - }) - - if (!process.env.IS_OFFLINE) { - await s3.upload({ - Bucket: process.env.searchExportBucket, - Key: key, - Body: returnBody, - ACL: 'authenticated-read', - ContentDisposition: `attachment; filename="${filename}"`, - ContentType: contentType, - - // Content-MD5 recommended by https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#putObject-property - ContentMD5: crypto.createHash("md5").update(returnBody).digest("base64") - }).promise() - console.log(`uploaded object to bucket "${process.env.searchExportBucket}" with key "${key}"`) - - await dbConnection('exports').where({ user_id: userId, key }).update({ state: "DONE" }) - console.log('updated export status in the database') + s3 ??= new AWS.S3(getS3Config()) + + const searchExportBucket = getSearchExportBucket() + + if (!(await doesBucketExist(s3, searchExportBucket))) { + await updateState("FAILED") + throw Error(`bucket with name "${searchExportBucket}" does not exist`) } + + await s3.upload({ + Bucket: searchExportBucket, + Key: key, + Body: returnBody, + ACL: 'authenticated-read', + ContentDisposition: `attachment; filename="${filename}"`, + ContentType: contentType, + + // Content-MD5 recommended by https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#putObject-property + ContentMD5: crypto.createHash("md5").update(returnBody).digest("base64") + }).promise() + console.log(`uploaded object to bucket "${searchExportBucket}" with key "${key}"`) + + await updateState("DONE") } export default exportSearch From b14579189da8885fd5ae46e0bf0ccad92ea614a8 Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Thu, 23 Feb 2023 15:44:16 -0500 Subject: [PATCH 32/43] EDSC-3612: updated exportSearchCheck --- .../__tests__/handler.test.js | 32 +++++++++++++------ serverless/src/exportSearchCheck/handler.js | 32 +++++++++++++++---- 2 files changed, 47 insertions(+), 17 deletions(-) diff --git a/serverless/src/exportSearchCheck/__tests__/handler.test.js b/serverless/src/exportSearchCheck/__tests__/handler.test.js index f5babd4abf..3da523fbf8 100644 --- a/serverless/src/exportSearchCheck/__tests__/handler.test.js +++ b/serverless/src/exportSearchCheck/__tests__/handler.test.js @@ -15,7 +15,7 @@ import exportSearchCheck from '../handler' const OLD_ENV = process.env -const MOCK_REGION = 'antarctica' +const MOCK_REGION = 'moon' const S3_TEST_PORT = 5000 const S3_TEST_HOST = `0.0.0.0:${S3_TEST_PORT}` const S3_TEST_LOCALHOST = `localhost:${S3_TEST_PORT}` @@ -25,9 +25,10 @@ const S3_TEST_ENDPOINT = `http://${S3_TEST_HOST}` const MOCK_EXPORT_REQUEST_KEY = '123456789abcdefg' const MOCK_FILENAME_CSV = 'search_results_export_123456789.csv' const MOCK_FILENAME_JSON = 'search_results_export_123456789.json' +const MOCK_REQUEST_ID = 'd1b9b99c-c001-4502-bf82-5efaccf8c542' -const CONTENT_TYPE_CSV = 'text/csv'; -const CONTENT_TYPE_JSON = 'application/json'; +const CONTENT_TYPE_CSV = 'text/csv' +const CONTENT_TYPE_JSON = 'application/json' // need to configure here because the aws-sdk expects it // without it, the handler will throw an error @@ -53,7 +54,7 @@ beforeAll(async () => { CreateBucketConfiguration: { LocationConstraint: MOCK_REGION } - }).promise(); + }).promise() // create test database mockDb = newDb() @@ -91,7 +92,6 @@ beforeEach(() => { jest.spyOn(getDbConnection, 'getDbConnection').mockImplementationOnce(() => mockDbConnection) jest.spyOn(getJwtToken, 'getJwtToken').mockImplementation(() => 'mockJwt') - // Manage resetting ENV variables // TODO: This is causing problems with mocking knex but is noted as important for managing process.env // jest.resetModules() @@ -130,7 +130,10 @@ describe('exportSearchCheck', () => { const event = { body: JSON.stringify({ - key: MOCK_EXPORT_REQUEST_KEY + requestId: MOCK_REQUEST_ID, + data: { + key: MOCK_EXPORT_REQUEST_KEY + } }) } @@ -156,7 +159,10 @@ describe('exportSearchCheck', () => { const event = { body: JSON.stringify({ - key: MOCK_EXPORT_REQUEST_KEY + requestId: MOCK_REQUEST_ID, + data: { + key: MOCK_EXPORT_REQUEST_KEY + } }) } @@ -197,7 +203,10 @@ describe('exportSearchCheck', () => { const event = { body: JSON.stringify({ - key: MOCK_EXPORT_REQUEST_KEY + requestId: MOCK_REQUEST_ID, + data: { + key: MOCK_EXPORT_REQUEST_KEY + } }) } @@ -244,7 +253,10 @@ describe('exportSearchCheck', () => { const event = { body: JSON.stringify({ - key: MOCK_EXPORT_REQUEST_KEY + requestId: MOCK_REQUEST_ID, + data: { + key: MOCK_EXPORT_REQUEST_KEY + } }) } @@ -255,7 +267,7 @@ describe('exportSearchCheck', () => { const response = await axios.get(url, { transformResponse: data => data, // ensure treated as plain text - }); + }) expect(response.data).toEqual(MOCK_CSV) expect(response.status).toEqual(200) expect(response.headers['content-type']).toEqual(CONTENT_TYPE_CSV) diff --git a/serverless/src/exportSearchCheck/handler.js b/serverless/src/exportSearchCheck/handler.js index 3eded2123c..bdd0e9dd56 100644 --- a/serverless/src/exportSearchCheck/handler.js +++ b/serverless/src/exportSearchCheck/handler.js @@ -1,6 +1,8 @@ import AWS from 'aws-sdk' import { determineEarthdataEnvironment } from '../util/determineEarthdataEnvironment' +import { getS3Config } from '../util/aws/getS3Config' +import { getSearchExportBucket } from '../util/aws/getSearchExportBucket' import { getDbConnection } from '../util/database/getDbConnection' import { getApplicationConfig } from '../../../sharedUtils/config' import { getJwtToken } from '../util/getJwtToken' @@ -21,12 +23,21 @@ const exportSearchCheck = async (event, context) => { // eslint-disable-next-line no-param-reassign context.callbackWaitsForEmptyEventLoop = false - s3 ??= new AWS.S3({ - endpoint: process.env.searchExportS3Endpoint - }) + if (process.env.IS_OFFLINE) { + // fake/mock values when doing local development + AWS.config.update({ + accessKeyId: Math.random().toString(), + secretAccessKey: Math.random().toString(), + region: 'moon' + }) + } + + s3 ??= new AWS.S3(getS3Config()) const { body, headers } = event + if (!body) throw Error("post did not include a body") + const { defaultResponseHeaders } = getApplicationConfig() const earthdataEnvironment = determineEarthdataEnvironment(headers) @@ -39,7 +50,10 @@ const exportSearchCheck = async (event, context) => { // adding a little extra validation if (body.length > 1000) throw Error("body is too long") - const { key } = JSON.parse(body); + const { requestId, data } = JSON.parse(body) + console.log(`requestId: ${requestId}`) + + const { key } = data if (!key) { return { isBase64Encoded: false, @@ -51,6 +65,7 @@ const exportSearchCheck = async (event, context) => { body: JSON.stringify({ errors: ["missing key"] }) } } + console.log(`key: ${key}`) const dbConnection = await getDbConnection() @@ -73,7 +88,10 @@ const exportSearchCheck = async (event, context) => { } } - const exists = await doesObjectExist(s3, process.env.searchExportBucket, key) + const searchExportBucket = getSearchExportBucket() + if (!searchExportBucket) throw Error('failed to get bucket name') + + const exists = await doesObjectExist(s3, searchExportBucket, key) if (!exists) { return { isBase64Encoded: false, @@ -87,7 +105,7 @@ const exportSearchCheck = async (event, context) => { } const signedUrl = await s3.getSignedUrlPromise('getObject', { - Bucket: process.env.searchExportBucket, + Bucket: searchExportBucket, Key: key, Expires: 10 * 60 // pre-signed URL expires after 10 minutes }) @@ -99,7 +117,7 @@ const exportSearchCheck = async (event, context) => { ...defaultResponseHeaders, 'jwt-token': jwt }, - body: JSON.stringify({ state, url: signedUrl }) + body: JSON.stringify({ state, url: signedUrl }) } } From eb6779ea540cee95b40406054f044824279adb5b Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Thu, 23 Feb 2023 15:45:42 -0500 Subject: [PATCH 33/43] EDSC-3612: updated actions/exportSearch.js --- static/src/js/actions/exportSearch.js | 120 +++++++++++++++++++------- 1 file changed, 87 insertions(+), 33 deletions(-) diff --git a/static/src/js/actions/exportSearch.js b/static/src/js/actions/exportSearch.js index a4596c4e25..929a467042 100644 --- a/static/src/js/actions/exportSearch.js +++ b/static/src/js/actions/exportSearch.js @@ -1,4 +1,5 @@ import { EXPORT_FINISHED, EXPORT_STARTED } from '../constants/actionTypes' +import { getApplicationConfig } from '../../../../sharedUtils/config' import { getEarthdataEnvironment } from '../selectors/earthdataEnvironment' import { buildCollectionSearchParams, prepareCollectionParams } from '../util/collections' import ExportSearchRequest from '../util/request/exportSearchRequest' @@ -23,6 +24,8 @@ export const exportSearch = (format) => (dispatch, getState) => { const state = getState() + if (!state.authToken) throw new Error('you must log in before exporting search results') + // Retrieve data from Redux using selectors const earthdataEnvironment = getEarthdataEnvironment(state) @@ -121,6 +124,35 @@ export const exportSearch = (format) => (dispatch, getState) => { } }` + const searchExportParams = { + itempath: 'collections.items', + cursorpath: 'collections.cursor' + } + + if (format === 'csv') { + searchExportParams.columns = [ + { name: 'Data Provider', path: 'provider' }, + { name: 'Short Name', path: 'shortName' }, + { name: 'Version', path: 'versionId' }, + { name: 'Entry Title', path: 'title' }, + { name: 'Processing Level', path: 'processingLevelId' }, + { name: 'Platform', path: 'platforms.shortName' }, + { name: 'Start Time', path: 'timeStart' }, + { name: 'End Time', path: 'timeEnd' } + ] + } + + const handleExportSearchError = (error) => { + dispatch(onExportFinished(format)) + + dispatch(handleError({ + error, + action: 'exportSearch', + resource: 'collections', + graphQlRequestObject + })) + } + const response = graphQlRequestObject.search(graphQuery, { ...buildCollectionSearchParams(collectionParams), limit: 1000, @@ -130,45 +162,67 @@ export const exportSearch = (format) => (dispatch, getState) => { includeTags: undefined, pageNum: undefined, pageSize: undefined - }, format) - .then((response) => { - const { data } = response - - // Create a blob with the text data from the export - let blob - if (format === 'csv') { - blob = new Blob([data], { type: 'text/csv' }) + }, format, searchExportParams) + .then(async (response) => { + // key is a random uuid for this specific export request + // we use it to poll the status of the export later + const { key } = response.data + if (!key) throw Error('server did not response with a uuid for the export request, which we need in order to find the download') + + const { exportStatusRefreshTime = 5 * 1000 } = getApplicationConfig() + + const [error, signedUrl] = await new Promise((resolve) => { + const intervalId = setInterval(async () => { + try { + const response = await graphQlRequestObject.post('collections/export-check', { key }) + + const { data } = response + + if (typeof data !== 'object') { + clearInterval(intervalId) // stop polling + resolve(['server returned an invalid response', null]) + } + + const { state, url } = data + + if (state === 'DONE') { + clearInterval(intervalId) + resolve([null, url]) + } else if (state === 'FAILED') { + clearInterval(intervalId) + resolve(['server reported that the export has failed', null]) + } else if ([null, ''].includes(state)) { + clearInterval(intervalId) + resolve(['server returned an invalid state', null]) + } + } catch (error) { + clearInterval(intervalId) + resolve([error, null]) + } + }, exportStatusRefreshTime) + }) + + if (error) { + handleExportSearchError(error) } else { - blob = new Blob([JSON.stringify(data)]) - } - const url = window.URL.createObjectURL(blob) - - // Create a hyperlink to the blob and give it a filename - const link = document.createElement('a') - link.href = url - link.setAttribute('download', `edsc_collection_results_export.${format}`) + // Create a hyperlink to the export and give it a filename + const link = document.createElement('a') + link.href = signedUrl + link.setAttribute('download', `edsc_collection_results_export.${format}`) - // Add the link to the page - document.body.appendChild(link) + // Add the link to the page + document.body.appendChild(link) - // Click on the link to download the export file to the user's computer - link.click() + // Click on the link to download the export file to the user's computer + link.click() - // Remove the link from the page - link.parentNode.removeChild(link) - - dispatch(onExportFinished(format)) - }) - .catch((error) => { - dispatch(onExportFinished(format)) + // Remove the link from the page + link.parentNode.removeChild(link) - dispatch(handleError({ - error, - action: 'exportSearch', - resource: 'collections', - graphQlRequestObject - })) + dispatch(onExportFinished(format)) + } }) + .catch(handleExportSearchError) return response } From 23f2576d3f5ea50d62f2b0c3a978ca1c559ac1cc Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Thu, 23 Feb 2023 15:46:33 -0500 Subject: [PATCH 34/43] EDSC-3612: updated serverless.yml --- serverless.yml | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/serverless.yml b/serverless.yml index 3d228b89db..a557c2999a 100644 --- a/serverless.yml +++ b/serverless.yml @@ -15,9 +15,12 @@ provider: Fn::ImportValue: ${self:provider.stage}-DatabasePort dbUsername: edsc dbName: edsc_${self:provider.stage} - + searchExportBucket: + Ref: SearchExportBucket colorMapQueueUrl: Ref: ColorMapsProcessingQueue + searchExportQueueUrl: + Ref: SearchExportQueue tagQueueUrl: Ref: TagProcessingQueue legacyServicesQueueUrl: @@ -71,6 +74,8 @@ plugins: - serverless-step-functions - serverless-plugin-split-stacks - serverless-plugin-log-subscription + - serverless-offline-sqs # see https://github.com/CoorpAcademy/serverless-plugins/tree/master/packages/serverless-offline-sqs#installation + - ./serverless-plugins/serverless-offline-config-s3.js - serverless-offline - ./serverless-plugins/serverless-webpack-include-migrations.js @@ -152,6 +157,21 @@ custom: # https://github.com/dherault/serverless-offline#run-modes # useInProcess: true + serverless-offline-config-s3: + endpoint: http://0.0.0.0:5000 + region: moon + buckets: + - mock-search-export-bucket + + serverless-offline-sqs: + autoCreate: true + apiVersion: '2012-11-05' + endpoint: http://0.0.0.0:9324 + region: moon + accessKeyId: MOCK_ACCESS_KEY_ID + secretAccessKey: MOCK_SECRET_ACCESS_KEY + skipCacheInvalidation: false + # Serverless Webpack configurations webpack: webpackConfig: 'serverless.webpack.config.js' From cada0e7b06244438f73a8a4ea11b94dd35522859 Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Thu, 23 Feb 2023 15:47:46 -0500 Subject: [PATCH 35/43] EDSC-3612: updated package-lock.json --- package-lock.json | 3923 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 3923 insertions(+) diff --git a/package-lock.json b/package-lock.json index b5ab5d3ea2..ab8d5aaa4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -86,6 +86,7 @@ "node-pre-gyp": "^0.12.0", "node-sass": "^8.0.0", "number-abbreviate": "^2.0.0", + "papaparse": "^5.3.2", "pg": "^8.7.3", "postcss": "^8.4.19", "postcss-cli": "^10.0.0", @@ -130,6 +131,7 @@ "sanitize-html": "^2.7.0", "sass-loader": "^13.2.0", "scatter-swap": "^0.1.0", + "sendero": "^0.1.0", "serverless": "^3.24.1", "serverless-finch": "^4.0.0", "serverless-offline": "^11.3.0", @@ -183,9 +185,11 @@ "nock": "^12.0.3", "nyc": "^15.1.0", "parse-multipart": "^1.0.4", + "pg-mem": "^2.6.4", "process": "^0.11.10", "redux-logger": "^3.0.6", "redux-mock-store": "^1.5.4", + "serverless-offline-sqs": "^7.3.2", "snyk": "^1.913.0", "start-server-and-test": "^1.14.0", "three": "^0.127.0", @@ -4722,6 +4726,317 @@ "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==", "dev": true }, + "node_modules/@mikro-orm/core": { + "version": "4.5.10", + "resolved": "https://registry.npmjs.org/@mikro-orm/core/-/core-4.5.10.tgz", + "integrity": "sha512-vnSSFGSR/JoGINJlci5fafGSqvLgHx+3Nt3XjnCTNLOjQ0WL7LsdUVwM9FE/W5FipcJRaQfWmY/iLXBqnaarGQ==", + "dev": true, + "dependencies": { + "ansi-colors": "4.1.1", + "clone": "2.1.2", + "dotenv": "8.2.0", + "escaya": "0.0.61", + "fs-extra": "9.1.0", + "globby": "11.0.3", + "reflect-metadata": "0.1.13", + "strip-json-comments": "3.1.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "peerDependencies": { + "@mikro-orm/entity-generator": "^4.0.0", + "@mikro-orm/mariadb": "^4.0.0", + "@mikro-orm/migrations": "^4.0.0", + "@mikro-orm/mongodb": "^4.0.0", + "@mikro-orm/mysql": "^4.0.0", + "@mikro-orm/postgresql": "^4.0.0", + "@mikro-orm/sqlite": "^4.0.0" + }, + "peerDependenciesMeta": { + "@mikro-orm/entity-generator": { + "optional": true + }, + "@mikro-orm/mariadb": { + "optional": true + }, + "@mikro-orm/migrations": { + "optional": true + }, + "@mikro-orm/mongodb": { + "optional": true + }, + "@mikro-orm/mysql": { + "optional": true + }, + "@mikro-orm/postgresql": { + "optional": true + }, + "@mikro-orm/sqlite": { + "optional": true + } + } + }, + "node_modules/@mikro-orm/core/node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@mikro-orm/core/node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/@mikro-orm/core/node_modules/dotenv": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", + "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@mikro-orm/core/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@mikro-orm/core/node_modules/globby": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.3.tgz", + "integrity": "sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@mikro-orm/knex": { + "version": "4.5.10", + "resolved": "https://registry.npmjs.org/@mikro-orm/knex/-/knex-4.5.10.tgz", + "integrity": "sha512-iYSsAlHVNC7m+yz7dDA3JwjBHxyNuGNwQijUpJ6n2Vt55PyorcW4I3wTnHHALUS30Fvjm4TnfZRdio+aQhynsA==", + "dev": true, + "dependencies": { + "fs-extra": "9.1.0", + "knex": "0.21.19", + "sqlstring": "2.3.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "peerDependencies": { + "@mikro-orm/core": "^4.0.0", + "@mikro-orm/entity-generator": "^4.0.0", + "@mikro-orm/migrations": "^4.0.0", + "mssql": "^7.0.0", + "mysql": "^2.18.1", + "mysql2": "^2.1.0", + "pg": "^8.0.3", + "sqlite3": "^5.0.0" + }, + "peerDependenciesMeta": { + "@mikro-orm/entity-generator": { + "optional": true + }, + "@mikro-orm/migrations": { + "optional": true + }, + "mssql": { + "optional": true + }, + "mysql": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, + "node_modules/@mikro-orm/knex/node_modules/colorette": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz", + "integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==", + "dev": true + }, + "node_modules/@mikro-orm/knex/node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@mikro-orm/knex/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@mikro-orm/knex/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@mikro-orm/knex/node_modules/getopts": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/getopts/-/getopts-2.2.5.tgz", + "integrity": "sha512-9jb7AW5p3in+IiJWhQiZmmwkpLaR/ccTWdWQCtZM66HJcHHLegowh4q4tSD7gouUyeNvFWRavfK9GXosQHDpFA==", + "dev": true + }, + "node_modules/@mikro-orm/knex/node_modules/knex": { + "version": "0.21.19", + "resolved": "https://registry.npmjs.org/knex/-/knex-0.21.19.tgz", + "integrity": "sha512-6etvrq9XI1Ck6mEc/XiXFGVpD1Lmj6v9XWojqZgEbOvyMbW7XRvgZ99yIhN/kaBH+43FEy3xv/AcbRaH+1pJtw==", + "dev": true, + "dependencies": { + "colorette": "1.2.1", + "commander": "^6.2.0", + "debug": "4.3.1", + "esm": "^3.2.25", + "getopts": "2.2.5", + "interpret": "^2.2.0", + "liftoff": "3.1.0", + "lodash": "^4.17.20", + "pg-connection-string": "2.4.0", + "tarn": "^3.0.1", + "tildify": "2.0.0", + "v8flags": "^3.2.0" + }, + "bin": { + "knex": "bin/cli.js" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "mssql": "^6.2.1", + "mysql": "^2.18.1", + "mysql2": "^2.1.0", + "pg": "^8.3.0", + "sqlite3": "^5.0.0" + }, + "peerDependenciesMeta": { + "mssql": { + "optional": true + }, + "mysql": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, + "node_modules/@mikro-orm/knex/node_modules/pg-connection-string": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.4.0.tgz", + "integrity": "sha512-3iBXuv7XKvxeMrIgym7njT+HlZkwZqqGX4Bu9cci8xHZNT+Um1gWKqCsAzcC0d95rcKMU5WBg6YRUcHyV0HZKQ==", + "dev": true + }, + "node_modules/@mikro-orm/postgresql": { + "version": "4.5.10", + "resolved": "https://registry.npmjs.org/@mikro-orm/postgresql/-/postgresql-4.5.10.tgz", + "integrity": "sha512-hVTW5rO43T3k4AN3e1tu2rUZIAN0VuBrizdf+sGYpKZ7xtLAOFXY10TCmLdv/B6euAwrXRSsCraOOT7JnOQyBg==", + "dev": true, + "dependencies": { + "@mikro-orm/knex": "^4.5.10", + "pg": "8.6.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "peerDependencies": { + "@mikro-orm/core": "^4.0.0" + } + }, + "node_modules/@mikro-orm/postgresql/node_modules/pg": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.6.0.tgz", + "integrity": "sha512-qNS9u61lqljTDFvmk/N66EeGq3n6Ujzj0FFyNMGQr6XuEv4tgNTXvJQTfJdcvGit5p5/DWPu+wj920hAJFI+QQ==", + "dev": true, + "dependencies": { + "buffer-writer": "2.0.0", + "packet-reader": "1.0.0", + "pg-connection-string": "^2.5.0", + "pg-pool": "^3.3.0", + "pg-protocol": "^1.5.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "pg-native": ">=2.0.0" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -7173,6 +7488,42 @@ "node": ">=6.0" } }, + "node_modules/arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", + "integrity": "sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/array-flatten": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", @@ -7202,6 +7553,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-slice": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", + "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/array-to-sentence": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/array-to-sentence/-/array-to-sentence-2.0.0.tgz", @@ -7215,6 +7575,15 @@ "node": ">=8" } }, + "node_modules/array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/array.prototype.filter": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array.prototype.filter/-/array.prototype.filter-1.0.2.tgz", @@ -7367,6 +7736,15 @@ "node": ">=0.8" } }, + "node_modules/assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ast-types-flow": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", @@ -7406,6 +7784,18 @@ "node": ">= 4.0.0" } }, + "node_modules/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true, + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, "node_modules/autobind-decorator": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/autobind-decorator/-/autobind-decorator-1.4.3.tgz", @@ -8118,6 +8508,36 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "dependencies": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -8694,6 +9114,26 @@ "node": ">= 10" } }, + "node_modules/cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "dependencies": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/cacheable-lookup": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", @@ -9102,6 +9542,104 @@ "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==", "dev": true }, + "node_modules/class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "dependencies": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/classnames": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", @@ -9335,6 +9873,19 @@ "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", "dev": true }, + "node_modules/collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==", + "dev": true, + "dependencies": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/color": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", @@ -9696,6 +10247,15 @@ "node": ">= 0.8" } }, + "node_modules/copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/copy-webpack-plugin": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-6.4.1.tgz", @@ -11072,6 +11632,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -11128,6 +11701,15 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/detect-indent": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", @@ -11848,6 +12430,15 @@ "node": ">=0.8.0" } }, + "node_modules/escaya": { + "version": "0.0.61", + "resolved": "https://registry.npmjs.org/escaya/-/escaya-0.0.61.tgz", + "integrity": "sha512-WLLmvdG72Z0pCq8XUBd03GEJlAiMceXFanjdQeEzeSiuV1ZgrJqbkU7ZEe/hu0OsBlg5wLlySEeOvfzcGoO8mg==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/escodegen": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", @@ -12753,6 +13344,155 @@ "node": ">= 0.8.0" } }, + "node_modules/expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==", + "dev": true, + "dependencies": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/expand-brackets/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", + "dev": true, + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/expect": { "version": "28.1.3", "resolved": "https://registry.npmjs.org/expect/-/expect-28.1.3.tgz", @@ -12883,6 +13623,19 @@ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, + "node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "dev": true, + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -12907,6 +13660,58 @@ "node": ">=0.6.0" } }, + "node_modules/extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "dependencies": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -13262,6 +14067,176 @@ "micromatch": "^4.0.2" } }, + "node_modules/findup-sync": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", + "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", + "dev": true, + "dependencies": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/findup-sync/node_modules/braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "dependencies": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/findup-sync/node_modules/braces/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/findup-sync/node_modules/fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/findup-sync/node_modules/fill-range/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/findup-sync/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/findup-sync/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/findup-sync/node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/findup-sync/node_modules/micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/findup-sync/node_modules/to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", + "dev": true, + "dependencies": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fined": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", + "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", + "dev": true, + "dependencies": { + "expand-tilde": "^2.0.2", + "is-plain-object": "^2.0.3", + "object.defaults": "^1.1.0", + "object.pick": "^1.2.0", + "parse-filepath": "^1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/flagged-respawn": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", + "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/flat": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", @@ -13314,6 +14289,27 @@ "is-callable": "^1.1.3" } }, + "node_modules/for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==", + "dev": true, + "dependencies": { + "for-in": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/foreground-child": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", @@ -13447,6 +14443,18 @@ "url": "https://www.patreon.com/infusion" } }, + "node_modules/fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==", + "dev": true, + "dependencies": { + "map-cache": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -13579,6 +14587,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "dev": true + }, "node_modules/functions-have-names": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", @@ -13738,6 +14752,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/getopts": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/getopts/-/getopts-2.3.0.tgz", @@ -13808,6 +14831,54 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "dependencies": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", + "dev": true, + "dependencies": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -14046,6 +15117,69 @@ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" }, + "node_modules/has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==", + "dev": true, + "dependencies": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==", + "dev": true, + "dependencies": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values/node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values/node_modules/kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/hash-base": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", @@ -14145,6 +15279,18 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "dev": true, + "dependencies": { + "parse-passwd": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/hosted-git-info": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", @@ -14771,6 +15917,31 @@ "resolved": "https://registry.npmjs.org/is_js/-/is_js-0.9.0.tgz", "integrity": "sha512-8Y5EHSH+TonfUHX2g3pMJljdbGavg55q4jmHzghJCdqYDbdNROC8uw/YFQwIRCRqRJT1EY3pJefz+kglw+o7sg==" }, + "node_modules/is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "dev": true, + "dependencies": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-arguments": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", @@ -14881,6 +16052,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-date-object": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", @@ -14895,6 +16078,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-docker": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", @@ -14909,6 +16106,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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==", + "dev": true, + "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", @@ -15114,6 +16323,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "dev": true, + "dependencies": { + "is-unc-path": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-retry-allowed": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", @@ -15213,6 +16434,18 @@ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" }, + "node_modules/is-unc-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "dev": true, + "dependencies": { + "unc-path-regex": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -17907,6 +19140,18 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, + "node_modules/json-stable-stringify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.2.tgz", + "integrity": "sha512-eunSSaEnxV12z+Z73y/j5N37/In40GK4GmsSy+tEHJMxknvqnA7/djeYtAgW0GsWHUfg+847WJjKaEylk2y09g==", + "dev": true, + "dependencies": { + "jsonify": "^0.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -17939,6 +19184,15 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", + "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/jsonpath-plus": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-7.2.0.tgz", @@ -18294,6 +19548,37 @@ "immediate": "~3.0.5" } }, + "node_modules/liftoff": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", + "integrity": "sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog==", + "dev": true, + "dependencies": { + "extend": "^3.0.0", + "findup-sync": "^3.0.0", + "fined": "^1.0.1", + "flagged-respawn": "^1.0.0", + "is-plain-object": "^2.0.4", + "object.map": "^1.0.0", + "rechoir": "^0.6.2", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/liftoff/node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "dev": true, + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/lilconfig": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", @@ -18985,6 +20270,18 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/make-iterator": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", + "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -18994,6 +20291,15 @@ "tmpl": "1.0.5" } }, + "node_modules/map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/map-obj": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", @@ -19011,6 +20317,18 @@ "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==", "dev": true }, + "node_modules/map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==", + "dev": true, + "dependencies": { + "object-visit": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/md5": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", @@ -19399,6 +20717,19 @@ "node": ">= 8" } }, + "node_modules/mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "dev": true, + "dependencies": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -19510,6 +20841,28 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/native-promise-only": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/native-promise-only/-/native-promise-only-0.8.1.tgz", @@ -20556,6 +21909,91 @@ "node": ">=0.10.0" } }, + "node_modules/object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==", + "dev": true, + "dependencies": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/is-descriptor/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-hash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", @@ -20596,6 +22034,18 @@ "node": ">= 0.4" } }, + "node_modules/object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==", + "dev": true, + "dependencies": { + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object.assign": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", @@ -20613,6 +22063,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object.defaults": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", + "integrity": "sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA==", + "dev": true, + "dependencies": { + "array-each": "^1.0.1", + "array-slice": "^1.0.0", + "for-own": "^1.0.0", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object.entries": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.6.tgz", @@ -20654,6 +22119,31 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object.map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", + "integrity": "sha512-3+mAJu2PLfnSVGHwIWubpOFLscJANBKuB/6A4CxBstc4aqwQY0FWcsppuy4jU5GSB95yES5JHSI+33AWuS4k6w==", + "dev": true, + "dependencies": { + "for-own": "^1.0.0", + "make-iterator": "^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==", + "dev": true, + "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", @@ -20995,6 +22485,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "dev": true, + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-retry": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-5.1.2.tgz", @@ -21062,6 +22568,11 @@ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" }, + "node_modules/papaparse": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.3.2.tgz", + "integrity": "sha512-6dNZu0Ki+gyV0eBsFKJhYr+MdQYAzFUGlBMNj3GNrmHxmz1lfRa24CjFObPXtjcetlOv5Ad299MhIK0znp3afw==" + }, "node_modules/param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", @@ -21099,6 +22610,20 @@ "safe-buffer": "^5.1.1" } }, + "node_modules/parse-filepath": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", + "integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==", + "dev": true, + "dependencies": { + "is-absolute": "^1.0.0", + "map-cache": "^0.2.0", + "path-root": "^0.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -21122,6 +22647,15 @@ "integrity": "sha512-BQBmA/1qJzNnrPnRUzJhqGwbybsXa6Ot8vII5w+JT8gp2p7h9oWHwgAc29ohp669ZPzIy9izLi8ali9ngGmuAQ==", "dev": true }, + "node_modules/parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/parse-srcset": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", @@ -21175,6 +22709,15 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" }, + "node_modules/pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path": { "version": "0.12.7", "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", @@ -21222,6 +22765,27 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, + "node_modules/path-root": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", + "integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==", + "dev": true, + "dependencies": { + "path-root-regex": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-root-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", + "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-to-regexp": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", @@ -21345,6 +22909,55 @@ "node": ">=4.0.0" } }, + "node_modules/pg-mem": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/pg-mem/-/pg-mem-2.6.4.tgz", + "integrity": "sha512-0QYoy2/jVmZF6Z3nCwQndhw/fAfrWM2vrVsJ+TOzlo1mxk3wBMQzUxsuPLcsZcSm4j6J+ACUHy2TIdz7nm9AcQ==", + "dev": true, + "dependencies": { + "@mikro-orm/core": "^4.5.3", + "@mikro-orm/postgresql": "^4.5.3", + "functional-red-black-tree": "^1.0.1", + "immutable": "^4.0.0-rc.12", + "json-stable-stringify": "^1.0.1", + "lru-cache": "^6.0.0", + "moment": "^2.27.0", + "object-hash": "^2.0.3", + "pgsql-ast-parser": "^10.5.2" + }, + "peerDependencies": { + "knex": ">=0.20", + "pg-promise": ">=10.8.7", + "slonik": ">=23.0.1", + "typeorm": ">=0.2.29" + }, + "peerDependenciesMeta": { + "knex": { + "optional": true + }, + "mikro-orm": { + "optional": true + }, + "pg-promise": { + "optional": true + }, + "slonik": { + "optional": true + }, + "typeorm": { + "optional": true + } + } + }, + "node_modules/pg-mem/node_modules/object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/pg-pool": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.5.2.tgz", @@ -21381,6 +22994,16 @@ "split2": "^4.1.0" } }, + "node_modules/pgsql-ast-parser": { + "version": "10.5.2", + "resolved": "https://registry.npmjs.org/pgsql-ast-parser/-/pgsql-ast-parser-10.5.2.tgz", + "integrity": "sha512-GkGhxSPiGlLlT6dRpeTveK8g0meJZuQldhbBmlFWwjoNp4SePoS2n5cCS2bmjVUkI5ugvvSnfl1WMIB36IQzxA==", + "dev": true, + "dependencies": { + "moo": "^0.5.1", + "nearley": "^2.19.5" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -21510,6 +23133,15 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/postcss": { "version": "8.4.19", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.19.tgz", @@ -23424,6 +25056,12 @@ "redux": "^4" } }, + "node_modules/reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", + "dev": true + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -23453,6 +25091,19 @@ "@babel/runtime": "^7.8.4" } }, + "node_modules/regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "dependencies": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/regex-parser": { "version": "2.2.11", "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.11.tgz", @@ -23638,6 +25289,24 @@ "entities": "^2.0.0" } }, + "node_modules/repeat-element": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", + "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, "node_modules/repeating": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", @@ -23723,6 +25392,19 @@ "node": ">=8" } }, + "node_modules/resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", + "dev": true, + "dependencies": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", @@ -23736,6 +25418,13 @@ "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==" }, + "node_modules/resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==", + "deprecated": "https://github.com/lydell/resolve-url#deprecated", + "dev": true + }, "node_modules/resolve-url-loader": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", @@ -23942,6 +25631,15 @@ } ] }, + "node_modules/safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==", + "dev": true, + "dependencies": { + "ret": "~0.1.10" + } + }, "node_modules/safe-regex-test": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", @@ -24308,6 +26006,11 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, + "node_modules/sendero": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/sendero/-/sendero-0.1.0.tgz", + "integrity": "sha512-YLWCsWHhGf06iCTOMSOAv+N3cqs/gusJAnU1k1krtDlbvbDIkdb9vDp1/0/UFMSZTA8BF0w6n4CHjtBioef4wA==" + }, "node_modules/serialize-javascript": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", @@ -24538,6 +26241,23 @@ "serverless": "^3.2.0" } }, + "node_modules/serverless-offline-sqs": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/serverless-offline-sqs/-/serverless-offline-sqs-7.3.2.tgz", + "integrity": "sha512-e2qsDdyidBYVct+UNCnw1tNjSwjbmQlw4iD0oe3SsH2X29UBL1lJkZbtnAPimWqoDsdSNIhU7vg70f1GYSmecQ==", + "dev": true, + "dependencies": { + "aws-sdk": "^2.1234.0", + "lodash": "^4.17.21", + "p-queue": "^6.6.2" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "serverless-offline": "^10.0.2 || ^11 || ^12" + } + }, "node_modules/serverless-offline/node_modules/chalk": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.1.2.tgz", @@ -25157,6 +26877,42 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, + "node_modules/set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/set-value/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/set-value/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -25427,6 +27183,203 @@ "node": ">=6" } }, + "node_modules/snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "dependencies": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "dependencies": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "dependencies": { + "kind-of": "^3.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-util/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/snapdragon/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/snapdragon/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/snyk": { "version": "1.1069.0", "resolved": "https://registry.npmjs.org/snyk/-/snyk-1.1069.0.tgz", @@ -25524,6 +27477,20 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", + "dev": true, + "dependencies": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -25533,6 +27500,13 @@ "source-map": "^0.6.0" } }, + "node_modules/source-map-url": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", + "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", + "deprecated": "See https://github.com/lydell/source-map-url#deprecated", + "dev": true + }, "node_modules/spawn-wrap": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", @@ -25643,6 +27617,18 @@ "node": ">=6" } }, + "node_modules/split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "dependencies": { + "extend-shallow": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/split2": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz", @@ -25664,6 +27650,15 @@ "es5-ext": "^0.10.53" } }, + "node_modules/sqlstring": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.2.tgz", + "integrity": "sha512-vF4ZbYdKS8OnoJAWBmMxCQDkiEBkGQYU7UZPtL8flbDRSNkhaXvRJ279ZtI6M+zDaQovVU4tuRgzK5fVhvFAhg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/sshpk": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", @@ -25800,6 +27795,102 @@ "node": ">=10.17.0" } }, + "node_modules/static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==", + "dev": true, + "dependencies": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -26707,6 +28798,45 @@ "resolved": "https://registry.npmjs.org/to-no-case/-/to-no-case-1.0.2.tgz", "integrity": "sha512-Z3g735FxuZY8rodxV4gH7LxClE4H0hTIyHNIHdk+vpQxjLm0cwnKXq/OFVZ76SOQmto7txVcwSCwkU5kqp+FKg==" }, + "node_modules/to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-object-path/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "dependencies": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -27083,6 +29213,15 @@ "ieee754": "^1.1.13" } }, + "node_modules/unc-path-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/uncontrollable": { "version": "7.2.1", "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", @@ -27146,6 +29285,30 @@ "node": ">=4" } }, + "node_modules/union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dev": true, + "dependencies": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/union-value/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/unique-filename": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", @@ -27179,6 +29342,54 @@ "node": ">= 0.8" } }, + "node_modules/unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==", + "dev": true, + "dependencies": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==", + "dev": true, + "dependencies": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-value/node_modules/isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", + "dev": true, + "dependencies": { + "isarray": "1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/untildify": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", @@ -27220,6 +29431,13 @@ "punycode": "^2.1.0" } }, + "node_modules/urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==", + "deprecated": "Please see https://github.com/lydell/urix#deprecated", + "dev": true + }, "node_modules/url": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", @@ -27249,6 +29467,15 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" }, + "node_modules/use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/util": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", @@ -27308,6 +29535,18 @@ "node": ">=10.12.0" } }, + "node_modules/v8flags": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", + "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", + "dev": true, + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -32255,6 +34494,173 @@ "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==", "dev": true }, + "@mikro-orm/core": { + "version": "4.5.10", + "resolved": "https://registry.npmjs.org/@mikro-orm/core/-/core-4.5.10.tgz", + "integrity": "sha512-vnSSFGSR/JoGINJlci5fafGSqvLgHx+3Nt3XjnCTNLOjQ0WL7LsdUVwM9FE/W5FipcJRaQfWmY/iLXBqnaarGQ==", + "dev": true, + "requires": { + "ansi-colors": "4.1.1", + "clone": "2.1.2", + "dotenv": "8.2.0", + "escaya": "0.0.61", + "fs-extra": "9.1.0", + "globby": "11.0.3", + "reflect-metadata": "0.1.13", + "strip-json-comments": "3.1.1" + }, + "dependencies": { + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "dev": true + }, + "dotenv": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", + "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==", + "dev": true + }, + "fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "globby": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.3.tgz", + "integrity": "sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + } + } + } + }, + "@mikro-orm/knex": { + "version": "4.5.10", + "resolved": "https://registry.npmjs.org/@mikro-orm/knex/-/knex-4.5.10.tgz", + "integrity": "sha512-iYSsAlHVNC7m+yz7dDA3JwjBHxyNuGNwQijUpJ6n2Vt55PyorcW4I3wTnHHALUS30Fvjm4TnfZRdio+aQhynsA==", + "dev": true, + "requires": { + "fs-extra": "9.1.0", + "knex": "0.21.19", + "sqlstring": "2.3.2" + }, + "dependencies": { + "colorette": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz", + "integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==", + "dev": true + }, + "commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "getopts": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/getopts/-/getopts-2.2.5.tgz", + "integrity": "sha512-9jb7AW5p3in+IiJWhQiZmmwkpLaR/ccTWdWQCtZM66HJcHHLegowh4q4tSD7gouUyeNvFWRavfK9GXosQHDpFA==", + "dev": true + }, + "knex": { + "version": "0.21.19", + "resolved": "https://registry.npmjs.org/knex/-/knex-0.21.19.tgz", + "integrity": "sha512-6etvrq9XI1Ck6mEc/XiXFGVpD1Lmj6v9XWojqZgEbOvyMbW7XRvgZ99yIhN/kaBH+43FEy3xv/AcbRaH+1pJtw==", + "dev": true, + "requires": { + "colorette": "1.2.1", + "commander": "^6.2.0", + "debug": "4.3.1", + "esm": "^3.2.25", + "getopts": "2.2.5", + "interpret": "^2.2.0", + "liftoff": "3.1.0", + "lodash": "^4.17.20", + "pg-connection-string": "2.4.0", + "tarn": "^3.0.1", + "tildify": "2.0.0", + "v8flags": "^3.2.0" + } + }, + "pg-connection-string": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.4.0.tgz", + "integrity": "sha512-3iBXuv7XKvxeMrIgym7njT+HlZkwZqqGX4Bu9cci8xHZNT+Um1gWKqCsAzcC0d95rcKMU5WBg6YRUcHyV0HZKQ==", + "dev": true + } + } + }, + "@mikro-orm/postgresql": { + "version": "4.5.10", + "resolved": "https://registry.npmjs.org/@mikro-orm/postgresql/-/postgresql-4.5.10.tgz", + "integrity": "sha512-hVTW5rO43T3k4AN3e1tu2rUZIAN0VuBrizdf+sGYpKZ7xtLAOFXY10TCmLdv/B6euAwrXRSsCraOOT7JnOQyBg==", + "dev": true, + "requires": { + "@mikro-orm/knex": "^4.5.10", + "pg": "8.6.0" + }, + "dependencies": { + "pg": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.6.0.tgz", + "integrity": "sha512-qNS9u61lqljTDFvmk/N66EeGq3n6Ujzj0FFyNMGQr6XuEv4tgNTXvJQTfJdcvGit5p5/DWPu+wj920hAJFI+QQ==", + "dev": true, + "requires": { + "buffer-writer": "2.0.0", + "packet-reader": "1.0.0", + "pg-connection-string": "^2.5.0", + "pg-pool": "^3.3.0", + "pg-protocol": "^1.5.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + } + } + } + }, "@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -34282,6 +36688,30 @@ "@babel/runtime-corejs3": "^7.10.2" } }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", + "dev": true + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", + "dev": true + }, + "array-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", + "integrity": "sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA==", + "dev": true + }, "array-flatten": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", @@ -34305,6 +36735,12 @@ "is-string": "^1.0.7" } }, + "array-slice": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", + "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", + "dev": true + }, "array-to-sentence": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/array-to-sentence/-/array-to-sentence-2.0.0.tgz", @@ -34315,6 +36751,12 @@ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==" }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==", + "dev": true + }, "array.prototype.filter": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array.prototype.filter/-/array.prototype.filter-1.0.2.tgz", @@ -34440,6 +36882,12 @@ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==" }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", + "dev": true + }, "ast-types-flow": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", @@ -34470,6 +36918,12 @@ "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==" }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true + }, "autobind-decorator": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/autobind-decorator/-/autobind-decorator-1.4.3.tgz", @@ -35051,6 +37505,32 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + } + } + }, "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -35485,6 +37965,23 @@ "unique-filename": "^1.1.1" } }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, "cacheable-lookup": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", @@ -35792,6 +38289,86 @@ "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==", "dev": true }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, "classnames": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", @@ -35958,6 +38535,16 @@ "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", "dev": true }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==", + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, "color": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", @@ -36249,6 +38836,12 @@ "keygrip": "~1.0.3" } }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==", + "dev": true + }, "copy-webpack-plugin": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-6.4.1.tgz", @@ -37276,6 +39869,16 @@ "object-keys": "^1.1.1" } }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + } + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -37316,6 +39919,12 @@ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", "dev": true }, + "detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", + "dev": true + }, "detect-indent": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", @@ -37920,6 +40529,12 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" }, + "escaya": { + "version": "0.0.61", + "resolved": "https://registry.npmjs.org/escaya/-/escaya-0.0.61.tgz", + "integrity": "sha512-WLLmvdG72Z0pCq8XUBd03GEJlAiMceXFanjdQeEzeSiuV1ZgrJqbkU7ZEe/hu0OsBlg5wLlySEeOvfzcGoO8mg==", + "dev": true + }, "escodegen": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", @@ -38577,6 +41192,128 @@ "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", "dev": true }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + } + } + }, + "expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", + "dev": true, + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, "expect": { "version": "28.1.3", "resolved": "https://registry.npmjs.org/expect/-/expect-28.1.3.tgz", @@ -38694,6 +41431,16 @@ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, "external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -38714,6 +41461,48 @@ } } }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + } + } + }, "extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -38971,6 +41760,148 @@ "micromatch": "^4.0.2" } }, + "findup-sync": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", + "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", + "dev": true, + "requires": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + }, + "dependencies": { + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "fined": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", + "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", + "dev": true, + "requires": { + "expand-tilde": "^2.0.2", + "is-plain-object": "^2.0.3", + "object.defaults": "^1.1.0", + "object.pick": "^1.2.0", + "parse-filepath": "^1.0.1" + } + }, + "flagged-respawn": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", + "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==", + "dev": true + }, "flat": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", @@ -39003,6 +41934,21 @@ "is-callable": "^1.1.3" } }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "dev": true + }, + "for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==", + "dev": true, + "requires": { + "for-in": "^1.0.1" + } + }, "foreground-child": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", @@ -39095,6 +42041,15 @@ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==" }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==", + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -39188,6 +42143,12 @@ "functions-have-names": "^1.2.2" } }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "dev": true + }, "functions-have-names": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", @@ -39301,6 +42262,12 @@ "get-intrinsic": "^1.1.1" } }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", + "dev": true + }, "getopts": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/getopts/-/getopts-2.3.0.tgz", @@ -39356,6 +42323,47 @@ "ini": "2.0.0" } }, + "global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "requires": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + } + }, + "global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", + "dev": true, + "requires": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + }, + "dependencies": { + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, "globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -39529,6 +42537,58 @@ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==", + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, "hash-base": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", @@ -39614,6 +42674,15 @@ } } }, + "homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "dev": true, + "requires": { + "parse-passwd": "^1.0.0" + } + }, "hosted-git-info": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", @@ -40097,6 +43166,25 @@ "resolved": "https://registry.npmjs.org/is_js/-/is_js-0.9.0.tgz", "integrity": "sha512-8Y5EHSH+TonfUHX2g3pMJljdbGavg55q4jmHzghJCdqYDbdNROC8uw/YFQwIRCRqRJT1EY3pJefz+kglw+o7sg==" }, + "is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "dev": true, + "requires": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, "is-arguments": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", @@ -40171,6 +43259,15 @@ "has": "^1.0.3" } }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, "is-date-object": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", @@ -40179,11 +43276,31 @@ "has-tostringtag": "^1.0.0" } }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, "is-docker": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==" }, + "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==", + "dev": true, + "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", @@ -40317,6 +43434,15 @@ "has-tostringtag": "^1.0.0" } }, + "is-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "dev": true, + "requires": { + "is-unc-path": "^1.0.0" + } + }, "is-retry-allowed": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", @@ -40380,6 +43506,15 @@ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" }, + "is-unc-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "dev": true, + "requires": { + "unc-path-regex": "^0.1.2" + } + }, "is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -42412,6 +45547,15 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, + "json-stable-stringify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.2.tgz", + "integrity": "sha512-eunSSaEnxV12z+Z73y/j5N37/In40GK4GmsSy+tEHJMxknvqnA7/djeYtAgW0GsWHUfg+847WJjKaEylk2y09g==", + "dev": true, + "requires": { + "jsonify": "^0.0.1" + } + }, "json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -42436,6 +45580,12 @@ "universalify": "^2.0.0" } }, + "jsonify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", + "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", + "dev": true + }, "jsonpath-plus": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-7.2.0.tgz", @@ -42715,6 +45865,33 @@ "immediate": "~3.0.5" } }, + "liftoff": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", + "integrity": "sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog==", + "dev": true, + "requires": { + "extend": "^3.0.0", + "findup-sync": "^3.0.0", + "fined": "^1.0.1", + "flagged-respawn": "^1.0.0", + "is-plain-object": "^2.0.4", + "object.map": "^1.0.0", + "rechoir": "^0.6.2", + "resolve": "^1.1.7" + }, + "dependencies": { + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "dev": true, + "requires": { + "resolve": "^1.1.6" + } + } + } + }, "lilconfig": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", @@ -43258,6 +46435,15 @@ } } }, + "make-iterator": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", + "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", + "dev": true, + "requires": { + "kind-of": "^6.0.2" + } + }, "makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -43267,6 +46453,12 @@ "tmpl": "1.0.5" } }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", + "dev": true + }, "map-obj": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", @@ -43278,6 +46470,15 @@ "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==", "dev": true }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==", + "dev": true, + "requires": { + "object-visit": "^1.0.0" + } + }, "md5": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", @@ -43569,6 +46770,16 @@ "yallist": "^4.0.0" } }, + "mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "dev": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + } + }, "mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -43655,6 +46866,25 @@ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==" }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, "native-promise-only": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/native-promise-only/-/native-promise-only-0.8.1.tgz", @@ -44473,6 +47703,74 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==", + "dev": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, "object-hash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", @@ -44498,6 +47796,15 @@ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==", + "dev": true, + "requires": { + "isobject": "^3.0.0" + } + }, "object.assign": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", @@ -44509,6 +47816,18 @@ "object-keys": "^1.1.1" } }, + "object.defaults": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", + "integrity": "sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA==", + "dev": true, + "requires": { + "array-each": "^1.0.1", + "array-slice": "^1.0.0", + "for-own": "^1.0.0", + "isobject": "^3.0.0" + } + }, "object.entries": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.6.tgz", @@ -44538,6 +47857,25 @@ "es-abstract": "^1.20.4" } }, + "object.map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", + "integrity": "sha512-3+mAJu2PLfnSVGHwIWubpOFLscJANBKuB/6A4CxBstc4aqwQY0FWcsppuy4jU5GSB95yES5JHSI+33AWuS4k6w==", + "dev": true, + "requires": { + "for-own": "^1.0.0", + "make-iterator": "^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==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, "object.values": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", @@ -44771,6 +48109,16 @@ } } }, + "p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "dev": true, + "requires": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + } + }, "p-retry": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-5.1.2.tgz", @@ -44822,6 +48170,11 @@ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" }, + "papaparse": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.3.2.tgz", + "integrity": "sha512-6dNZu0Ki+gyV0eBsFKJhYr+MdQYAzFUGlBMNj3GNrmHxmz1lfRa24CjFObPXtjcetlOv5Ad299MhIK0znp3afw==" + }, "param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", @@ -44858,6 +48211,17 @@ "safe-buffer": "^5.1.1" } }, + "parse-filepath": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", + "integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==", + "dev": true, + "requires": { + "is-absolute": "^1.0.0", + "map-cache": "^0.2.0", + "path-root": "^0.1.1" + } + }, "parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -44875,6 +48239,12 @@ "integrity": "sha512-BQBmA/1qJzNnrPnRUzJhqGwbybsXa6Ot8vII5w+JT8gp2p7h9oWHwgAc29ohp669ZPzIy9izLi8ali9ngGmuAQ==", "dev": true }, + "parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", + "dev": true + }, "parse-srcset": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", @@ -44921,6 +48291,12 @@ } } }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==", + "dev": true + }, "path": { "version": "0.12.7", "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", @@ -44974,6 +48350,21 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, + "path-root": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", + "integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==", + "dev": true, + "requires": { + "path-root-regex": "^0.1.0" + } + }, + "path-root-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", + "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==", + "dev": true + }, "path-to-regexp": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", @@ -45059,6 +48450,31 @@ "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==" }, + "pg-mem": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/pg-mem/-/pg-mem-2.6.4.tgz", + "integrity": "sha512-0QYoy2/jVmZF6Z3nCwQndhw/fAfrWM2vrVsJ+TOzlo1mxk3wBMQzUxsuPLcsZcSm4j6J+ACUHy2TIdz7nm9AcQ==", + "dev": true, + "requires": { + "@mikro-orm/core": "^4.5.3", + "@mikro-orm/postgresql": "^4.5.3", + "functional-red-black-tree": "^1.0.1", + "immutable": "^4.0.0-rc.12", + "json-stable-stringify": "^1.0.1", + "lru-cache": "^6.0.0", + "moment": "^2.27.0", + "object-hash": "^2.0.3", + "pgsql-ast-parser": "^10.5.2" + }, + "dependencies": { + "object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "dev": true + } + } + }, "pg-pool": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.5.2.tgz", @@ -45090,6 +48506,16 @@ "split2": "^4.1.0" } }, + "pgsql-ast-parser": { + "version": "10.5.2", + "resolved": "https://registry.npmjs.org/pgsql-ast-parser/-/pgsql-ast-parser-10.5.2.tgz", + "integrity": "sha512-GkGhxSPiGlLlT6dRpeTveK8g0meJZuQldhbBmlFWwjoNp4SePoS2n5cCS2bmjVUkI5ugvvSnfl1WMIB36IQzxA==", + "dev": true, + "requires": { + "moo": "^0.5.1", + "nearley": "^2.19.5" + } + }, "picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -45177,6 +48603,12 @@ "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", "peer": true }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==", + "dev": true + }, "postcss": { "version": "8.4.19", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.19.tgz", @@ -46517,6 +49949,12 @@ "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", "requires": {} }, + "reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", + "dev": true + }, "regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -46543,6 +49981,16 @@ "@babel/runtime": "^7.8.4" } }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, "regex-parser": { "version": "2.2.11", "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.11.tgz", @@ -46680,6 +50128,18 @@ } } }, + "repeat-element": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", + "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true + }, "repeating": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", @@ -46747,6 +50207,16 @@ "resolve-from": "^5.0.0" } }, + "resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", + "dev": true, + "requires": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + } + }, "resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", @@ -46757,6 +50227,12 @@ "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==" }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==", + "dev": true + }, "resolve-url-loader": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", @@ -46887,6 +50363,15 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, "safe-regex-test": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", @@ -47157,6 +50642,11 @@ } } }, + "sendero": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/sendero/-/sendero-0.1.0.tgz", + "integrity": "sha512-YLWCsWHhGf06iCTOMSOAv+N3cqs/gusJAnU1k1krtDlbvbDIkdb9vDp1/0/UFMSZTA8BF0w6n4CHjtBioef4wA==" + }, "serialize-javascript": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", @@ -47639,6 +51129,17 @@ } } }, + "serverless-offline-sqs": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/serverless-offline-sqs/-/serverless-offline-sqs-7.3.2.tgz", + "integrity": "sha512-e2qsDdyidBYVct+UNCnw1tNjSwjbmQlw4iD0oe3SsH2X29UBL1lJkZbtnAPimWqoDsdSNIhU7vg70f1GYSmecQ==", + "dev": true, + "requires": { + "aws-sdk": "^2.1234.0", + "lodash": "^4.17.21", + "p-queue": "^6.6.2" + } + }, "serverless-plugin-log-subscription": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/serverless-plugin-log-subscription/-/serverless-plugin-log-subscription-2.1.5.tgz", @@ -47776,6 +51277,35 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, + "set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + } + } + }, "setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -48004,6 +51534,168 @@ } } }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, "snyk": { "version": "1.1069.0", "resolved": "https://registry.npmjs.org/snyk/-/snyk-1.1069.0.tgz", @@ -48076,6 +51768,19 @@ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" }, + "source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "dev": true, + "requires": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, "source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -48085,6 +51790,12 @@ "source-map": "^0.6.0" } }, + "source-map-url": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", + "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", + "dev": true + }, "spawn-wrap": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", @@ -48179,6 +51890,15 @@ "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==" }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.0" + } + }, "split2": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz", @@ -48197,6 +51917,12 @@ "es5-ext": "^0.10.53" } }, + "sqlstring": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.2.tgz", + "integrity": "sha512-vF4ZbYdKS8OnoJAWBmMxCQDkiEBkGQYU7UZPtL8flbDRSNkhaXvRJ279ZtI6M+zDaQovVU4tuRgzK5fVhvFAhg==", + "dev": true + }, "sshpk": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", @@ -48296,6 +52022,84 @@ } } }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==", + "dev": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, "statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -48994,6 +52798,38 @@ "resolved": "https://registry.npmjs.org/to-no-case/-/to-no-case-1.0.2.tgz", "integrity": "sha512-Z3g735FxuZY8rodxV4gH7LxClE4H0hTIyHNIHdk+vpQxjLm0cwnKXq/OFVZ76SOQmto7txVcwSCwkU5kqp+FKg==" }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -49257,6 +53093,12 @@ } } }, + "unc-path-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==", + "dev": true + }, "uncontrollable": { "version": "7.2.1", "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", @@ -49305,6 +53147,26 @@ "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==" }, + "union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + } + } + }, "unique-filename": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", @@ -49332,6 +53194,46 @@ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "dev": true }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==", + "dev": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==", + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==", + "dev": true + } + } + }, "untildify": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", @@ -49354,6 +53256,12 @@ "punycode": "^2.1.0" } }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==", + "dev": true + }, "url": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", @@ -49385,6 +53293,12 @@ "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==" }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true + }, "util": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", @@ -49435,6 +53349,15 @@ "convert-source-map": "^1.6.0" } }, + "v8flags": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", + "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", + "dev": true, + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", From 5abf6a515a0d70446ba10e98f600c5aea568d3f8 Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Thu, 23 Feb 2023 19:09:59 -0500 Subject: [PATCH 36/43] EDSC-3612: updated exportSearch action and test --- .../js/actions/__tests__/exportSearch.test.js | 79 +++++++++++++------ static/src/js/actions/exportSearch.js | 5 +- 2 files changed, 59 insertions(+), 25 deletions(-) diff --git a/static/src/js/actions/__tests__/exportSearch.test.js b/static/src/js/actions/__tests__/exportSearch.test.js index 52954f276c..74380b7374 100644 --- a/static/src/js/actions/__tests__/exportSearch.test.js +++ b/static/src/js/actions/__tests__/exportSearch.test.js @@ -9,6 +9,14 @@ import { } from '../exportSearch' import { EXPORT_FINISHED, EXPORT_STARTED } from '../../constants/actionTypes' +import * as sharedConfig from '../../../../../sharedUtils/config' + + +const MOCK_AUTH_TOKEN = '7SDFGI856gi12s36x7fe5duv5fb6bgds' +const MOCK_KEY = '00000000-0000-0000-0000-000000000000' +const MOCK_SIGNED_URL = 'http://0.0.0.0:5000/fake-signed-url' + +const defaultApplicationConfig = sharedConfig.getApplicationConfig() const mockStore = configureMockStore([thunk]) @@ -16,6 +24,10 @@ beforeEach(() => { jest.clearAllMocks() }) +afterEach(() => { + jest.clearAllMocks() +}) + describe('onExportStarted', () => { test('should create an action to update the store', () => { const payload = 'json' @@ -42,11 +54,17 @@ describe('onExportFinished', () => { describe('exportSearch', () => { test('calls lambda to get csv search export', async () => { - const createObjectMock = jest.fn() - window.URL.createObjectURL = createObjectMock + jest.spyOn(sharedConfig, 'getApplicationConfig').mockImplementation(() => ({ + ...defaultApplicationConfig, + exportStatusRefreshTime: 1000 // prevents test timeout + })) + + let clicked = false jest.spyOn(document, 'createElement').mockImplementation(() => ({ setAttribute: jest.fn(), - click: jest.fn(), + click: () => { + clicked = true + }, parentNode: { removeChild: jest.fn() } @@ -55,15 +73,21 @@ describe('exportSearch', () => { nock(/localhost/) .post(/export/) - .reply(200, [ - { - mock: 'data' - } - ]) + .reply(200, { + key: MOCK_KEY + }) + + + nock(/localhost/) + .post(/export-check/) + .reply(200, { + state: 'DONE', + url: MOCK_SIGNED_URL + }) // mockStore with initialState const store = mockStore({ - authToken: '' + authToken: MOCK_AUTH_TOKEN }) // call the dispatch @@ -71,17 +95,22 @@ describe('exportSearch', () => { const storeActions = store.getActions() expect(storeActions[0]).toEqual({ type: EXPORT_STARTED, payload: 'csv' }) expect(storeActions[1]).toEqual({ type: EXPORT_FINISHED, payload: 'csv' }) - - expect(createObjectMock).toHaveBeenCalledTimes(1) + expect(clicked).toEqual(true) }) }) test('calls lambda to get json search export', async () => { - const createObjectMock = jest.fn() - window.URL.createObjectURL = createObjectMock + jest.spyOn(sharedConfig, 'getApplicationConfig').mockImplementation(() => ({ + ...defaultApplicationConfig, + exportStatusRefreshTime: 1000 // prevents test timeout + })) + + let clicked = false jest.spyOn(document, 'createElement').mockImplementation(() => ({ setAttribute: jest.fn(), - click: jest.fn(), + click: () => { + clicked = true + }, parentNode: { removeChild: jest.fn() } @@ -90,15 +119,20 @@ describe('exportSearch', () => { nock(/localhost/) .post(/export/) - .reply(200, [ - { - mock: 'data' - } - ]) + .reply(200, { + key: MOCK_KEY + }) + + nock(/localhost/) + .post(/export-check/) + .reply(200, { + state: 'DONE', + url: MOCK_SIGNED_URL + }) // mockStore with initialState const store = mockStore({ - authToken: '' + authToken: MOCK_AUTH_TOKEN }) // call the dispatch @@ -106,8 +140,7 @@ describe('exportSearch', () => { const storeActions = store.getActions() expect(storeActions[0]).toEqual({ type: EXPORT_STARTED, payload: 'json' }) expect(storeActions[1]).toEqual({ type: EXPORT_FINISHED, payload: 'json' }) - - expect(createObjectMock).toHaveBeenCalledTimes(1) + expect(clicked).toEqual(true) }) }) @@ -121,7 +154,7 @@ describe('exportSearch', () => { .reply(200) const store = mockStore({ - authToken: '' + authToken: MOCK_AUTH_TOKEN }) const consoleMock = jest.spyOn(console, 'error').mockImplementation(() => jest.fn()) diff --git a/static/src/js/actions/exportSearch.js b/static/src/js/actions/exportSearch.js index 929a467042..645530379c 100644 --- a/static/src/js/actions/exportSearch.js +++ b/static/src/js/actions/exportSearch.js @@ -167,9 +167,9 @@ export const exportSearch = (format) => (dispatch, getState) => { // key is a random uuid for this specific export request // we use it to poll the status of the export later const { key } = response.data - if (!key) throw Error('server did not response with a uuid for the export request, which we need in order to find the download') + if (!key) throw Error('server did not respond with a uuid for the export request, which we need in order to find the download') - const { exportStatusRefreshTime = 5 * 1000 } = getApplicationConfig() + const { exportStatusRefreshTime = 5000 } = getApplicationConfig() const [error, signedUrl] = await new Promise((resolve) => { const intervalId = setInterval(async () => { @@ -207,6 +207,7 @@ export const exportSearch = (format) => (dispatch, getState) => { } else { // Create a hyperlink to the export and give it a filename const link = document.createElement('a') + link.href = signedUrl link.setAttribute('download', `edsc_collection_results_export.${format}`) From b48ee26af6e0bd4f35587ef5ce664401877f041f Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Fri, 24 Feb 2023 12:08:20 -0500 Subject: [PATCH 37/43] EDSC-3612: updated export search tests to work async and added s3:reset script --- package.json | 1 + .../exportSearch/__tests__/handler.test.js | 49 ++++++++++++------- .../__tests__/handler.test.js | 22 ++++++--- .../__tests__/handler.test.js | 27 ++++++++-- 4 files changed, 69 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index a71f27fcb2..42c594010a 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "cypress:prepare-ci": "bin/cypress-prepare-ci", "s3": "EXTRA_CORS_ALLOWED_ORIGINS=http://localhost:5000 MOTO_ALLOW_NONEXISTENT_REGION=True moto_server -H 0.0.0.0", "s3:install": "brew install moto", + "s3:reset": "curl -X POST http://localhost:5000/moto-api/reset", "sqs": "java -jar elasticmq-server-1.3.9.jar", "sqs:install": "curl https://s3-eu-west-1.amazonaws.com/softwaremill-public/elasticmq-server-1.3.9.jar --output elasticmq-server-1.3.9.jar" }, diff --git a/serverless/src/exportSearch/__tests__/handler.test.js b/serverless/src/exportSearch/__tests__/handler.test.js index 31cc99ccca..d510d819cf 100644 --- a/serverless/src/exportSearch/__tests__/handler.test.js +++ b/serverless/src/exportSearch/__tests__/handler.test.js @@ -13,11 +13,11 @@ const OLD_ENV = process.env const MOCK_REGION = 'moon' const S3_TEST_PORT = 5000 const S3_TEST_HOST = `0.0.0.0:${S3_TEST_PORT}` -const S3_TEST_LOCALHOST = `localhost:${S3_TEST_PORT}` -const S3_TEST_BUCKET_NAME = 'S3_TEST_BUCKET_NAME' +const S3_TEST_BUCKET_NAME = 'test-export-search-bucket' const S3_TEST_ENDPOINT = `http://${S3_TEST_HOST}` const MOCK_REQUEST_ID = 'MOCK_REQUEST_ID' const MOCK_USER_ID = 1234 +const S3_HOST_REGEX = /(localhost|127.0.0.1|0.0.0.0):5000/ // need to configure here because the aws-sdk expects it // without it, the handler will throw an error @@ -27,21 +27,14 @@ AWS.config.update({ region: MOCK_REGION }) -const s3 = new AWS.S3({ endpoint: S3_TEST_ENDPOINT }) +const s3 = new AWS.S3({ + endpoint: S3_TEST_ENDPOINT, + s3ForcePathStyle: true +}) let mockDb, mockDbConnection beforeAll(async () => { - // explicitly allow network connections to local mock S3 server - nock.enableNetConnect(host => host === S3_TEST_HOST || host === S3_TEST_LOCALHOST) - - await s3.createBucket({ - Bucket: S3_TEST_BUCKET_NAME, - CreateBucketConfiguration: { - LocationConstraint: MOCK_REGION - } - }).promise(); - // create test database mockDb = newDb() @@ -62,17 +55,23 @@ beforeAll(async () => { }) afterAll(async () => { - await deleteBucket(s3, S3_TEST_BUCKET_NAME) - - // re-disable all network connections, including those to localhost and 0.0.0.0 nock.disableNetConnect() }) -beforeEach(() => { +beforeEach(async () => { jest.clearAllMocks() jest.spyOn(getDbConnection, 'getDbConnection').mockImplementationOnce(() => mockDbConnection) + nock.enableNetConnect(S3_HOST_REGEX) + + await s3.createBucket({ + Bucket: S3_TEST_BUCKET_NAME, + CreateBucketConfiguration: { + LocationConstraint: MOCK_REGION + } + }).promise(); + // Manage resetting ENV variables // TODO: This is causing problems with mocking knex but is noted as important for managing process.env // jest.resetModules() @@ -80,15 +79,23 @@ beforeEach(() => { delete process.env.NODE_ENV }) -afterEach(() => { +afterEach(async () => { // Restore any ENV variables overwritten in tests process.env = OLD_ENV // just in case, for safety jest.clearAllMocks() + // re-enable connections to the local S3 instance + // in case another test re-disabled connections + nock.enableNetConnect(S3_HOST_REGEX) + + await deleteBucket(s3, S3_TEST_BUCKET_NAME) + // clear in-memory database table mockDb.public.none('DELETE FROM exports') + + nock.disableNetConnect() }) describe('exportSearch', () => { @@ -175,7 +182,10 @@ describe('exportSearch', () => { await exportSearch(event, {}) + nock.enableNetConnect(S3_HOST_REGEX) // allow connections to local S3 const obj = await s3.getObject({ Bucket: S3_TEST_BUCKET_NAME, Key: key }).promise() + nock.disableNetConnect() + expect(obj.ContentType).toEqual('text/csv'); expect(obj.Body.toString()).toEqual('Data Provider,Short Name,Version,Entry Title,Processing Level,Platform,Start Time,End Time\r\n,,,Test collection,,platform,,\r\n,,,Test collection 1,,platform,,\r\n') @@ -275,7 +285,10 @@ describe('exportSearch', () => { await exportSearch(event, {}) + nock.enableNetConnect(S3_HOST_REGEX) // allow connections to local S3 const obj = await s3.getObject({ Bucket: S3_TEST_BUCKET_NAME, Key: key }).promise() + nock.disableNetConnect() + expect(obj.ContentType).toEqual('application/json'); expect(JSON.parse(obj.Body.toString())).toEqual([{ "conceptId": "C100000-EDSC", "title": "Test collection", "platforms": [{ "shortName": "platform" }] }, { "conceptId": "C100001-EDSC", "title": "Test collection 1", "platforms": [{ "shortName": "platform" }] }]) diff --git a/serverless/src/exportSearchCheck/__tests__/handler.test.js b/serverless/src/exportSearchCheck/__tests__/handler.test.js index 3da523fbf8..fc465821f8 100644 --- a/serverless/src/exportSearchCheck/__tests__/handler.test.js +++ b/serverless/src/exportSearchCheck/__tests__/handler.test.js @@ -18,9 +18,9 @@ const OLD_ENV = process.env const MOCK_REGION = 'moon' const S3_TEST_PORT = 5000 const S3_TEST_HOST = `0.0.0.0:${S3_TEST_PORT}` -const S3_TEST_LOCALHOST = `localhost:${S3_TEST_PORT}` -const S3_TEST_BUCKET_NAME = 'S3_TEST_BUCKET_NAME' +const S3_TEST_BUCKET_NAME = 'test-export-search-check-bucket' const S3_TEST_ENDPOINT = `http://${S3_TEST_HOST}` +const S3_HOST_REGEX = /(localhost|127.0.0.1|0.0.0.0):5000/ const MOCK_EXPORT_REQUEST_KEY = '123456789abcdefg' const MOCK_FILENAME_CSV = 'search_results_export_123456789.csv' @@ -38,16 +38,15 @@ AWS.config.update({ region: MOCK_REGION }) -const s3 = new AWS.S3({ endpoint: S3_TEST_ENDPOINT }) +const s3 = new AWS.S3({ + endpoint: S3_TEST_ENDPOINT, + s3ForcePathStyle: true +}) let mockDb, mockDbConnection beforeAll(async () => { - // explicitly allow network connections to local mock S3 server - nock.enableNetConnect(host => host === S3_TEST_HOST || host === S3_TEST_LOCALHOST) - - // see http://docs.getmoto.org/en/latest/docs/server_mode.html#reset-api - axios.post(`${S3_TEST_ENDPOINT}/moto-api/reset`) + nock.enableNetConnect(S3_HOST_REGEX) await s3.createBucket({ Bucket: S3_TEST_BUCKET_NAME, @@ -76,6 +75,8 @@ beforeAll(async () => { }) afterAll(async () => { + nock.enableNetConnect(S3_HOST_REGEX) + await deleteBucket(s3, S3_TEST_BUCKET_NAME) // re-disable all network connections, including those to localhost and 0.0.0.0 @@ -83,6 +84,8 @@ afterAll(async () => { }) beforeEach(() => { + nock.enableNetConnect(S3_HOST_REGEX) + process.env.searchExportBucket = S3_TEST_BUCKET_NAME process.env.searchExportS3Endpoint = S3_TEST_ENDPOINT @@ -100,6 +103,8 @@ beforeEach(() => { }) afterEach(async () => { + nock.enableNetConnect(S3_HOST_REGEX) + // Restore any ENV variables overwritten in tests process.env = OLD_ENV @@ -109,6 +114,7 @@ afterEach(async () => { // clear in-memory database table mockDb.public.none('DELETE FROM exports') + nock.disableNetConnect() }) describe('exportSearchCheck', () => { diff --git a/serverless/src/exportSearchRequest/__tests__/handler.test.js b/serverless/src/exportSearchRequest/__tests__/handler.test.js index 091c7a5d6b..034cf23a5b 100644 --- a/serverless/src/exportSearchRequest/__tests__/handler.test.js +++ b/serverless/src/exportSearchRequest/__tests__/handler.test.js @@ -16,9 +16,9 @@ const OLD_ENV = process.env const AWS_TEST_REGION = 'us-east-1' const SQS_TEST_PORT = 9324 const SQS_TEST_HOST = `0.0.0.0:${SQS_TEST_PORT}` -const SQS_TEST_LOCALHOST = `localhost:${SQS_TEST_PORT}` -const SQS_TEST_QUEUE_NAME = 'REQUEST_SEARCH_EXPORT_TEST_QUEUE' +const SQS_TEST_QUEUE_NAME = 'SEARCH_EXPORT_REQUEST_TEST_QUEUE' const SQS_TEST_ENDPOINT = `http://${SQS_TEST_HOST}` +const SQS_HOST_REGEX = /(localhost|127.0.0.1|0.0.0.0):9324/ const MOCK_USER_ID = 1234 const MOCK_KEY = '00000000-0000-0000-0000-000000000000' // see /__mocks__/crypto.js @@ -40,11 +40,14 @@ let mockDb, mockDbConnection let testSearchExportQueueUrl beforeAll(async () => { // explicitly allow network connections to ElasticMQ (SQS-Compatible) server - nock.enableNetConnect(host => host === SQS_TEST_HOST || host === SQS_TEST_LOCALHOST) + nock.enableNetConnect(SQS_HOST_REGEX) // create a queue and save the url to it const { QueueUrl } = await sqs.createQueue({ QueueName: SQS_TEST_QUEUE_NAME }).promise() + // re-disable network connections after creating the queue + nock.disableNetConnect() + // we save the url here, so we can pass it to the handler via an environmental variable testSearchExportQueueUrl = QueueUrl @@ -68,6 +71,9 @@ beforeAll(async () => { }) afterAll(async () => { + // re-enable network connections to local SQS + nock.enableNetConnect(SQS_HOST_REGEX) + await sqs.deleteQueue({ QueueUrl: testSearchExportQueueUrl }).promise() // re-disable all network connections, including those to localhost and 0.0.0.0 @@ -75,6 +81,9 @@ afterAll(async () => { }) beforeEach(async () => { + // explicitly allow network connections to ElasticMQ (SQS-Compatible) server + nock.enableNetConnect(SQS_HOST_REGEX) + jest.clearAllMocks() jest.spyOn(deployedEnvironment, 'deployedEnvironment').mockImplementation(() => 'prod') @@ -104,6 +113,9 @@ afterEach(() => { // clear in-memory database table mockDb.public.none('DELETE FROM exports') + + // re-disable all network connections, including those to localhost and 0.0.0.0 + nock.disableNetConnect() }) describe('exportSearch', () => { @@ -131,7 +143,10 @@ describe('exportSearch', () => { expect(result.body).toEqual(`{"key":"${MOCK_KEY}"}`) + nock.enableNetConnect(SQS_HOST_REGEX) const { Messages } = await sqs.receiveMessage({ QueueUrl: testSearchExportQueueUrl }).promise() + nock.disableNetConnect() + expect(Messages).toHaveLength(1) const message = JSON.parse(Messages[0].Body); @@ -187,7 +202,10 @@ describe('exportSearch', () => { expect(result.body).toEqual(`{"key":"${MOCK_KEY}"}`) + nock.enableNetConnect(SQS_HOST_REGEX) const { Messages } = await sqs.receiveMessage({ QueueUrl: testSearchExportQueueUrl }).promise() + nock.disableNetConnect() + expect(Messages).toHaveLength(1) const message = JSON.parse(Messages[0].Body); @@ -241,8 +259,9 @@ describe('exportSearch', () => { expect(errorMessage).toEqual('SyntaxError: Unexpected end of JSON input') - // retrieve message from queue + nock.enableNetConnect(SQS_HOST_REGEX) const { Messages = [] } = await sqs.receiveMessage({ QueueUrl: testSearchExportQueueUrl }).promise() + nock.disableNetConnect() // check that no message was created expect(Messages).toHaveLength(0) From 4171a9f3fa6b408c39a7b1293d0623fb8c1d0e97 Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Fri, 24 Feb 2023 14:36:58 -0500 Subject: [PATCH 38/43] EDSC-3612: updated github ci workflow to install and run ElasticMQ and moto --- .github/workflows/ci.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8784a7ad92..d18453312f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,10 +34,18 @@ jobs: ${{ runner.os }}- - name: Install dependencies run: npm ci + - name: Install local S3 + run: npm run s3:install + - name: Install local SQS + run: npm run sqs:install - name: Copy secrets example file run: npm run copy-secrets - name: Run Jest tests - run: npm run silent-test -- --shard=${{ matrix.shard }} + run: | + npm run s3 & + npm run sqs & + sleep 5 + npm run silent-test -- --shard=${{ matrix.shard }} - name: Upload coverage to codecov uses: codecov/codecov-action@v3 cypress: From 9951e4105c9eaa17efa56517233f5caca7078797 Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Fri, 24 Feb 2023 18:42:20 -0500 Subject: [PATCH 39/43] EDSC-3612: install moto with pip --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d18453312f..a6001f5b05 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,8 +34,9 @@ jobs: ${{ runner.os }}- - name: Install dependencies run: npm ci + - uses: actions/setup-python@v4 - name: Install local S3 - run: npm run s3:install + run: pip install moto[server] - name: Install local SQS run: npm run sqs:install - name: Copy secrets example file From 384c9ffbed4dd311003af177433d291fcdf859e7 Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Sat, 25 Feb 2023 10:57:13 -0500 Subject: [PATCH 40/43] EDSC-3612: hopefully fixed edge cases around disabling/enabling net connections while running tests in parallel --- .../exportSearch/__tests__/handler.test.js | 10 --------- .../__tests__/handler.test.js | 4 ---- .../__tests__/handler.test.js | 21 ------------------- test-env.js | 10 +++++++++ 4 files changed, 10 insertions(+), 35 deletions(-) diff --git a/serverless/src/exportSearch/__tests__/handler.test.js b/serverless/src/exportSearch/__tests__/handler.test.js index d510d819cf..224c94f573 100644 --- a/serverless/src/exportSearch/__tests__/handler.test.js +++ b/serverless/src/exportSearch/__tests__/handler.test.js @@ -54,10 +54,6 @@ beforeAll(async () => { mockDbConnection = await mockDb.adapters.createKnex() }) -afterAll(async () => { - nock.disableNetConnect() -}) - beforeEach(async () => { jest.clearAllMocks() @@ -94,8 +90,6 @@ afterEach(async () => { // clear in-memory database table mockDb.public.none('DELETE FROM exports') - - nock.disableNetConnect() }) describe('exportSearch', () => { @@ -182,9 +176,7 @@ describe('exportSearch', () => { await exportSearch(event, {}) - nock.enableNetConnect(S3_HOST_REGEX) // allow connections to local S3 const obj = await s3.getObject({ Bucket: S3_TEST_BUCKET_NAME, Key: key }).promise() - nock.disableNetConnect() expect(obj.ContentType).toEqual('text/csv'); expect(obj.Body.toString()).toEqual('Data Provider,Short Name,Version,Entry Title,Processing Level,Platform,Start Time,End Time\r\n,,,Test collection,,platform,,\r\n,,,Test collection 1,,platform,,\r\n') @@ -285,9 +277,7 @@ describe('exportSearch', () => { await exportSearch(event, {}) - nock.enableNetConnect(S3_HOST_REGEX) // allow connections to local S3 const obj = await s3.getObject({ Bucket: S3_TEST_BUCKET_NAME, Key: key }).promise() - nock.disableNetConnect() expect(obj.ContentType).toEqual('application/json'); expect(JSON.parse(obj.Body.toString())).toEqual([{ "conceptId": "C100000-EDSC", "title": "Test collection", "platforms": [{ "shortName": "platform" }] }, { "conceptId": "C100001-EDSC", "title": "Test collection 1", "platforms": [{ "shortName": "platform" }] }]) diff --git a/serverless/src/exportSearchCheck/__tests__/handler.test.js b/serverless/src/exportSearchCheck/__tests__/handler.test.js index fc465821f8..447fa3d464 100644 --- a/serverless/src/exportSearchCheck/__tests__/handler.test.js +++ b/serverless/src/exportSearchCheck/__tests__/handler.test.js @@ -78,9 +78,6 @@ afterAll(async () => { nock.enableNetConnect(S3_HOST_REGEX) await deleteBucket(s3, S3_TEST_BUCKET_NAME) - - // re-disable all network connections, including those to localhost and 0.0.0.0 - nock.disableNetConnect() }) beforeEach(() => { @@ -114,7 +111,6 @@ afterEach(async () => { // clear in-memory database table mockDb.public.none('DELETE FROM exports') - nock.disableNetConnect() }) describe('exportSearchCheck', () => { diff --git a/serverless/src/exportSearchRequest/__tests__/handler.test.js b/serverless/src/exportSearchRequest/__tests__/handler.test.js index 034cf23a5b..f59b677919 100644 --- a/serverless/src/exportSearchRequest/__tests__/handler.test.js +++ b/serverless/src/exportSearchRequest/__tests__/handler.test.js @@ -39,15 +39,9 @@ let mockDb, mockDbConnection let testSearchExportQueueUrl beforeAll(async () => { - // explicitly allow network connections to ElasticMQ (SQS-Compatible) server - nock.enableNetConnect(SQS_HOST_REGEX) - // create a queue and save the url to it const { QueueUrl } = await sqs.createQueue({ QueueName: SQS_TEST_QUEUE_NAME }).promise() - // re-disable network connections after creating the queue - nock.disableNetConnect() - // we save the url here, so we can pass it to the handler via an environmental variable testSearchExportQueueUrl = QueueUrl @@ -71,13 +65,7 @@ beforeAll(async () => { }) afterAll(async () => { - // re-enable network connections to local SQS - nock.enableNetConnect(SQS_HOST_REGEX) - await sqs.deleteQueue({ QueueUrl: testSearchExportQueueUrl }).promise() - - // re-disable all network connections, including those to localhost and 0.0.0.0 - nock.disableNetConnect() }) beforeEach(async () => { @@ -113,9 +101,6 @@ afterEach(() => { // clear in-memory database table mockDb.public.none('DELETE FROM exports') - - // re-disable all network connections, including those to localhost and 0.0.0.0 - nock.disableNetConnect() }) describe('exportSearch', () => { @@ -143,9 +128,7 @@ describe('exportSearch', () => { expect(result.body).toEqual(`{"key":"${MOCK_KEY}"}`) - nock.enableNetConnect(SQS_HOST_REGEX) const { Messages } = await sqs.receiveMessage({ QueueUrl: testSearchExportQueueUrl }).promise() - nock.disableNetConnect() expect(Messages).toHaveLength(1) @@ -202,9 +185,7 @@ describe('exportSearch', () => { expect(result.body).toEqual(`{"key":"${MOCK_KEY}"}`) - nock.enableNetConnect(SQS_HOST_REGEX) const { Messages } = await sqs.receiveMessage({ QueueUrl: testSearchExportQueueUrl }).promise() - nock.disableNetConnect() expect(Messages).toHaveLength(1) @@ -259,9 +240,7 @@ describe('exportSearch', () => { expect(errorMessage).toEqual('SyntaxError: Unexpected end of JSON input') - nock.enableNetConnect(SQS_HOST_REGEX) const { Messages = [] } = await sqs.receiveMessage({ QueueUrl: testSearchExportQueueUrl }).promise() - nock.disableNetConnect() // check that no message was created expect(Messages).toHaveLength(0) diff --git a/test-env.js b/test-env.js index 49166033bb..1a5a9b2658 100644 --- a/test-env.js +++ b/test-env.js @@ -33,6 +33,16 @@ console.error = consoleError nock.cleanAll() nock.disableNetConnect() +nock.enableNetConnect(host => { + // allow requests to local S3-compatible server (moto) + if (host.match(/^(localhost|127.0.0.1|0.0.0.0):5000/)) return true + + // allow requests to local SQS-compatible server (ElasticMQ) + if (host.match(/^(localhost|127.0.0.1|0.0.0.0):9324/)) return true + + return false +}) + // Mock toast provider window.reactToastProvider = { current: { From 7336562bd84045e37e591ed492e365004764a60f Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Sat, 25 Feb 2023 11:54:01 -0500 Subject: [PATCH 41/43] EDSC-3612: clear nock interceptors before each search export-related test --- serverless/src/exportSearch/__tests__/handler.test.js | 9 ++------- .../src/exportSearchCheck/__tests__/handler.test.js | 9 +-------- .../src/exportSearchRequest/__tests__/handler.test.js | 4 +--- 3 files changed, 4 insertions(+), 18 deletions(-) diff --git a/serverless/src/exportSearch/__tests__/handler.test.js b/serverless/src/exportSearch/__tests__/handler.test.js index 224c94f573..0cf1ffc851 100644 --- a/serverless/src/exportSearch/__tests__/handler.test.js +++ b/serverless/src/exportSearch/__tests__/handler.test.js @@ -17,7 +17,6 @@ const S3_TEST_BUCKET_NAME = 'test-export-search-bucket' const S3_TEST_ENDPOINT = `http://${S3_TEST_HOST}` const MOCK_REQUEST_ID = 'MOCK_REQUEST_ID' const MOCK_USER_ID = 1234 -const S3_HOST_REGEX = /(localhost|127.0.0.1|0.0.0.0):5000/ // need to configure here because the aws-sdk expects it // without it, the handler will throw an error @@ -55,12 +54,12 @@ beforeAll(async () => { }) beforeEach(async () => { + nock.cleanAll() // remove any interceptors created by other files + jest.clearAllMocks() jest.spyOn(getDbConnection, 'getDbConnection').mockImplementationOnce(() => mockDbConnection) - nock.enableNetConnect(S3_HOST_REGEX) - await s3.createBucket({ Bucket: S3_TEST_BUCKET_NAME, CreateBucketConfiguration: { @@ -82,10 +81,6 @@ afterEach(async () => { // just in case, for safety jest.clearAllMocks() - // re-enable connections to the local S3 instance - // in case another test re-disabled connections - nock.enableNetConnect(S3_HOST_REGEX) - await deleteBucket(s3, S3_TEST_BUCKET_NAME) // clear in-memory database table diff --git a/serverless/src/exportSearchCheck/__tests__/handler.test.js b/serverless/src/exportSearchCheck/__tests__/handler.test.js index 447fa3d464..45ddcf41ef 100644 --- a/serverless/src/exportSearchCheck/__tests__/handler.test.js +++ b/serverless/src/exportSearchCheck/__tests__/handler.test.js @@ -20,7 +20,6 @@ const S3_TEST_PORT = 5000 const S3_TEST_HOST = `0.0.0.0:${S3_TEST_PORT}` const S3_TEST_BUCKET_NAME = 'test-export-search-check-bucket' const S3_TEST_ENDPOINT = `http://${S3_TEST_HOST}` -const S3_HOST_REGEX = /(localhost|127.0.0.1|0.0.0.0):5000/ const MOCK_EXPORT_REQUEST_KEY = '123456789abcdefg' const MOCK_FILENAME_CSV = 'search_results_export_123456789.csv' @@ -46,8 +45,6 @@ const s3 = new AWS.S3({ let mockDb, mockDbConnection beforeAll(async () => { - nock.enableNetConnect(S3_HOST_REGEX) - await s3.createBucket({ Bucket: S3_TEST_BUCKET_NAME, CreateBucketConfiguration: { @@ -75,13 +72,11 @@ beforeAll(async () => { }) afterAll(async () => { - nock.enableNetConnect(S3_HOST_REGEX) - await deleteBucket(s3, S3_TEST_BUCKET_NAME) }) beforeEach(() => { - nock.enableNetConnect(S3_HOST_REGEX) + nock.cleanAll() // remove any interceptors created by other files process.env.searchExportBucket = S3_TEST_BUCKET_NAME process.env.searchExportS3Endpoint = S3_TEST_ENDPOINT @@ -100,8 +95,6 @@ beforeEach(() => { }) afterEach(async () => { - nock.enableNetConnect(S3_HOST_REGEX) - // Restore any ENV variables overwritten in tests process.env = OLD_ENV diff --git a/serverless/src/exportSearchRequest/__tests__/handler.test.js b/serverless/src/exportSearchRequest/__tests__/handler.test.js index f59b677919..c35182fc60 100644 --- a/serverless/src/exportSearchRequest/__tests__/handler.test.js +++ b/serverless/src/exportSearchRequest/__tests__/handler.test.js @@ -18,7 +18,6 @@ const SQS_TEST_PORT = 9324 const SQS_TEST_HOST = `0.0.0.0:${SQS_TEST_PORT}` const SQS_TEST_QUEUE_NAME = 'SEARCH_EXPORT_REQUEST_TEST_QUEUE' const SQS_TEST_ENDPOINT = `http://${SQS_TEST_HOST}` -const SQS_HOST_REGEX = /(localhost|127.0.0.1|0.0.0.0):9324/ const MOCK_USER_ID = 1234 const MOCK_KEY = '00000000-0000-0000-0000-000000000000' // see /__mocks__/crypto.js @@ -69,8 +68,7 @@ afterAll(async () => { }) beforeEach(async () => { - // explicitly allow network connections to ElasticMQ (SQS-Compatible) server - nock.enableNetConnect(SQS_HOST_REGEX) + nock.cleanAll() // remove any interceptors created by other files jest.clearAllMocks() From c9bd85ce380bfc44cdd52c7ee61f364a43c109bf Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Sat, 25 Feb 2023 12:23:19 -0500 Subject: [PATCH 42/43] EDSC-3612: updated getSearchExportQueueUrl to return 0.0.0.0 when doing local development --- serverless/src/util/aws/getSearchExportQueueUrl.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/serverless/src/util/aws/getSearchExportQueueUrl.js b/serverless/src/util/aws/getSearchExportQueueUrl.js index 50d5c332de..1bb3a04bb6 100644 --- a/serverless/src/util/aws/getSearchExportQueueUrl.js +++ b/serverless/src/util/aws/getSearchExportQueueUrl.js @@ -7,7 +7,7 @@ export default function getSearchExportQueueUrl() { if (process.env.IS_OFFLINE) { // If we are running locally offline, this is the queueUrl - queueUrl = 'http://localhost:9324/queue/earthdata-search-dev-SearchExportQueue' + queueUrl = 'http://0.0.0.0:9324/queue/earthdata-search-dev-SearchExportQueue' } return queueUrl From 7ad1d176c44697b2f2ad422c4ffe4d844cd40671 Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Sat, 25 Feb 2023 12:39:01 -0500 Subject: [PATCH 43/43] EDSC-3612: make sure testSearchExportQueueUrl uses 0.0.0.0 not localhost --- serverless/src/exportSearchRequest/__tests__/handler.test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/serverless/src/exportSearchRequest/__tests__/handler.test.js b/serverless/src/exportSearchRequest/__tests__/handler.test.js index c35182fc60..fb5ed52171 100644 --- a/serverless/src/exportSearchRequest/__tests__/handler.test.js +++ b/serverless/src/exportSearchRequest/__tests__/handler.test.js @@ -42,7 +42,8 @@ beforeAll(async () => { const { QueueUrl } = await sqs.createQueue({ QueueName: SQS_TEST_QUEUE_NAME }).promise() // we save the url here, so we can pass it to the handler via an environmental variable - testSearchExportQueueUrl = QueueUrl + // we replace localhost with 0.0.0.0 to avoid conflicts with interceptors created by other tests + testSearchExportQueueUrl = QueueUrl.replace('localhost', '0.0.0.0') // create test database mockDb = newDb()